Skip to main content
.NETStandard2.0

An implementation of the mediator pattern in .NET using source-generation.

Published
Jan 15, 2024
2y ago
Total Downloads
283.3K
283,328

Install

dotnet add package Immediate.Handlers --version 0.3.0
ProductVersions
.NET Standard
netstandard2.0

README

Immediate.Handlers

NuGet GitHub release GitHub license GitHub issues GitHub issues-closed GitHub Actions

Immediate.Handlers is an implementation of the mediator pattern in .NET using source-generation. All pipeline behaviors are determined and the call-tree built at compile-time; meaning that all dependencies are enforced via compile-time safety checks. Behaviors and dependencies are obtained via DI at runtime based on compile-time determined dependencies.

Examples

Installing Immediate.Handlers

You can install Immediate.Handlers with NuGet:

Install-Package Immediate.Handlers

Or via the .NET Core command line interface:

dotnet add package Immediate.Handlers

Either commands, from Package Manager Console or .NET Core CLI, will download and install Immediate.Handlers.

Using Immediate.Handlers

Creating Handlers

Create a Handler by adding the following code:

[Handler]
public static class GetUsersQuery
{
    public record Query;

    private static ValueTask<IEnumerable<User>> HandleAsync(
        Query _,
        UsersService usersService,
        CancellationToken token)
    {
        return usersService.GetUsers();
    }
}

This will automatically create a new class, GetUsersQuery.Handler, which encapsulates the following:

  • attaching any behaviors defined for all queries in the assembly
  • using a class to receive any DI services, such as UsersService

Any consumer can now do the following:

public class Consumer(GetUsersQuery.Handler handler)
{
	public async Task Consumer(CancellationToken token)
	{
		var response = await handler.HandleAsync(new(), token);
		// do something with response
	}
}

In case your project layout does not allow direct for references between consumer and handler, the handler will also be registered as an IHandler<TRequest, Response>.

public class Consumer(IHandler<Query, IEnumerable<User>> handler)
{
	public async Task Consumer(CancellationToken token)
	{
		var response = await handler.HandleAsync(new(), token);
		// do something with response
	}
}

Creating Behaviors

Create a behavior by implementing the Immediate.Handlers.Shared.Behaviors<,> class, as so:

public sealed class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
	: Behavior<TRequest, TResponse>
{
	public override async ValueTask<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken)
	{
		logger.LogInformation("LoggingBehavior.Enter");
		var response = await Next(request, cancellationToken);
		logger.LogInformation("LoggingBehavior.Exit");
		return response;
	}
}

This can be registered assembly-wide using:

[assembly: Behaviors(
	typeof(LoggingBehavior<,>)
)]

or on an individual handler using:

[Handler]
[Behavior(
	typeof(LoggingBehavior<,>)
)]
public static class GetUsersQuery
{
	// ..
}

Once added to the pipeline, the behavior will be called as part of the pipeline to handle a request.

Note: adding a [Behavior] attribute to a handler will disregard all assembly-wide behaviors for that handler, so any global behaviors necessary must be independently added to the handler override behaviors list.

Behavior Constraints

A constraint can be added to a behavior by using:

public sealed class LoggingBehavior<TRequest, TResponse>
	: Behavior<TRequest, TResponse>
	where TRequest : IRequestConstraint
	where TResponse : IResponseConstraint

When a pipeline is generated, all potential behaviors are evaluated against the request and response types, and if either type does not match a given constraint, the behavior is not added to the generated pipeline.

Registering with IServiceCollection

Immediate.Handlers supports Microsoft.Extensions.DependencyInjection.Abstractions directly.

Registering Handlers
services.AddHandlers();

This registers all classes in the assembly marked with [Handler].

Registering Behaviors
services.AddBehaviors();

This registers all behaviors referenced in any [Behaviors] attribute.

Performance Comparisons

All performance benchmarks reported use the following environment:

// * Summary *

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22621.3007/22H2/2022Update/SunValley2)
12th Gen Intel Core i7-12700H, 1 CPU, 20 logical and 14 physical cores
.NET SDK 8.0.101
  [Host]     : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Benchmarks.Simple

This benchmark tests the various mediator implementations with a single request/response handler.

Method Mean Error Ratio Rank Allocated
SendRequest_Baseline 0.7701 ns 0.0180 ns 1.00 1 -
SendRequest_IHandler 15.6780 ns 0.0476 ns 20.36 2 -
SendRequest_ImmediateHandler 16.6023 ns 0.0561 ns 21.56 3 -
SendRequest_Mediator 27.2993 ns 0.4269 ns 35.49 4 -
SendRequest_IMediator 31.3420 ns 0.1006 ns 40.64 5 -
SendRequest_MediatR 68.3384 ns 0.3453 ns 88.73 6 240 B
Benchmarks.Large

This benchmark tests the various mediator implementations in the face of 999 request/response handlers.

Method Mean Error Ratio Rank Allocated
SendRequest_Baseline 0.5656 ns 0.0252 ns 1.00 1 -
SendRequest_ImmediateHandler 15.4346 ns 0.0516 ns 27.34 2 -
SendRequest_IHandler 16.0959 ns 0.0552 ns 28.50 3 -
SendRequest_Mediator 27.4104 ns 0.0449 ns 48.54 4 -
SendRequest_MediatR 80.0953 ns 0.4749 ns 141.83 5 240 B
SendRequest_IMediator 435.3890 ns 1.6399 ns 771.01 6 -
Benchmarks.Behaviors

This benchmark tests a more realistic scenario of using 1 behavior and 1 service.

Method Mean Error Ratio Rank Allocated
SendRequest_Baseline 56.71 ns 0.174 ns 1.00 1 40 B
SendRequest_IHandler 78.90 ns 0.304 ns 1.39 2 40 B
SendRequest_ImmediateHandler 80.02 ns 0.288 ns 1.41 3 40 B
SendRequest_Mediator 101.23 ns 0.263 ns 1.78 4 40 B
SendRequest_IMediator 104.92 ns 0.297 ns 1.85 5 40 B
SendRequest_MediatR 201.27 ns 1.023 ns 3.55 6 560 B

Changes from Previous Version

No structural changes between v0.2.0 and v0.3.0.

Adoption Assessment

Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.

Compatibility metadata lists 1 target framework; confirm the exact target used by your application.

Related Guides & Workflows

Learn how to evaluate, integrate, or audit this package in your .NET project.