How I Upgrade my Code-Style of MediatR Pipeline using .NET 6

I want to share with you how .NET 6 features with the new MediatR.Extensions.AttributedBehaviors NuGet package helped me polish my MediatR pipelines.

How I Upgrade my Code-Style of MediatR Pipeline using .NET 6

What is MediatR

It is a popular library for C# created by Jimmy Boggard, which can be described as a simple Mediator Pattern implementation in .NET. MediatR NuGet package currently has almost 50 million downloads and still rising.

Wiki page on its GitHub says:

MediatR is a low-ambition library trying to solve a simple problem — decoupling the in-process sending of messages from handling messages.

The word in-process is essential. This is not a Message Broker as Kinesis or RabbitMQ. This is about sending the message from one place and handling such message in a different place, but still in one process.

I use MediatR for decoupling Command and Query Requests. So In my case, the MediatR message is Query or Command, depending on the direction of data flow. This architectural design is called Command-Query Separation, and I am explaining it in the first paragraph of this blog-post.

You can see how I can handle HTTP GET by defining a GetWarehousesQuery as a MediatR request (message), and GetWarehousesQueryHandler as its Handler in the code sample below.

using MediatR;
/*Other usings not included in sample*/

namespace WarehouseSystem
{    
    [ApiController]
    public class WarehousesController : AppControllerBase
    {
        public WarehousesController(IMediator mediator) : base(mediator)
        {
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<WarehouseListDto>>> GetWarehouses([FromQuery] GetWarehousesQuery query)
        {
            return Ok(await Mediator.Send(query));
        }
    }

    public class GetWarehousesQuery : IRequest<IEnumerable<WarehouseListDto>>{
      public bool OnlyInventoryWarehouses { get; set; }
    }

    public class GetWarehousesQueryHandler : IRequestHandler<GetWarehousesQuery, IEnumerable<WarehouseListDto>>
    {
        private readonly WarehouseDbContext _context;
        private readonly IMapper _mapper;

        public GetWarehousesQueryHandler(WarehouseDbContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        public async Task<IEnumerable<WarehouseListDto>> Handle(GetWarehousesQuery request, CancellationToken cancellationToken)
        {
            IQueryable<Warehouse> query = _context.Warehouses.Include(x => x.Stocks).AsNoTracking();

            if (request.OnlyInventoryWarehouses)
            {
                query = query.Where(w => w.Stocks.Any(a => a.IsInventory));
            }

            return await _mapper.ProjectTo<WarehouseListDto>(query).ToListAsync();
        }
    }
}

Upgrade Code with .NET 6 Features

Records

Actually records are with us since .NET 5 and C# 9, but it is related to the topic of a blog post so that I will introduce them to you too. The feature called Records. Records introduced a shorter syntax for defining an immutable class. I find helpful when I am declaring Command or Query class.

using MediatR;
/*Other usings not included in sample*/

namespace WarehouseSystem
{    
    public record  GetWarehousesQuery(bool OnlyInventoryWarehouses) : IRequest<IEnumerable<WarehouseListDto>>;

    public class GetWarehousesQueryHandler : IRequestHandler<GetWarehousesQuery, IEnumerable<WarehouseListDto>>
    {
        private readonly WarehouseDbContext _context;
        private readonly IMapper _mapper;

        public GetWarehousesQueryHandler(WarehouseDbContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        public async Task<IEnumerable<WarehouseListDto>> Handle(GetWarehousesQuery request, CancellationToken cancellationToken)
        {
            IQueryable<Warehouse> query = _context.Warehouses.Include(x => x.Stocks).AsNoTracking();

            if (request.OnlyInventoryWarehouses)
            {
                query = query.Where(w => w.Stocks.Any(a => a.IsInventory));
            }

            return await _mapper.ProjectTo<WarehouseListDto>(query).ToListAsync();
        }
    }
}

Look at line 8. The definition of GetWarehousesQuery is now one-line code. I like to have Query and Command classes with theirs Handler classes in one place, which makes my code cleaner.

There is a drawback. You will not be able to change the value of any property after Record initialization. It is an immutable object, and the setter of every property is defined with the init keyword.

Global Usings

Another sugar feature of .NET 6 is Global Usings. The new keyword global before Using directive is making it possible not to include using MediatR in every file where we are implementing interfaces from the MediatR library.

Global Usings are project-scoped, so I recommend you to create just one dedicated file for them in the root of the project folder. Define your Global Usings only there. It can be a mess once you start to write global using in a random file.

Let’s create a GlobalUsings.cs file with this line of code:

global using MediatR;

Now, we can remove using MediatR; from the code sample.

namespace WarehouseSystem
{    
    public record  GetWarehousesQuery(bool OnlyInventoryWarehouses) : IRequest<IEnumerable<WarehouseListDto>>;

