Start Debugging

Endpunkt-Filter vs. Middleware in ASP.NET Core 11: Was sollten Sie verwenden?

Ein Entscheidungsleitfaden für ASP.NET Core 11: Middleware läuft bei jeder Anfrage, bevor Ihr Handler das Binding durchführt, Endpunkt-Filter laufen nur für den passenden Endpunkt, nach dem Binding, und können die typisierten Argumente sehen. Enthält eine Vergleichstabelle, Szenarien zur Auswahl, die Reihenfolge-Regeln und die Details, die die Wahl erzwingen.

Verwenden Sie Middleware, wenn die Logik bei jeder Anfrage laufen muss, vor oder unabhängig davon, welcher Endpunkt passt: Ausnahmebehandlung, CORS, Authentifizierung, Antwortkomprimierung, statische Dateien, weitergeleitete Header. Verwenden Sie einen Endpunkt-Filter, wenn die Logik die gebundenen Handler-Argumente benötigt oder nur für einige Endpunkte gelten soll: Eingabevalidierung, Argument-Normalisierung, Auditierung pro Endpunkt. Der schärfste Test: Wenn Ihr Code das typisierte Modell benötigt, das der Handler gleich empfangen wird, will er einen Filter, weil ein Filter nach dem Modell-Binding läuft und context.GetArgument<T>(index) lesen kann. Wenn er laufen muss, egal ob eine Route passt oder nicht, will er Middleware, weil Middleware läuft, bevor das Routing einen Endpunkt auflöst. Alles Folgende ist das Detail hinter dieser Entscheidung. Dieser Artikel zielt auf .NET 11 (Preview 6 zum Zeitpunkt des Schreibens, GA im November 2026) mit Microsoft.NET.Sdk.Web und C# 14, aber beide Funktionen sind seit ASP.NET Core 7 stabil, sodass jedes Beispiel hier unverändert unter .NET 8, 9 und 10 läuft.

Die Vergleichstabelle

Dies ist die Tabelle, für die Sie gekommen sind. Lesen Sie sie von oben nach unten, und die Entscheidung trifft sich meist von selbst.

MerkmalEndpunkt-FilterMiddleware
Läuft fürnur den passenden Endpunktjede Anfrage in diesem Pipeline-Zweig
Position relativ zum Routingnach Routing und Modell-Bindingvor, bei oder nach dem Routing (Position)
Sieht Handler-Argumenteja, typisiert über GetArgument<T>(index)nein, nur den rohen HttpContext
Kann gebundene Argumente ändernja, context.Arguments ist veränderbarnein, das Binding hat noch nicht stattgefunden
Kurzschluss-Mechanismusein IResult statt next zurückgebennext(context) nicht aufrufen
Gültigkeitsbereich-Kontrollepro Endpunkt oder pro MapGrouppro App oder pro Zweig via Map/UseWhen
Registrierung.AddEndpointFilter(...)app.Use(...) / app.UseMiddleware<T>()
RückgabetypValueTask<object?>Task
Läuft, wenn kein Endpunkt passtnieja, wenn vor der Endpunkt-Ausführung platziert
Wiederverwendbar an MVC-Controllernja, auch an Controller-Endpunktenja, pipelineweit

Die Zeilen, die die Wahl tatsächlich entscheiden, sind die ersten drei. Middleware sitzt in der Anfrage-Pipeline, und jede Anfrage, die durch dieses Segment fließt, führt sie aus, sogar eine Anfrage, die einen 404 ergibt, weil kein Endpunkt passte. Ein Endpunkt-Filter ist an einen bestimmten Routen-Handler gebunden und läuft nur, wenn dieser Handler ausgewählt wird, was geschieht, nachdem UseRouting die Anfrage abgeglichen hat und nachdem das Framework die Routenwerte, die Query-Zeichenkette und den Anfragetext an die Parameter des Handlers gebunden hat. Dieser Unterschied im Zeitpunkt ist die ganze Geschichte.

Was Middleware sieht, und wann

Middleware ist eine Kette von Komponenten, von denen jede den HttpContext und einen next-Delegate empfängt. Sie registrieren sie in der Program.cs in Reihenfolge, und die Reihenfolge ist das Verhalten: Anfragen fließen von oben nach unten, Antworten fließen von unten nach oben zurück.

// .NET 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Runs for EVERY request, including ones that will 404.
    var sw = System.Diagnostics.Stopwatch.StartNew();
    await next(context);
    sw.Stop();
    app.Logger.LogInformation(
        "{Method} {Path} -> {Status} in {Elapsed}ms",
        context.Request.Method, context.Request.Path,
        context.Response.StatusCode, sw.ElapsedMilliseconds);
});

app.MapGet("/hello/{name}", (string name) => $"Hi {name}");

