Sharing Context in MediatR Pipelines

MediatR, a small library that implements the Mediator pattern, helps simplify scenarios when you want a simple in-memory request/response and notification implementation. Once you adopt its pattern, you'll often find many other related patterns start to show up - decorators, chains of responsibility, pattern matching, and more.

Everything starts with a very basic implementation - a handler for a request:

public interface IRequestHandler<in TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}

Then on top of calling into a handler, we might want to call things around our handler for cross-cutting concerns, which led to behaviors:

public interface IPipelineBehavior<in TRequest, TResponse>
{
    Task<TResponse> Handle(TRequest request, 
        CancellationToken cancellationToken, 
        RequestHandlerDelegate<TResponse> next);
}

For simple scenarios, where I want to execute something just before or after a handler, MediatR includes a built-in pipeline behavior with additional pre/post-processors:

public interface IRequestPreProcessor<in TRequest>
{
    Task Process(TRequest request, CancellationToken cancellationToken);
}

public interface IRequestPostProcessor<in TRequest, in TResponse>
{
    Task Process(TRequest request, TResponse response, 
        CancellationToken cancellationToken);
}

Basically, I'm recreating a lot of existing functional patterns in an OO language, using dependency injection.

Side note - it's possible to do functional patterns directly in C#, higher order functions, monads, partial application, currying, and more, but it's really, really ugly and not idiomatic C#.

Inevitably however, it becomes necessary to share information across behaviors/processors. What are our options here? In ASP.NET Core, we have filters. For example, an action filter:

public interface IAsyncActionFilter : IFilterMetadata
{
    Task OnActionExecutionAsync(ActionExecutingContext context, 
        ActionExecutionDelegate next);
}

This looks similar to our behavior, except with the first parameter. Our behaviors take simply a request object, while these filters have some sort of context object. Filters must explicitly use this context object for any kind of resolution of objects.

For each of our behaviors, we have either our request object, dependency injection, or service location available for us.

Request hijacking

One option available to us is to simply hijack the request object through some sort of base class:

public abstract class ContextualRequest<TResponse>
    : IRequest<TResponse>
{
    public IDictionary<string, object> Items { get; }
        = new Dictionary<string, object>();
}

We use the request object as the object to place any shared context items, with any behavior then putting/removing items on it:

public class AuthBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
      where TRequest : ContextualRequest<TResponse>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AuthBehavior(IHttpContextAccessor httpContextAccessor)
        => _httpContextAccessor = httpContextAccessor;

    public Task<TResponse> Handle(TRequest request, 
        CancellationToken cancellationToken, 
        RequestHandlerDelegate<TResponse> next)
    {
        request.Items["CurrentUser"] = _httpContextAccessor.User;

        return next;
    }
}

Here we're placing the current request user onto an items dictionary on the request object, so that subsequent behaviors/processors can then use that user object.

It's a bit ugly, however, since we're forcing a base class into our request objects, breaking the concept of "favor composition over inheritance". At the time of writing, the built-in DI container doesn't support this kind of open generics constraint, so you'd be forced to adopt a 3rd-party container (literally any of them, they all support this).

Service Location

In places where you don't really need DI, you can instead use service location to pluck out the current user and do something with it:

public class AuthBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
{
    public Task<TResponse> Handle(TRequest request, 
        CancellationToken cancellationToken, 
        RequestHandlerDelegate<TResponse> next)
    {
        var user = (IPrincipal) HttpContext.Items["CurrentUser"];

        if (!user.Principal.IsAuthenticated)
            return Task.FromResult<TResponse>(default);

        return next;
    }
}

I'd not recommend this unless your system doesn't have much DI going on, such as in ASP.NET Classic.

Finally, we can use dependency injection share information.

Dependency Injecting Context

Rather than hijacking our request (a crude form of partial application/currying), or service location, we can instead take advantage of dependency injection to inject a context object into any behavior/processor that needs it.

First, we'll need to define our dependency. A dictionary is good, but having a concrete type is better:

public class ItemsCache : Dictionary<string, object>
{
}

Then we can register our special context object in the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ItemsCache>();

Now we can add items to our cache directly:

public class AuthBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
      where TRequest : ContextualRequest<TResponse>
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ItemsCache _itemsCache;

    public AuthBehavior(IHttpContextAccessor httpContextAccessor,
        ItemsCache itemsCache)
    {
        _httpContextAccessor = httpContextAccessor;
        _itemsCache = itemsCache;
    }

    public Task<TResponse> Handle(TRequest request, 
        CancellationToken cancellationToken, 
        RequestHandlerDelegate<TResponse> next)
    {
        _itemsCache["CurrentUser"] = _httpContextAccessor.User;

        return next;
    }
}

Any behavior that wants to use or context object only needs to depend on it (composition over inheritance). We don't have to resort to funny generics business or base classes to do so. Nor do we need to modify our pipeline to have custom request objects (built-in or otherwise).

If you find yourself needing to share information across components/services/behaviors/filters in a request, custom scoped dependencies are a great means of doing so.