Start Debugging

Migrar do MediatR para injeção de dependência simples no .NET 11

Uma lista de verificação passo a passo para remover o MediatR 12-14 e substituir os handlers de IRequest, ISender, os pipeline behaviors e INotification por classes de serviço simples e injeção por construtor.

Remover o MediatR de uma base de código em .NET 11 é uma refatoração mecânica, não uma reescrita. Para um serviço típico com 50-150 handlers, reserve de meio dia a um dia: a maioria dos handlers se colapsa um a um em métodos sobre classes de serviço simples, as chamadas a ISender.Send viram chamadas diretas à interface, e a única parte que exige raciocínio de design é o pipeline. O que quebra é tudo que dependia da resolução de handlers em runtime ou do envoltório de IPipelineBehavior<,> em volta de cada requisição; isso vira injeção por construtor e decorators. Vale a pena fazer se você só chama Send, se você está acima da linha de receita de $5,000,000 da edição Community do MediatR e não quer comprar uma licença comercial, ou se você quer Native AOT e compatibilidade com trimming. Não vale a pena se seus pipeline behaviors são fundamentais em centenas de tipos de requisição.

Versões referenciadas: este guia cobre a remoção do MediatR 12.5.0 (a última versão com licença Apache 2.0), 13.0 (lançada em 2025-07-02, a primeira versão com licença dupla Reciprocal Public License 1.5 / comercial da Lucky Penny Software) e a linha atual 14.x. O código de substituição mira <TargetFramework>net11.0</TargetFramework> com o SDK do .NET 11 e C# 14, mais Scrutor 6.x para os decorators. Se você ainda está decidindo se deve sair, leia primeiro MediatR vs classes de serviço simples em 2026; este artigo assume que a decisão já foi tomada.

Por que as equipes estão removendo o MediatR justamente agora

O que quebra

ÁreaMudançaSeveridade
Injeção de ISender / IMediatorSubstituída pela interface de serviço concreta injetada diretamentealta
IRequest<T> + IRequestHandler<,>Colapsam em uma interface + o método de implementaçãoalta
IPipelineBehavior<,>Sem ponto de envoltório genérico; substituído por interface via decorators ou middlewarealta
INotification + INotificationHandler<>Substituído por um IEnumerable<IHandler> injetado para o fan-out ou um agregador de eventosmédia
Registro AddMediatR(...)Substituído por chamadas explícitas a AddScoped (ou uma varredura do Scrutor)média
RequestHandlerDelegate<T> em behaviorsSem equivalente; a cadeia “next” desaparecemédia
ISender.CreateStream (IStreamRequest)Substituído por um método que retorna IAsyncEnumerable<T>baixa
Testes unitários que mockam ISenderReapontados para mockar a interface concretabaixa

Lista de verificação prévia

# .NET 11 - inventory MediatR usage before touching anything
grep -rn "IRequestHandler\|IRequest<\|: IRequest" src/      # handlers + requests
grep -rn "IPipelineBehavior" src/                            # the hard part
grep -rn "INotification" src/                                # fan-out
grep -rn "ISender\|IMediator\|\.Send(\|\.Publish(" src/      # call sites

Passos de migração

  1. Converta cada requisição e handler em uma interface de serviço e um método.

Uma requisição do MediatR é um tipo de mensagem mais um handler separado. Substitua ambos por uma interface e uma implementação. Agrupe requisições relacionadas em um único serviço em vez de criar uma interface por requisição antiga.

// .NET 11, C# 14, MediatR 14.x - BEFORE
using MediatR;

public record GetOrderById(int OrderId) : IRequest<OrderDto>;

public sealed class GetOrderByIdHandler(AppDbContext db)
    : IRequestHandler<GetOrderById, OrderDto>
{
    public async Task<OrderDto> Handle(GetOrderById request, CancellationToken ct)
    {
        var order = await db.Orders.FindAsync([request.OrderId], ct)
            ?? throw new OrderNotFoundException(request.OrderId);
        return order.ToDto();
    }
}
// .NET 11, C# 14 - AFTER: plain service, no MediatR reference
public interface IOrderService
{
    Task<OrderDto> GetByIdAsync(int orderId, CancellationToken ct);
}

public sealed class OrderService(AppDbContext db) : IOrderService
{
    public async Task<OrderDto> GetByIdAsync(int orderId, CancellationToken ct)
    {
        var order = await db.Orders.FindAsync([orderId], ct)
            ?? throw new OrderNotFoundException(orderId);
        return order.ToDto();
    }
}

