DispatchR Documentation

repository·main·Indexed 18 days ago

https://github.com/hasanxdev/dispatchr

A high-performance mediator implementation for .NET designed to minimize memory footprint and maximize execution speed. DispatchR eliminates runtime reflection after registration and supports optimized return types like ValueTask and IAsyncEnumerable for stream requests. It features a Chain of Responsibility pattern for pipeline behaviors, support for notifications with open-generic handlers, and flexible registration options via AddDispatchR, including fine-grained control over pipeline order and handler inclusion/exclusion.

Tokens
3.9K
Snippets
11
Records
11
Agent score
14%

What's inside DispatchR

  1. Implement Pipeline Behavior in DispatchR

    main

    DispatchR uses the Chain of Responsibility pattern for pipeline behaviors. Instead of a delegate next(), you must inject the NextPipeline handler and call its Handle method.

    To create a generic pipeline behavior that applies to all requests of a specific return type (e.g., all ValueTask requests), use the following pattern:

    // Specific behavior
    public sealed class LoggingBehaviorDispatchR : IPipelineBehavior<PingDispatchR, ValueTask<int>>
    {
        public required IRequestHandler<PingDispatchR, ValueTask<int>> NextPipeline { get; set; }
    
        public ValueTask<int> Handle(PingDispatchR request, CancellationToken cancellationToken)
        {
            return NextPipeline.Handle(request, cancellationToken);
        }
    }
    
    // Generic behavior for all ValueTask requests
    public class GenericPipelineBehavior<TRequest, TResponse>() : IPipelineBehavior<TRequest, ValueTask<TResponse>>
        where TRequest : class, IRequest<TRequest, ValueTask<TResponse>>
    {
        public required IRequestHandler<TRequest, ValueTask<TResponse>> NextPipeline { get; set; }
        
        public ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
        {
            return NextPipeline.Handle(request, cancellationToken);
        }
    }
  2. How DispatchR pipeline behaviors work

    main

    DispatchR implements pipeline behaviors by chaining them at resolution time through Dependency Injection. When a request is made, the DI container resolves a set of objects (the handler and its associated pipeline behaviors).

    DispatchR then iterates through these resolved objects and uses a SetNext mechanism to link them into a chain. This design avoids the use of static lists or heavy reflection during the actual request execution, contributing to its high performance.

    Key characteristics:

    • DI Caching: Handlers are cached via DI. In scoped scenarios, the handler object is constructed once and reused.
    • Chaining: The first object in the resolved set is the actual handler, and subsequent objects are the pipeline behaviors that wrap the handler.
    // Conceptual logic of how pipelines are chained during DI resolution
    services.AddScoped(handlerInterface, sp =>
    {
        var pipelinesWithHandler = Unsafe.As<IRequestHandler[]>(sp.GetKeyedServices<IRequestHandler>(key));
        
        IRequestHandler lastPipeline = pipelinesWithHandler[0];
        for (int i = 1; i < pipelinesWithHandler.Length; i++)
        {
            var pipeline = pipelinesWithHandler[i];
            pipeline.SetNext(lastPipeline);
            lastPipeline = pipeline;
        }
    
        return lastPipeline;
    });
  3. Install DispatchR.Mediator

    main

    To use the full DispatchR mediator implementation, install the main package via NuGet. If you only need the interfaces and abstractions (e.g., for a separate layer in your architecture), you can install the abstractions package instead.

    # Install the full implementation
    dotnet add package DispatchR.Mediator
    
    # OR install only the abstractions
    dotnet add package DispatchR.Mediator.Abstractions
  4. Configure DispatchR in .NET

    main

    To use DispatchR, register it in your service collection using AddDispatchR. You can register handlers by providing an assembly, or use a configuration delegate for fine-grained control over pipelines, notifications, and handler filtering.

    Quick Registration

    By default, this enables automatic registration of pipelines and notifications:

    builder.Services.AddDispatchR(typeof(MyCommand).Assembly, withPipelines: true, withNotifications: true);

    Advanced Configuration

    Use the ConfigurationOptions delegate to specify assemblies, define the order of pipeline behaviors, or include/exclude specific handlers. This is useful when working with environments like .NET Aspire where you might need to limit handler registration.

    Manual Registration

    If you need complete control or want to bypass the automatic assembly scanning, you can disable automatic registration and add your pipelines, notification handlers, or stream pipeline behaviors manually using standard DI registration.

    // Quick setup
    builder.Services.AddDispatchR(typeof(MyCommand).Assembly, withPipelines: true, withNotifications: true);
    
    // Advanced setup
    builder.Services.AddDispatchR(options =>
    {
        options.Assemblies.Add(typeof(DispatchRSample.Ping).Assembly);
        options.RegisterPipelines = true;
        options.RegisterNotifications = true;
        options.PipelineOrder = 
        [
            typeof(DispatchRSample.FirstPipelineBehavior),
            typeof(DispatchRSample.SecondPipelineBehavior),
            typeof(DispatchRSample.GenericPipelineBehavior<,>)
        ];
        options.IncludeHandlers = null;
        options.ExcludeHandlers = null;
    });
    
    // Manual registration
    builder.Services.AddDispatchR(typeof(MyCommand).Assembly, withPipelines: false, withNotifications: false);
    builder.Services.AddScoped<IPipelineBehavior<MyCommand, int>, PipelineBehavior>();
    builder.Services.AddScoped<IPipelineBehavior<MyCommand, int>, ValidationBehavior>();
    builder.Services.AddScoped<IStreamPipelineBehavior<MyStreamCommand, int>, ValidationBehavior>();
    builder.Services.AddScoped<INotificationHandler<Event>, EventHandler>();
  5. Include only specific handlers using IncludeHandlers

    main

    If you want to restrict registration to a specific subset of handlers from a shared assembly, use the IncludeHandlers option. When IncludeHandlers is used, only the types explicitly listed in this collection will be registered, effectively ignoring all other handlers found in the specified assemblies.

    builder.Services.AddDispatchR(options =>
    {
        options.Assemblies.Add(Assembly.Load("AspireModularSample.Modules"));
        options.IncludeHandlers = [typeof(PongHandler)];
    });
  6. Exclude specific handlers using ExcludeHandlers

    main

    In modular architectures where you want to register most handlers from a shared assembly but skip specific ones, use the ExcludeHandlers option within the AddDispatchR configuration. This prevents the specified handler types from being registered in the DI container, even if their assembly is included.

    builder.Services.AddDispatchR(options =>
    {
        options.Assemblies.Add(Assembly.Load("AspireModularSample.Modules"));
        options.ExcludeHandlers = [typeof(PongHandler)];
    });
  7. Configure Pipeline Order via ConfigurationOptions

    main

    When using the AddDispatchR configuration delegate, you can explicitly define the execution order of your pipeline behaviors using the PipelineOrder property. This property accepts an array of types representing the behaviors in the order they should be executed.

    options.PipelineOrder = 
    [
        typeof(DispatchRSample.FirstPipelineBehavior),
        typeof(DispatchRSample.SecondPipelineBehavior),
        typeof(DispatchRSample.GenericPipelineBehavior<,>)
    ];
  8. Define and Handle Stream Requests

    main

    Stream requests allow for asynchronous streaming of results using IAsyncEnumerable<T>.

    1. Request: Implement IStreamRequest<TRequest, TResponse>.
    2. Handler: Implement IStreamRequestHandler<TRequest, TResponse> returning IAsyncEnumerable<TResponse>.
    3. Pipeline: Implement IStreamPipelineBehavior<TRequest, TResponse> using the Chain of Responsibility pattern by calling NextPipeline.Handle.
    // Request
    public sealed class CounterStreamRequestDispatchR : IStreamRequest<CounterStreamRequestDispatchR, int> { }
    
    // Handler
    public sealed class CounterStreamHandlerDispatchR : IStreamRequestHandler<CounterStreamRequestDispatchR, int>
    {
        public async IAsyncEnumerable<int> Handle(CounterStreamRequestDispatchR request, CancellationToken cancellationToken)
        {
            yield return 1;
        }
    }
    
    // Pipeline
    public sealed class CounterPipelineStreamHandler : IStreamPipelineBehavior<CounterStreamRequestDispatchR, string>
    {
        public required IStreamRequestHandler<CounterStreamRequestDispatchR, string> NextPipeline { get; set; }
        
        public async IAsyncEnumerable<string> Handle(CounterStreamRequestDispatchR request, [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await foreach (var response in NextPipeline.Handle(request, cancellationToken).ConfigureAwait(false))
            {
                yield return response;
            }
        }
    }
  9. Define a Simple Request in DispatchR

    main

    Unlike MediatR, where the request defines the return type via a generic parameter on the interface, DispatchR requires the request to explicitly define both itself and its return type (e.g., Task, ValueTask, or a synchronous type) within the IRequest interface.

    Important: Always use a generic return type like Task<TResult> or ValueTask<TResult>. Using a bare Task or ValueTask without a type argument may prevent the handler from being triggered when pipeline behaviors or validators are present.

    // DispatchR request definition
    public sealed class PingDispatchR : IRequest<PingDispatchR, ValueTask<int>> { }
  10. Use Notifications and Open-Generic Handlers

    main

    Notifications are one-way messages that do not return a value. Implement INotification for the event and INotificationHandler<TNotification> for the handler.

    For cross-cutting concerns like logging or auditing, you can implement an open-generic handler that intercepts every notification type by using a generic constraint on INotification.

    // Notification
    public sealed record Event(Guid Id) : INotification;
    
    // Specific Handler
    public sealed class EventHandler(ILogger<Event> logger) : INotificationHandler<Event>
    {
        public ValueTask Handle(Event notification, CancellationToken cancellationToken)
        {
            return ValueTask.CompletedTask;
        }
    }
    
    // Open-generic handler for ALL notifications
    public sealed class AllNotificationsLogger<TNotification>(ILogger<AllNotificationsLogger<TNotification>> logger)
        : INotificationHandler<TNotification>
        where TNotification : INotification
    {
        public ValueTask Handle(TNotification notification, CancellationToken cancellationToken)
        {
            logger.LogInformation("Received notification of type {NotificationType}", typeof(TNotification).Name);
            return ValueTask.CompletedTask;
        }
    }
  11. Define a Simple Request Handler in DispatchR

    main

    Implement IRequestHandler<TRequest, TResponse> to handle requests. The Handle method signature must match the return type specified in the request definition.

    public class PingHandlerDispatchR : IRequestHandler<PingDispatchR, ValueTask<int>>
    {
        public ValueTask<int> Handle(PingDispatchR request, CancellationToken cancellationToken)
        {
            return ValueTask.FromResult(0);
        }
    }