app.Run();

Diese Timing-Middleware misst die gesamte Anfrage, einschließlich Routing und jedes 404. Sie hat nur Zugriff auf context.Request.Path als Zeichenkette. Sie kann nicht sehen, dass name an "world" gebunden wurde, denn an dem Punkt, an dem die äußere Middleware läuft, hat das Binding noch nicht stattgefunden. Middleware arbeitet eine Ebene unter dem Typsystem Ihres Handlers.

Die Position relativ zu UseRouting ist wichtiger, als die meisten erwarten. Im modernen Minimal-Hosting-Modell fügt WebApplication das Routing automatisch ein, aber Sie können app.UseRouting() explizit aufrufen, um zu steuern, wo die Trennung geschieht. Middleware, die vor dem Routing registriert wird, läuft, bevor überhaupt ein Endpunkt ausgewählt ist. Middleware, die nach UseRouting registriert wird, kann die Metadaten des ausgewählten Endpunkts über context.GetEndpoint() lesen, so weiß UseAuthorization, welche Richtlinie durchzusetzen ist. Deshalb ist die kanonische Reihenfolge UseRouting, dann UseAuthentication, dann UseAuthorization und dann die Endpunkt-Ausführung: Die Autorisierung benötigt die Endpunkt-Metadaten, die das Routing erzeugt hat.

Was ein Endpunkt-Filter sieht, und wann

Ein Endpunkt-Filter umschließt die Aufrufung eines einzelnen Routen-Handlers. Er läuft nach dem Routing und nach dem Binding, sodass er das Eine hat, das Middleware nicht bekommen kann: die tatsächlichen, typisierten Argumente, die Ihr Handler gleich empfangen wird.

// .NET 11, C# 14
app.MapPost("/orders", (Order order) => Results.Created($"/orders/{order.Id}", order))
    .AddEndpointFilter(async (context, next) =>
    {
        // The Order is already bound. Middleware could never see this.
        var order = context.GetArgument<Order>(0);
        if (order.Quantity < 1)
        {
            return Results.Problem("Quantity must be at least 1.");
        }
        return await next(context);
    });

Der Rückgabetyp des Filters ist ValueTask<object?>. Die Rückgabe eines beliebigen IResult (wie Results.Problem) erzeugt einen Kurzschluss und schreibt dieses Ergebnis in die Antwort, ohne den Handler jemals aufzurufen. Die Rückgabe von await next(context) führt den Handler aus und gibt sein Ergebnis die Kette hinauf zurück, sodass ein Filter die Antwort auch auf dem Weg nach außen transformieren kann. Da der Filter die gebundene Order sieht, lebt die Validierung natürlich hier. Eine Middleware-Komponente, die dieselbe Aufgabe versucht, müsste den Anfragetext selbst erneut lesen und erneut deserialisieren und damit die Arbeit duplizieren, die das Framework bereits erledigt hat. Die vollständigen Mechanismen von AddEndpointFilter, die klassenbasierte IEndpointFilter-Form und die Filter-Reihenfolge werden in Wie Sie einer Minimal API einen Endpunkt-Filter hinzufügen behandelt; dieser Artikel handelt davon, wann Sie ihn überhaupt der Middleware vorziehen.

Wann Sie Middleware wählen

Wann Sie einen Endpunkt-Filter wählen

Der einzige Ort, an dem die Leistung wirklich in die Entscheidung eingeht

Es ist verlockend, zu einem Filter zu greifen, “weil Middleware für alles läuft und das Verschwendung ist”. Widerstehen Sie der Versuchung, es als Durchsatz-Wettbewerb zu rahmen. Beide Funktionen sind dünn: Ein Filter ist ein Delegate, der ValueTask<object?> zurückgibt, und eine Middleware-Komponente ist ein Delegate, der Task zurückgibt, und der Aufwand pro Aufruf beider ist neben jedem echten Handler, der eine Datenbank berührt oder JSON serialisiert, vernachlässigbar. Der bedeutende Unterschied ist nicht der Aufwand pro Aufruf, sondern wie viele Aufrufe geschehen. Eine früh in der Pipeline platzierte Middleware-Komponente wird bei jeder Anfrage ausgeführt, sodass teure Arbeit dort (eine Datenbankabfrage, eine große Speicherzuweisung) von jedem 404 und jedem Health-Check-Ping bezahlt wird. Dieselbe Arbeit in einem Endpunkt-Filter läuft nur, wenn dieser Endpunkt ausgewählt wird. Die Leistungsregel lautet also nicht “Filter sind schneller”, sondern “begrenzen Sie die Arbeit dorthin, wo sie benötigt wird”. Wenn ein übergreifendes Anliegen wirklich für jede Route gilt, ist Middleware der richtige und nicht langsamere Ort dafür. Wenn es für eine Handvoll Endpunkte gilt, vermeidet ein Filter, es bei den Tausenden von Anfragen auszuführen, die diese Endpunkte nie berühren. Das ist eine Gültigkeitsbereich-Entscheidung, verkleidet als Leistungsentscheidung, und das ist die ehrliche Version der Behauptung.

