Start Debugging

Migration von MediatR zu einfacher Dependency Injection in .NET 11

Eine Schritt-für-Schritt-Checkliste, um MediatR 12-14 zu entfernen und IRequest-Handler, ISender, Pipeline Behaviors und INotification durch einfache Service-Klassen und Konstruktorinjektion zu ersetzen.

Diese Website nutzt Walter, einen KI-Agenten, für Recherche, Texte und Pflege. Redaktion und Überprüfung

Migriere jeweils eine Anfrage. Ersetze ihren Handler durch eine Dienstmethode, passe die Aufrufer an und erhalte Validierung, Transaktionen und Benachrichtigungen. Schätze den Aufwand anhand dieser Abhängigkeiten und Aufrufstellen, bevor du Registrierungen änderst.

Korrektur — 2. November 2026: Fehlerzeitpunkt bei DI und Community-Kriterien korrigiert, unbelegte Kaltstartaussagen entfernt und den Validierungsdekorator an seinen Schreibvertrag angepasst.

Die Beispiele zielen auf net11.0 und zeigen die Verträge von MediatR 13/14. Die unten beschriebene DI-Prüfung lief mit .NET 10. Wenn die Architekturentscheidung noch aussteht, beginne mit dem Vergleich von MediatR und direkten Diensten.

Warum Teams MediatR gerade jetzt entfernen

  • Erfasse die eingesetzten MediatR-Versionen und prüfe die Kriterien des Anbieters, darunter Finanzierung und organisatorischer Einsatz, bevor du die Lizenz als Migrationsgrund wertest.
  • Bei einem Interface-Aufruf führt der Sprung zur Definition zur Interface-Methode. Die IDE kann deren Implementierungen anzeigen. Bei Send suche nach dem Handler des Anfragetyps.
  • Ist die Startzeit ein Migrationskriterium, miss den Kaltstart derselben Bereitstellung vor und nach der Umstellung. Prüfe bei Trimming oder Native AOT die veröffentlichte Anwendung.

Was bricht

BereichÄnderungSchweregrad
ISender / IMediator-InjektionErsetzt durch das direkt injizierte konkrete Service-Interfacehoch
IRequest<T> + IRequestHandler<,>Kollabieren zu einem Interface + der Implementierungsmethodehoch
IPipelineBehavior<,>Kein generischer Wrap-Punkt; pro Interface durch Decorators oder Middleware ersetzthoch
INotification + INotificationHandler<>Ersetzt durch ein injiziertes IEnumerable<IHandler>-Fan-out oder einen Event-Aggregatormittel
AddMediatR(...)-RegistrierungErsetzt durch explizite AddScoped-Aufrufe (oder einen Scrutor-Scan)mittel
RequestHandlerDelegate<T> in BehaviorsKein Äquivalent; die “next”-Kette entfälltmittel
ISender.CreateStream (IStreamRequest)Ersetzt durch eine Methode, die IAsyncEnumerable<T> zurückgibtniedrig
Unit-Tests, die ISender mockenAuf das Mocken des konkreten Interface umgelenktniedrig

Vorab-Checkliste

  • Das .NET-11-SDK ist installiert: Bestätigen Sie mit dotnet --version (erwarten Sie 11.x).
  • Sie haben einen sauberen git-Branch und eine grüne Testsuite, bevor Sie beginnen. Verifizieren Sie mit dotnet test und bestätigen Sie null Fehlschläge.
  • Inventarisieren Sie die betroffene Oberfläche. Führen Sie eine Suche aus und notieren Sie die Anzahl, denn sie sagt Ihnen, wie groß die Aufgabe ist:
# .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
  • Entscheiden Sie zuerst über die Ersatzform der Behaviors. Wenn Sie null IPipelineBehavior-Treffer haben, ist dies ein reines Suchen-und-Ersetzen. Wenn Sie mehrere haben, planen Sie, ob jeder zu einem Decorator, ASP.NET-Core-Middleware oder einem Endpunkt-Filter wird.
  • Fügen Sie Scrutor hinzu, falls Sie Decorators verwenden: dotnet add package Scrutor (6.x, MIT-lizenziert).

Migrationsschritte

  1. Wandeln Sie jede Anfrage und jeden Handler in ein Service-Interface und eine Methode um.

Eine MediatR-Anfrage ist ein Nachrichtentyp plus ein separater Handler. Ersetzen Sie beide durch ein Interface und eine Implementierung. Gruppieren Sie verwandte Anfragen auf einem einzigen Service, statt ein Interface pro alter Anfrage zu erstellen.

// .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();
    }
}