Verifique: o arquivo novo não tem using MediatR; e o corpo do handler é idêntico byte a byte exceto pela assinatura do método. Os parâmetros do record da requisição viram parâmetros do método na mesma ordem.

  1. Substitua as chamadas a ISender.Send por chamadas diretas à interface.

Cada chamador que injetava ISender ou IMediator agora injeta a interface de serviço específica e chama o método.

// .NET 11, C# 14 - BEFORE
public async Task<OrderDto> Get(int id, ISender sender, CancellationToken ct)
    => await sender.Send(new GetOrderById(id), ct);

// AFTER
public async Task<OrderDto> Get(int id, IOrderService orders, CancellationToken ct)
    => await orders.GetByIdAsync(id, ct);

Verifique: após este passo, grep -rn "ISender\|IMediator" src/ retorna apenas linhas que você deliberadamente ainda não migrou. A contagem deve marchar em direção a zero.

  1. Registre os serviços explicitamente.

Remova AddMediatR(...) e registre cada serviço. O registro explícito é seguro para trimming e falha rápido na inicialização, que é exatamente o modo de falha por trás de Unable to resolve service for type while attempting to activate.

// .NET 11, C# 14 - BEFORE
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssemblyContaining<GetOrderById>());

// AFTER - explicit, or one Scrutor scan if you prefer convention
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();

Se você quer um registro baseado em convenção sem o modelo de reflexão do MediatR, o Scrutor varre pelo nome da interface:

// .NET 11, C# 14, Scrutor 6.x - register every *Service against its interface
builder.Services.Scan(scan => scan
    .FromAssemblyOf<OrderService>()
    .AddClasses(c => c.Where(t => t.Name.EndsWith("Service")))
    .AsImplementedInterfaces()
    .WithScopedLifetime());

Verifique: a aplicação inicia. Execute-a e acesse um endpoint migrado; um registro ausente agora lança na inicialização, não na primeira requisição. Confirme que nenhum erro de serviço scoped a partir de singleton se infiltrou, a classe de bug coberta em Cannot consume scoped service from singleton.

  1. Substitua os pipeline behaviors por decorators ou middleware.

Este é o único passo que exige julgamento. Um IPipelineBehavior<,> envolvia cada requisição em um único lugar. Você tem duas substituições honestas.

Para concerns que são genuinamente por serviço (logging, cache, retentativas), use um decorator do Scrutor:

// .NET 11, C# 14, Scrutor 6.x - BEFORE was a generic ValidationBehavior<TRequest,TResponse>
public sealed class LoggingOrderService(
    IOrderService inner,
    ILogger<LoggingOrderService> logger) : IOrderService
{
    public async Task<OrderDto> GetByIdAsync(int orderId, CancellationToken ct)
    {
        logger.LogInformation("Fetching order {OrderId}", orderId);
        return await inner.GetByIdAsync(orderId, ct);
    }
}

// Registration: wrap the real implementation
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, LoggingOrderService>();

Para concerns que na verdade são concerns de HTTP (logging de requisições, mapeamento de exceções para ProblemDetails, autenticação), tire-os completamente da camada de aplicação para middleware do ASP.NET Core ou um filtro de endpoint. Um behavior que capturava exceções e moldava as respostas pertence ao mesmo lugar que um filtro de exceções global no ASP.NET Core 11, não a um decorator.

Se você usava FluentValidation dentro de um ValidationBehavior, mantenha os validadores e chame-os a partir de um único decorator ou de um filtro de endpoint de minimal API:

// .NET 11, C# 14 - a validation decorator replacing ValidationBehavior for one interface
public sealed class ValidatingOrderService(
    IOrderService inner,
    IValidator<CreateOrder> validator) : IOrderService
{
    public async Task<OrderDto> CreateAsync(CreateOrder cmd, CancellationToken ct)
    {
        await validator.ValidateAndThrowAsync(cmd, ct);
        return await inner.CreateAsync(cmd, ct);
    }

    public Task<OrderDto> GetByIdAsync(int orderId, CancellationToken ct)
        => inner.GetByIdAsync(orderId, ct);
}

Verifique: escreva ou mantenha um teste que afirme que o concern transversal ainda executa, por exemplo que um comando inválido lance ValidationException antes de tocar o banco de dados. Execute dotnet test e confirme que os testes do behavior passam.

  1. Substitua o fan-out de INotification.

O Publish do MediatR invocava cada INotificationHandler<T>. Substitua-o por um IEnumerable<T> injetado de uma pequena interface de handler que você define, e um publicador enxuto.