Die Details, die für Sie entscheiden

Ein paar harte Einschränkungen setzen die Vorliebe vollständig außer Kraft.

Ein Filter kann niemals vor dem Routing laufen. Wenn Ihre Anforderung lautet “die Anfrage ablehnen, bevor der Router sie sieht” oder “die URL umschreiben”, ist ein Filter physisch dazu nicht in der Lage, weil er innerhalb der Endpunkt-Ausführung lebt, die dem Routing nachgelagert ist. Das erzwingt Middleware.

Middleware kann das gebundene Modell nicht sehen, ohne die Arbeit zu wiederholen. Wenn Ihre Anforderung lautet “den deserialisierten Anfragetext validieren”, müsste die Middleware den Text selbst puffern und deserialisieren, und dann deserialisiert das Framework ihn erneut für den Handler. Dieses doppelte Binding ist ein starkes Signal, dass Sie einen Filter wollten. Das erzwingt einen Filter.

Ausnahmen entkommen dem Gültigkeitsbereich eines Filters. Ein Filter umschließt nur seinen Endpunkt, sodass er nicht Ihr app-weites Sicherheitsnetz sein kann. Wenn Sie Ihre einzige Ausnahmebehandlung in einen Filter legen, segelt eine in anderer Middleware oder während des Routings geworfene Ausnahme daran vorbei und trifft den Standard-500-Handler. Die globale Fehlerbehandlung erzwingt Middleware.

Die Reihenfolge-Modelle unterscheiden sich, und ihr Mischen verwirrt die Leute. Middleware verschachtelt sich nach der Registrierungsreihenfolge in der Program.cs. Filter verschachteln sich nach der Reihenfolge, in der Sie die .AddEndpointFilter-Aufrufe verketten: Der zuerst registrierte führt seinen Code vor next zuerst und seinen Code nach next zuletzt aus. Wenn Sie beide stapeln, läuft die gesamte Filter-Kette eines Endpunkts innerhalb des innersten Punktes der Middleware-Pipeline, nachdem UseRouting, UseAuthentication und UseAuthorization ausgeführt wurden. Die Autorisierung läuft also immer vor jedem Endpunkt-Filter, was meist gewünscht ist, aber es bedeutet, dass ein Filter der falsche Ort ist, um ein Authentifizierungsschema zu implementieren. Die Authentifizierung erzwingt Middleware.

Das Terminal-Verhalten ist entgegengesetzt. Eine Middleware-Komponente, die next nicht aufruft, erzeugt einen Kurzschluss, indem sie einfach nicht fortfährt. Ein Filter erzeugt einen Kurzschluss, indem er ein IResult zurückgibt. Wenn Sie einen Filter schreiben und vergessen, auf dem Kurzschluss-Pfad etwas zurückzugeben, erhalten Sie einen Kompilierungsfehler oder ein null-Ergebnis statt einer stillschweigend verschluckten Anfrage, was ein kleiner, aber echter ergonomischer Vorteil für Filter ist.

Die Empfehlung, erneut formuliert

Standardmäßig gilt: übergreifende Anliegen, die bei jeder Anfrage oder vor dem Routing laufen müssen, sind Middleware. Anliegen, die die typisierten Handler-Argumente benötigen oder für eine Teilmenge von Endpunkten gelten, sind Endpunkt-Filter. Authentifizierung, CORS, Ausnahmebehandlung, Komprimierung und statische Dateien sind Middleware und werden es immer sein. Validierung, Argument-Normalisierung, Auditierung pro Endpunkt und Ergebnis-Formung sind Endpunkt-Filter. Der Graubereich-Fall ist die Autorisierungslogik pro Endpunkt: Wenn sie nur Claims aus HttpContext.User benötigt, funktioniert beides, aber bevorzugen Sie einen Filter, damit die Richtlinie neben dem Endpunkt lebt, den sie schützt; wenn sie die gebundenen Argumente benötigt, um die Entscheidung zu treffen (Zugriffsprüfungen auf Zeilenebene für eine gebundene Entitäts-Id), muss sie ein Filter sein. Wenn Sie sich wirklich nicht entscheiden können, stellen Sie die eine Frage, die fast jeden Fall löst: Muss dieser Code die Argumente sehen, die mein Handler empfangen wird? Ja bedeutet Filter. Nein, und er muss unabhängig von der Route laufen, bedeutet Middleware.

Verwandt

Quellen

Comments

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

< Zurück