    public class GetWarehousesQueryHandler : IRequestHandler<GetWarehousesQuery, IEnumerable<WarehouseListDto>>
    {
        private readonly WarehouseDbContext _context;
        private readonly IMapper _mapper;

        public GetWarehousesQueryHandler(WarehouseDbContext context, IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        public async Task<IEnumerable<WarehouseListDto>> Handle(GetWarehousesQuery request, CancellationToken cancellationToken)
        {
            IQueryable<Warehouse> query = _context.Warehouses.Include(x => x.Stocks).AsNoTracking();

            if (request.OnlyInventoryWarehouses)
            {
                query = query.Where(w => w.Stocks.Any(a => a.IsInventory));
            }

            return await _mapper.ProjectTo<WarehouseListDto>(query).ToListAsync();
        }
    }
}

File Scoped Namespaces

File Scoped Namespace means defining one namespace per file without curly brackets. So our code sample can be even cleaner. Take a look below.

namespace WarehouseSystem;

public record  GetWarehousesQuery(bool OnlyInventoryWarehouses) : IRequest<IEnumerable<WarehouseListDto>>;

public class GetWarehousesQueryHandler : IRequestHandler<GetWarehousesQuery, IEnumerable<WarehouseListDto>>
{
    private readonly WarehouseDbContext _context;
    private readonly IMapper _mapper;

    public GetWarehousesQueryHandler(WarehouseDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    public async Task<IEnumerable<WarehouseListDto>> Handle(GetWarehousesQuery request, CancellationToken cancellationToken)
    {
        IQueryable<Warehouse> query = _context.Warehouses.Include(x => x.Stocks).AsNoTracking();

        if (request.OnlyInventoryWarehouses)
        {
            query = query.Where(w => w.Stocks.Any(a => a.IsInventory));
        }

        return await _mapper.ProjectTo<WarehouseListDto>(query).ToListAsync();
    }
}

Query and Handler in one short file. That’s how I like it.

Upgrade Code with MediatR Attributed Behaviors

MediatR Pipeline

Before moving to my last upgrade, I need to explain what MediatR Pipeline is. It is a pipeline defined for one MediatR Request (message). Pipelines execution starts with Mediator.Send method invocation and passes an object of MediatR Request into it.

Simple-pipeline.jpg

Pipeline Behaviors

MediatR has a feature called Pipeline Behaviors. Behaviors can be attached to the pipeline by defining a proper interface implementation.

behaviors.png

You can attach Behavior before or after Handler, depends where you will invoke the next() delegate. A typical example can be a Validation Behavior that will validate the MediatR Request form before Handler. Now some code. This is my new CreateWarehouseCommand pipeline. The code in the Handler is not vital for proof of the concept. What it does is to persist the new Warehouse record into the database.

namespace Warehouse;

public record class CreateWarehouseCommand(string Description, string Number, IEnumerable<CreateStockDto> Stocks, bool Active) : IRequest<NewRecordDto<string>>;
public record class CreateStockDto(string? Description, string Name, bool Input, bool Active);

public class CreateWarehouseCommandHandler : IRequestHandler<CreateWarehouseCommand, NewRecordDto<string>>
{
    private readonly IWarehouseRepository _warehouseRepository;

    public CreateWarehouseCommandHandler(IWarehouseRepository warehouseRepository)
    {
        _warehouseRepository = warehouseRepository;
    }