// .NET 11, C# 14 - BEFORE: INotification + handlers, AFTER: explicit fan-out
public interface IOrderPlacedHandler
{
    Task HandleAsync(OrderPlaced evt, CancellationToken ct);
}

public sealed class OrderEventPublisher(IEnumerable<IOrderPlacedHandler> handlers)
{
    public async Task PublishAsync(OrderPlaced evt, CancellationToken ct)
    {
        foreach (var handler in handlers)
            await handler.HandleAsync(evt, ct);
    }
}

// Registration - each handler registered against the interface
builder.Services.AddScoped<IOrderPlacedHandler, SendConfirmationEmail>();
builder.Services.AddScoped<IOrderPlacedHandler, UpdateInventory>();
builder.Services.AddScoped<OrderEventPublisher>();

O container te entrega cada handler registrado em IEnumerable<IOrderPlacedHandler>, então adicionar um handler é uma única linha de registro, a mesma ergonomia que o MediatR te dava. Se você publica muitos tipos de evento, gere o publicador com um IEventPublisher<T> genérico em vez de um por evento.

Verifique: afirme que publicar um evento invoca todos os handlers registrados. Um teste com dois handlers falsos que ambos registram uma chamada é suficiente.

  1. Remova o pacote e as últimas referências.

Uma vez que as chamadas tenham desaparecido, retire a dependência.

# .NET 11 - remove the package from every project that referenced it
dotnet remove package MediatR
grep -rn "MediatR" src/ || echo "clean"

Verifique: grep -rn "MediatR" src/ imprime clean. O build não tem using MediatR; não resolvido e dotnet build -c Release tem sucesso sem avisos sobre um pacote ausente.

Verificação: o teste de fumaça após a migração

Execute esta lista de cima a baixo e não pule a linha de desempenho:

Plano de rollback

Esta migração é reversível por commit mas tediosa de desfazer em bloco, então escalone-a. Mantenha cada um dos seis passos como um commit separado, e migre uma fatia vertical de cada vez (uma interface de serviço e seus chamadores) em vez de toda a solução de uma vez. MediatR e serviços simples podem coexistir durante a transição: deixe AddMediatR registrado para os handlers não migrados enquanto você converte o resto. Se uma fatia se comportar mal, reverta os commits dessa fatia e o resto da aplicação não é afetado. Aqui não há mudança de dados nem de esquema, então o rollback é puramente um revert de código sem migração a reverter.

Problemas que encontramos

A ordem de IPipelineBehavior não mapeia de forma limpa para decorators. O MediatR executa os behaviors na ordem de registro em volta de um único handler. Os decorators do Scrutor se aninham na ordem em que você chama Decorate, e o último decorator registrado é o envoltório mais externo. Erre a ordem e seu decorator de logging executa dentro do seu decorator de validação em vez de em volta dele. Escreva um teste de ordenação por interface decorada.

Um ISender dentro de um handler que chama outro handler vira uma chamada direta ao serviço, e isso pode expor um ciclo. A indireção do MediatR escondia as dependências entre handlers. Quando OrderService injeta ICustomerService e CustomerService injeta IOrderService, o container lança na inicialização com uma dependência circular. Isso é a migração trazendo à tona um problema de design que o MediatR estava mascarando; quebre o ciclo extraindo a lógica compartilhada para um terceiro serviço.

Requisições de streaming precisam de IAsyncEnumerable<T>, não Task<T>. Se você usava IStreamRequest<T> e CreateStream, o método de substituição retorna IAsyncEnumerable<T> e usa yield return. Não o colapse em um Task<List<T>>; isso muda a semântica de streaming e pode estourar a memória em resultados grandes, a mesma armadilha descrita em ler um CSV grande no .NET 11 sem ficar sem memória.

Testes que mockavam ISender passam silenciosamente contra nada. Um teste que configurava sender.Send(...) para retornar um valor dará erro de compilação assim que ISender desaparecer, o que é bom. Mas um teste que injetava um IMediator real e afirmava o comportamento através dele precisa ser reapontado para a interface concreta. Execute novamente a suíte completa, não confie só em um build no verde.

Este é o tipo de racionalização de dependências que compensa da mesma forma que escolher o serializador embutido compensa em System.Text.Json vs Newtonsoft.Json em 2026: uma biblioteca de terceiros a menos no caminho quente, uma pergunta de licenciamento a menos, e código que um colega novo pode navegar sem ter que aprender primeiro uma convenção de despacho.

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar