Skip to main content
The Intent.AspNetCore.Controllers.Dispatch.MediatR module instructs ASP.NET Core Controllers to dispatch Commands and Queries to separate handlers via MediatR, implementing the CQRS pattern in your Web API.

Overview

This module bridges ASP.NET Core Controllers with the MediatR library, enabling a clean separation between your HTTP layer and application logic. Controllers become thin dispatchers that forward requests to specialized command and query handlers.

Architecture

The module implements a clean CQRS architecture:

What Gets Generated

Controller with MediatR Dispatch

Controllers are generated with MediatR injection:
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly ISender _mediator;

    public OrdersController(ISender mediator)
    {
        _mediator = mediator;
    }

    [HttpGet("{id}")]
    [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<OrderDto>> GetById(
        [FromRoute] Guid id,
        CancellationToken cancellationToken)
    {
        var result = await _mediator.Send(
            new GetOrderByIdQuery { Id = id },
            cancellationToken);
        return result != null ? Ok(result) : NotFound();
    }

    [HttpPost]
    [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<Guid>> Create(
        [FromBody] CreateOrderCommand command,
        CancellationToken cancellationToken)
    {
        var orderId = await _mediator.Send(command, cancellationToken);
        return CreatedAtAction(nameof(GetById), new { id = orderId }, orderId);
    }

    [HttpPut("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult> Update(
        [FromRoute] Guid id,
        [FromBody] UpdateOrderCommand command,
        CancellationToken cancellationToken)
    {
        if (command.Id != id)
            return BadRequest();

        await _mediator.Send(command, cancellationToken);
        return NoContent();
    }

    [HttpDelete("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult> Delete(
        [FromRoute] Guid id,
        CancellationToken cancellationToken)
    {
        await _mediator.Send(new DeleteOrderCommand { Id = id }, cancellationToken);
        return NoContent();
    }
}

Key Features

CQRS Pattern

Separates read and write operations using Commands and Queries

Thin Controllers

Controllers focus on HTTP concerns, not business logic

MediatR Integration

Leverages MediatR’s request/response pipeline

Pipeline Behaviors

Supports validation, logging, and other cross-cutting concerns

Commands vs Queries

Commands

Commands represent write operations that modify state:
public class CreateOrderCommand : IRequest<Guid>
{
    public Guid CustomerId { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IApplicationDbContext _dbContext;

    public CreateOrderCommandHandler(IApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Guid> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        var order = new Order
        {
            CustomerId = request.CustomerId,
            // Map properties...
        };

        _dbContext.Orders.Add(order);
        await _dbContext.SaveChangesAsync(cancellationToken);

        return order.Id;
    }
}

Queries

Queries represent read operations that don’t modify state:
public class GetOrderByIdQuery : IRequest<OrderDto>
{
    public Guid Id { get; set; }
}

public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IApplicationDbContext _dbContext;
    private readonly IMapper _mapper;

    public GetOrderByIdQueryHandler(
        IApplicationDbContext dbContext,
        IMapper mapper)
    {
        _dbContext = dbContext;
        _mapper = mapper;
    }

    public async Task<OrderDto> Handle(
        GetOrderByIdQuery request,
        CancellationToken cancellationToken)
    {
        var order = await _dbContext.Orders
            .Include(x => x.Items)
            .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);

        return _mapper.Map<OrderDto>(order);
    }
}

Benefits

Separation of Concerns

  • HTTP request/response handling
  • Route configuration
  • Parameter binding
  • Status code determination
  • Business logic
  • Data access
  • Domain operations
  • Validation

Testability

Handlers can be tested independently of HTTP concerns:
[Fact]
public async Task CreateOrder_ValidCommand_ReturnsOrderId()
{
    // Arrange
    var dbContext = CreateInMemoryDbContext();
    var handler = new CreateOrderCommandHandler(dbContext);
    var command = new CreateOrderCommand
    {
        CustomerId = Guid.NewGuid(),
        Items = new List<OrderItemDto> { /* ... */ }
    };

    // Act
    var result = await handler.Handle(command, CancellationToken.None);

    // Assert
    Assert.NotEqual(Guid.Empty, result);
    var order = await dbContext.Orders.FindAsync(result);
    Assert.NotNull(order);
}

Pipeline Behaviors

MediatR supports pipeline behaviors for cross-cutting concerns:

Validation Behavior

public class ValidationBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);
            var results = await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(context, cancellationToken)));
            
            var failures = results
                .SelectMany(r => r.Errors)
                .Where(f => f != null)
                .ToList();

            if (failures.Any())
                throw new ValidationException(failures);
        }

        return await next();
    }
}

Logging Behavior

public class LoggingBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Handling {RequestName}",
            typeof(TRequest).Name);

        var response = await next();

        _logger.LogInformation(
            "Handled {RequestName}",
            typeof(TRequest).Name);

        return response;
    }
}

Services Designer Configuration

In the Services Designer, operations are automatically mapped to Commands or Queries:
1

Create Service

Add a service in the Services Designer
2

Add Operations

Create operations with appropriate HTTP verbs
3

CQRS Mapping

  • GET operations → Queries
  • POST/PUT/DELETE → Commands
4

Generate

Run the Software Factory to generate controllers and handlers

Installation

The module is automatically installed when you have both:
Intent.AspNetCore.Controllers
Intent.Application.MediatR

Dependencies

  • Intent.Application.MediatR (>= 4.3.0)
  • Intent.AspNetCore.Controllers (>= 7.1.0)
  • Intent.Modelers.Services.CQRS

Next Steps

MediatR Behaviors

Add validation, logging, and other pipeline behaviors

CQRS

Generate CQRS handlers automatically

FluentValidation

Add request validation

Controllers

Learn more about controller generation

Build docs developers (and LLMs) love