    public async Task<NewRecordDto<string>> Handle(CreateWarehouseCommand request, CancellationToken cancellationToken)
    {
        Warehouse warehouse = new(request.Description, request.Number);
        warehouse.Activate(request.Active);
        warehouse.Stocks = request.Stocks
            .Select(sDto =>
            {
                Stock stock = new (sDto.Name, sDto.Input, warehouse)
                {
                    Description = sDto.Description ?? string.Empty
                };
                stock.Activate(sDto.Active);
                return stock;
            })
            .ToList();

        warehouse = _warehouseRepository.Add(warehouse);
        await _warehouseRepository.UnitOfWork.SaveChangesAsync(cancellationToken);

        return new NewRecordDto<string>(warehouse.Id);
    }
}

Now let’s define Validation Behavior.

namespace 
ToolsManagement.Warehouse.Application.Warehouses.Commands.Create;

public class ValidateCreateWarehouseBehavior : IPipelineBehavior<CreateWarehouseCommand, NewRecordDto<string>>
{
    private readonly WarehouseDbContext _context;

    public ValidateCreateWarehouseBehavior(WarehouseDbContext context)
    {
        _context = context;
    }

    public async Task<NewRecordDto<string>> Handle(CreateWarehouseCommand request, CancellationToken cancellationToken, RequestHandlerDelegate<NewRecordDto<string>> next)
    {
        if (await _context.Warehouses.AnyAsync(x => x.Number.Trim() == request.Number.Trim(), cancellationToken))
        {
            throw ErrorHelper.CreateValidationException(ValidErrorCodes.Duplicate, nameof(CreateWarehouseCommand.Number), request.Number);
        }

        return await next();
    }
}

The ValidateCreateWarehouseBehavior will be executed before Handler because we are invoking next delegate after validation. If the await next() was at line 14 before if, Handler would be invoked before validation. This is how you can manage the position of Behavior towards the Handler or another Behavior in the pipeline.

Behaviors have to be registered into the DI container manually.

services.AddScoped<IPipelineBehavior<CreateWarehouseCommand, NewRecordDto<string>>, ValidateCreatedWarehouseBehavior>();

By the way, if you have multiple Behaviors for one pipeline, their order of execution is defined by order of registrations into the DI container. This is an unpleasantly hidden way of behaving by MediatR. But I have a nice fix for it in the next paragraph.

MediatR Attributed Behaviors

The last upgrade is the extension library for MediatR called MediatR Attributed Behaviors. The library helps you remove the responsibility of manual DI registrations of Behaviors from the developer. Also, you can see the Behaviors of your pipeline right over the MediatR Request definition. Take a look at the last code sample.

namespace ToolsManagement.Warehouse.Application.Warehouses.Commands.Create;

[MediatRPipelineBehavior(typeof(ValidateCreateWarehouseBehavior))]
public record class CreateWarehouseCommand(string Description, string Number, IEnumerable<CreateStockDto> Stocks, bool Active) : IRequest<NewRecordDto<string>>;
public record class CreateStockDto(string? Description, string Name, bool Input, bool Active);

public class CreateWarehouseCommandHandler : IRequestHandler<CreateWarehouseCommand, NewRecordDto<string>>
{
    private readonly IWarehouseRepository _warehouseRepository;

    public CreateWarehouseCommandHandler(IWarehouseRepository warehouseRepository)
    {
        _warehouseRepository = warehouseRepository;
    }

    public async Task<NewRecordDto<string>> Handle(CreateWarehouseCommand request, CancellationToken cancellationToken)
    {
        Warehouse warehouse = new(request.Description, request.Number);
        warehouse.Activate(request.Active);
        warehouse.Stocks = request.Stocks
            .Select(sDto =>
            {
                Stock stock = new (sDto.Name, sDto.Input, warehouse)
                {
                    Description = sDto.Description ?? string.Empty
                };
                stock.Activate(sDto.Active);
                return stock;
            })
            .ToList();

        warehouse = _warehouseRepository.Add(warehouse);
        await _warehouseRepository.UnitOfWork.SaveChangesAsync(cancellationToken);

        return new NewRecordDto<string>(warehouse.Id);
    }
}

Another power of this extension library is defining the order and scope of behaviors and passing it as other parameters.

[MediatRBehavior(typeof(MySingletonPipelineBehavior<MyQuery>), serviceLifetime: ServiceLifetime.Singleton, order: 1)]
public class MyQuery : IRequest

Sources

itixo-logo-blue.png