Verifizieren: Die neue Datei hat kein using MediatR; und der Handler-Rumpf ist byte-identisch bis auf die Methodensignatur. Die record-Parameter der Anfrage werden in derselben Reihenfolge zu Methodenparametern.

  1. Ersetzen Sie ISender.Send-Aufrufstellen durch direkte Interface-Aufrufe.

Jeder Aufrufer, der ISender oder IMediator injizierte, injiziert nun das spezifische Service-Interface und ruft die Methode auf.

// .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);

Verifizieren: Nach diesem Schritt gibt grep -rn "ISender\|IMediator" src/ nur Zeilen zurück, die Sie bewusst noch nicht migriert haben. Die Anzahl sollte gegen null marschieren.

  1. Registrieren Sie die Services explizit.

Entferne AddMediatR(...), nachdem die übrigen Handler umgestellt sind. Registriere die neuen Interfaces und Implementierungen ausdrücklich. Fehlende Konstruktorabhängigkeiten zeigen sich bei Validierung oder Auflösung. Prüfe die Registrierungen in der Zielumgebung. Aktiviere ValidateOnBuild, damit der Standardprovider registrierte Konstruktorgraphen beim Erstellen des Hosts prüft. Ohne Validierung kann eine fehlende Abhängigkeit in Produktion erst beim Anfordern des Dienstes auffallen. ValidateScopes prüft die Lebensdauern separat. Siehe die ASP.NET-Core-Provideroptionen.

// .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>();

Für eine Registrierung nach Konvention kann Scrutor eine Assembly durchsuchen und Klassennamen abgleichen:

// .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());

Im .NET-10-Test führte die fehlende Konstruktorabhängigkeit mit explizitem ValidateOnBuild beim Erstellen des Hosts zum Fehler. Ohne die Option gelang der Build, und erst die Auflösung von IOrderService im Scope scheiterte. Löse jeden migrierten Dienst in einem Scope auf und rufe seinen Endpunkt auf.

  1. Ersetzen Sie Pipeline Behaviors durch Decorators oder Middleware.

Dies ist der einzige Schritt, der Urteilsvermögen erfordert. Ein IPipelineBehavior<,> umhüllte jede Anfrage an einer einzigen Stelle. Sie haben zwei ehrliche Ersätze.

Für Concerns, die wirklich pro Service gelten (Logging, Caching, Wiederholungen), verwenden Sie einen Scrutor-Decorator:

// .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>();

Für Concerns, die in Wahrheit HTTP-Concerns sind (Request-Logging, Zuordnung von Exceptions zu ProblemDetails, Authentifizierung), verlagern Sie diese vollständig aus der Anwendungsschicht in ASP.NET-Core-Middleware oder einen Endpunkt-Filter. Ein Behavior, das Exceptions abfing und Antworten formte, gehört an denselben Ort wie ein globaler Exception-Filter in ASP.NET Core 11, nicht in einen Decorator.

Für einen Schreibvorgang erhält der Dekorator einen eigenen Vertrag IOrderWriter. Der lesende IOrderService oben kann seine Methode GetByIdAsync behalten.

using FluentValidation;

public interface IOrderWriter
{
    Task<OrderDto> CreateAsync(CreateOrder command, CancellationToken ct);
}

public sealed class ValidatingOrderWriter(
    IOrderWriter inner,
    IValidator<CreateOrder> validator) : IOrderWriter
{
    public async Task<OrderDto> CreateAsync(CreateOrder command, CancellationToken ct)
    {
        await validator.ValidateAndThrowAsync(command, ct);
        return await inner.CreateAsync(command, ct);
    }
}

Verifizieren: Schreiben oder behalten Sie einen Test, der bestätigt, dass das übergreifende Concern weiterhin läuft, zum Beispiel dass ein ungültiges Kommando eine ValidationException wirft, bevor es die Datenbank berührt. Führen Sie dotnet test aus und bestätigen Sie, dass die Behavior-Tests bestehen.

  1. Ersetzen Sie das INotification-Fan-out.

Das Publish von MediatR rief jeden INotificationHandler<T> auf. Ersetzen Sie es durch ein injiziertes IEnumerable<T> eines kleinen Handler-Interface, das Sie definieren, und einen schlanken Publisher.

// .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>();

Der Container übergibt Ihnen jeden registrierten Handler in IEnumerable<IOrderPlacedHandler>, sodass das Hinzufügen eines Handlers eine einzige Registrierungszeile ist, dieselbe Ergonomie, die MediatR Ihnen gab. Wenn Sie viele Event-Typen veröffentlichen, generieren Sie den Publisher mit einem generischen IEventPublisher<T> statt einem pro Event.

Verifizieren: Bestätigen Sie, dass das Veröffentlichen eines Events alle registrierten Handler aufruft. Ein Test mit zwei Fake-Handlern, die beide einen Aufruf protokollieren, genügt.

  1. Entfernen Sie das Paket und die letzten Referenzen.

Sobald die Aufrufstellen weg sind, entfernen Sie die Abhängigkeit.

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

Verifizieren: grep -rn "MediatR" src/ gibt clean aus. Der Build hat kein ungelöstes using MediatR; und dotnet build -c Release gelingt ohne Warnungen über ein fehlendes Paket.

Verifizierung: der Smoke-Test nach der Migration

Arbeiten Sie diese Liste von oben nach unten ab und überspringen Sie die Performance-Zeile nicht:

  • dotnet build -c Release gelingt ohne Warnungen.
  • dotnet test besteht mit null Fehlschlägen, einschließlich der Behavior- und Fan-out-Tests, die Sie in den Schritten 4 und 5 hinzugefügt haben.
  • Starte die Anwendung und rufe jeden migrierten Endpunkt auf. Prüfe Antwort und Auflösung der Scoped-Dienste.
  • grep -rn "MediatR" src/ gibt nichts zurück.
  • Keine Lizenzwarnung erscheint in den Logs (die Lizenzprüfung von MediatR 13+ protokollierte eine Warnung bei einem nicht registrierten Schlüssel; sie sollte nun verschwunden sein).
  • Vergleiche den Kaltstart vor und nach der Migration mit dem vorgesehenen Veröffentlichungsprofil.

Rollback-Plan

Diese Migration ist pro Commit umkehrbar, aber mühsam im Ganzen rückgängig zu machen, also staffeln Sie sie. Halten Sie jeden der sechs Schritte als separaten Commit und migrieren Sie eine vertikale Scheibe nach der anderen (ein Service-Interface und seine Aufrufer) statt der gesamten Solution auf einmal. MediatR und einfache Services können während des Übergangs koexistieren: Lassen Sie AddMediatR für die nicht migrierten Handler registriert, während Sie den Rest umstellen. Wenn eine Scheibe sich falsch verhält, machen Sie die Commits dieser Scheibe rückgängig, und der Rest der Anwendung bleibt unberührt. Hier gibt es keine Daten- oder Schemaänderung, also ist der Rollback rein ein Code-Revert ohne rückgängig zu machende Migration.

Stolperfallen, die wir trafen

Die Reihenfolge von IPipelineBehavior bildet sich nicht sauber auf Decorators ab. MediatR führt Behaviors in der Registrierungsreihenfolge um einen einzigen Handler aus. Scrutor-Decorators verschachteln sich in der Reihenfolge, in der Sie Decorate aufrufen, und der zuletzt registrierte Decorator ist der äußerste Wrapper. Wenn Sie die Reihenfolge falsch wählen, läuft Ihr Logging-Decorator innerhalb Ihres Validierungs-Decorators statt darum herum. Schreiben Sie einen Reihenfolgetest pro dekoriertem Interface.

Ein Zyklus kann bei der Validierung des registrierten Graphen oder bei der Dienstauflösung auffallen. Benötigen OrderService und CustomerService einander, lagere die gemeinsame Operation in einen dritten Dienst aus.

Streaming-Anfragen brauchen IAsyncEnumerable<T>, nicht Task<T>. Wenn Sie IStreamRequest<T> und CreateStream verwendet haben, gibt die Ersatzmethode IAsyncEnumerable<T> zurück und verwendet yield return. Kollabieren Sie sie nicht zu einem Task<List<T>>; das ändert die Streaming-Semantik und kann bei großen Ergebnissen den Speicher sprengen, dieselbe Falle, die in Lesen einer großen CSV in .NET 11 ohne Speichermangel beschrieben wird.

Tests, die ISender mockten, bestehen stillschweigend gegen nichts. Ein Test, der sender.Send(...) so einrichtete, dass er einen Wert zurückgibt, wird einen Compile-Fehler werfen, sobald ISender weg ist, was gut ist. Aber ein Test, der einen echten IMediator injizierte und das Verhalten darüber bestätigte, muss auf das konkrete Interface umgelenkt werden. Führen Sie die gesamte Suite erneut aus, vertrauen Sie nicht allein einem grünen Build.

Dies ist die Art von Abhängigkeitsrationalisierung, die sich auf dieselbe Weise auszahlt wie die Wahl des integrierten Serializers in System.Text.Json vs Newtonsoft.Json in 2026: eine Drittanbieter-Bibliothek weniger auf dem heißen Pfad, eine Lizenzfrage weniger, und Code, durch den sich ein neuer Teamkollege navigieren kann, ohne zuerst eine Dispatch-Konvention zu lernen.

Verwandt

Quellen

Comments

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