Freigeben über


Routing in ASP.NET Core

Von Ryan Nowak, Kirk Larkin und Rick Anderson

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

Warnung

Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der .NET- und .NET Core-Supportrichtlinie. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

Das Routing wird für das Abgleichen von HTTP-Anforderungen und das Verteilen an ausführbare Endpunkte der App eingesetzt. Endpunkte sind die Einheiten des ausführbaren Codes für die Anforderungsverarbeitung in der App. Endpunkte werden in der App definiert und beim Start der App konfiguriert. Beim Endpunktabgleich können Werte aus der Anforderungs-URL extrahiert und für die Verarbeitung der Anforderung bereitgestellt werden. Mithilfe von Endpunktinformationen aus der App lassen sich durch das Routing URLs generieren, die Endpunkten zugeordnet werden.

Apps können das Routing mit folgenden Funktionen konfigurieren:

In diesem Artikel werden die grundlegenden Details zum ASP.NET Core-Routing beschrieben. Informationen zur Routingkonfiguration finden Sie wie folgt:

Routinggrundlagen

Der folgende Code veranschaulicht ein einfaches Beispiel für das Routing:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Das vorherige Beispiel enthält einen einzelnen Endpunkt unter Verwendung der MapGet-Methode:

  • Wenn eine GET-HTTP-Anforderung an die Stamm-URL /gesendet wird:
    • Der Anforderungsdelegat wird ausgeführt.
    • Hello World! wird in die HTTP-Antwort geschrieben.
  • Wenn die Anforderungsmethode nicht GET bzw. die Stamm-URL nicht / ist, gibt es keinen Routenabgleich und es wird ein HTTP-404-Fehler zurückgegeben.

Beim Routing wird ein Middlewarepaar verwendet, das durch UseRouting und UseEndpoints registriert wird:

  • UseRouting fügt der Middlewarepipeline einen Routenabgleich hinzu. Diese Middleware prüft die in der App definierten Endpunkte und wählt anhand der Anforderung die beste Übereinstimmung aus.
  • UseEndpoints fügt der Middlewarepipeline die Endpunktausführung hinzu. Dabei wird der mit dem ausgewählten Endpunkt verknüpfte Delegat ausgeführt.

Apps müssen UseRouting oder UseEndpoints normalerweise nicht aufrufen. WebApplicationBuilder konfiguriert eine Middlewarepipeline, die die in Program.cs hinzugefügte Middleware mit UseRouting und UseEndpoints umschließt. Apps können jedoch die Reihenfolge ändern, in der UseRouting und UseEndpoints ausgeführt werden, indem sie diese Methoden explizit aufrufen. Der folgende Code ruft beispielsweise UseRouting explizit auf:

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

Für den Code oben gilt:

  • Mit dem Aufruf von app.Use wird eine benutzerdefinierte Middleware registriert, die am Anfang der Pipeline ausgeführt wird.
  • Der Aufruf von UseRouting konfiguriert die Middleware für den Routenabgleich zur Ausführung nach der benutzerdefinierten Middleware.
  • Der mit MapGet registrierte Endpunkt wird am Ende der Pipeline ausgeführt.

Wenn das vorhergehende Beispiel keinen Aufruf an UseRouting enthält, wird die benutzerdefinierte Middleware nach der Middleware zum Routenabgleich ausgeführt.

Hinweis: Routen, die WebApplication direkt hinzugefügt werden, werden am Ende der Pipeline ausgeführt.

Endpunkte

Die MapGet-Methode wird verwendet, um einen Endpunkt zu definieren. Ein Endpunkt kann Folgendes sein:

  • Ausgewählt, indem die URL und die HTTP-Methode abgeglichen werden.
  • Ausgeführt, indem ein Delegat ausgeführt wird.

Endpunkte, die von der App zugeordnet und ausgeführt werden können, sind in UseEndpoints konfiguriert. Mit MapGet, MapPost und ähnlichen Methoden werden beispielsweise Anforderungsdelegate mit dem Routingsystem verbunden. Zudem können weitere Methoden zur Verbindung von ASP.NET Core-Frameworkfunktionen mit dem Routingsystem verwendet werden:

Das folgende Beispiel zeigt das Routing mit einer anspruchsvolleren Routenvorlage:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

Die Zeichenfolge /hello/{name:alpha} ist eine Routenvorlage. Eine Routenvorlage, um zu konfigurieren, wie der Endpunkt abgeglichen wird. In diesem Fall gleicht die Vorlage Folgendes ab:

  • Eine URL wie /hello/Docs
  • Alle URL-Pfade, die mit /hello/ beginnen, gefolgt von einer Sequenz alphabetischer Zeichen. :alpha wendet eine Routeneinschränkung an, die nur alphabetische Zeichen abgleicht. Routeneinschränkungen werden weiter unten in diesem Artikel erläutert.

Das zweite Segment des URL-Pfads, {name:alpha}:

Das folgende Beispiel zeigt das Routing mit Integritätsprüfungen und Autorisierung:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

Im vorherigen Beispiel wird veranschaulicht, wie Sie:

  • Die Autorisierungsmiddleware für das Routing verwenden.
  • Endpunkte zum Konfigurieren des Autorisierungsverhaltens verwendet werden können.

Der MapHealthChecks-Aufruf fügt einen Endpunkt für eine Integritätsprüfung hinzu. Durch die Verkettung von RequireAuthorization mit diesem Aufruf wird eine Autorisierungsrichtlinie an den Endpunkt angefügt.

Der Aufruf von UseAuthentication und UseAuthorization wird die Authentifizierungs- und Autorisierungsmiddleware hinzugefügt. Diese Middleware wird zum Ausführen folgender Aktionen zwischen UseRouting und UseEndpoints platziert:

  • Anzeigen des von UseRouting ausgewählten Endpunkts.
  • Anwenden einer Autorisierungsrichtlinie vor dem Senden von UseEndpoints an den Endpunkt.

Endpunktmetadaten

Im vorangehenden Beispiel gibt es zwei Endpunkte, aber nur dem für die Integritätsprüfung ist eine Autorisierungsrichtlinie angefügt. Wenn die Anforderung mit dem Endpunkt der Integritätsprüfung, /healthz, übereinstimmt, wird eine Autorisierungsprüfung durchgeführt. Dadurch wird veranschaulicht, dass Endpunkten zusätzliche Daten zugeordnet werden können. Diese zusätzlichen Daten werden als Metadaten des Endpunkts bezeichnet:

  • Die Metadaten lassen sich von routingfähiger Middleware verarbeiten.
  • Die Metadaten können einen beliebigen .NET-Typ aufweisen.

Routingkonzepte

Durch Hinzufügen des effizienten Endpunkt-Konzepts stellt das Routingsystem eine Ergänzung der Middlewarepipeline dar. Endpunkte stehen für Funktionseinheiten der App, die sich in Bezug auf Routing, Autorisierung und die Anzahl der ASP.NET Core-Systeme voneinander unterscheiden.

ASP.NET Core-Endpunktdefinition

Ein ASP.NET Core-Endpunkt ist:

Der folgende Code zeigt, wie der Endpunkt, der mit der aktuellen Anforderung übereinstimmt, abgerufen und geprüft werden kann:

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

Der Endpunkt, falls ausgewählt, kann aus dem HttpContext-Element abgerufen werden. Seine Eigenschaften können geprüft werden. Endpunktobjekte sind unveränderlich und können nach der Erstellung nicht mehr geändert werden. Der häufigste Typ des Endpunkts ist eine RouteEndpoint-Klasse. RouteEndpoint enthält Informationen, die eine Auswahl durch das Routingsystem ermöglichen.

Im vorangehenden Code wird mit app.Use eine Middleware inline konfiguriert.

Der folgende Code zeigt, dass es, je nachdem, wo app.Use in der Pipeline aufgerufen wird, möglicherweise keinen Endpunkt gibt:

// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

Im vorangehenden Beispiel werden Console.WriteLine-Anweisungen hinzugefügt, die anzeigen, ob ein Endpunkt ausgewählt wurde oder nicht. Aus Gründen der Übersichtlichkeit wird in dem Beispiel dem bereitgestellten /-Endpunkt ein Anzeigename zugewiesen.

Das vorherige Beispiel enthält auch Aufrufe an UseRouting und UseEndpoints, um genau zu steuern, wann diese Middleware in der Pipeline ausgeführt wird.

Wenn Sie diesen Code mit einer URL / ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Wenn Sie diesen Code mit einer anderen URL ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Diese Ausgabe zeigt Folgendes:

  • Der Endpunkt ist immer NULL, bevor UseRouting aufgerufen wird.
  • Wenn eine Übereinstimmung gefunden wird, ist der Endpunkt zwischen UseRouting und UseEndpoints ungleich NULL.
  • Die UseEndpoints-Middleware ist eine Terminalmiddleware, wenn eine Übereinstimmung gefunden wird. Terminalmiddleware wird weiter unten in diesem Artikel definiert.
  • Die Middleware nach UseEndpoints wird nur ausgeführt, wenn keine Übereinstimmung gefunden wird.

Die UseRouting-Middleware verwendet die SetEndpoint-Methode, um den Endpunkt an den aktuellen Kontext anzufügen. Es ist möglich, die UseRouting-Middleware durch benutzerdefinierte Logik zu ersetzen und dennoch die Vorteile durch die Verwendung von Endpunkten zu nutzen. Endpunkte befinden sich auf niedriger Ebene, wie Middleware, und sind nicht an die Routingimplementierung gekoppelt. Die meisten Apps müssen UseRouting nicht durch eigene Logik ersetzen.

Die UseEndpoints-Middleware ist so konzipiert, dass Sie zusammen mit der UseRouting-Middleware verwendet werden kann. Die Hauptlogik zum Ausführen eines Endpunkts ist nicht kompliziert. Mit GetEndpoint können Sie einen Endpunkt abrufen und dann dessen RequestDelegate-Eigenschaft aufrufen.

Der folgende Code veranschaulicht, wie Middleware das Routing beeinflussen oder darauf reagieren kann:

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

Im vorherigen Beispiel werden zwei wichtige Konzepte dargestellt:

  • Die Middleware kann vor UseRouting ausgeführt werden, um die Daten zu ändern, auf denen das Routing basiert.
  • Die Middleware kann zwischen UseRouting und UseEndpoints ausgeführt werden, um die Ergebnisse des Routings vor der Ausführung des Endpunkts zu verarbeiten.
    • Middleware, die zwischen UseRouting und UseEndpoints ausgeführt wird:
      • Überprüft in der Regel die Metadaten, um die Endpunkte zu ermitteln.
      • Trifft häufig Sicherheitsentscheidungen, wie UseAuthorization und UseCors.
    • Durch die Kombination aus Middleware und Metadaten ist es möglich, für jeden einzelnen Endpunkt Richtlinien zu konfigurieren.

Der vorangehende Code zeigt ein Beispiel für eine benutzerdefinierte Middleware, die entpunktbezogene Richtlinien unterstützt. Die Middleware schreibt ein Überwachungsprotokoll für den Zugriff auf vertrauliche Daten in der Konsole. Die Middleware kann so konfiguriert werden, dass ein Endpunkt mit den RequiresAuditAttribute-Metadaten überwacht wird. In diesem Beispiel wird ein Opt-In-Muster veranschaulicht, bei dem nur Endpunkte überwacht werden, die als vertraulich markiert sind. Es ist möglich, diese Logik umgekehrt zu definieren, indem beispielsweise alles geprüft wird, was nicht als sicher markiert ist. Das Endpunktmetadaten-System ist flexibel. Diese Logik lässt sich für jeden Anwendungsfall passend schreiben.

Der vorherige Beispielcode soll die grundlegenden Konzepte von Endpunkten veranschaulichen. Das Beispiel ist nicht für Produktionsumgebungen vorgesehen. Eine vollständigere Version einer Middleware für Überwachungsprotokolle würde Folgendes bieten:

  • Protokollieren in einer Datei oder einer Datenbank.
  • Einschließen von Details wie Benutzer, IP-Adresse, Name des vertraulichen Endpunkts usw.

Die Metadaten der Überwachungsrichtlinie RequiresAuditAttribute sind als Attribute definiert, um die Verwendung mit klassenbasierten Frameworks wie Controllern und SignalR zu erleichtern. Bei Verwendung von Route-zu-Code:

  • Metadaten werden an eine Generator-API angefügt.
  • Klassenbasierte Frameworks enthalten beim Erstellen von Endpunkten alle Attribute der entsprechenden Methode und Klasse.

Die bewährten Methoden für Metadatentypen sind, sie entweder als Schnittstellen oder als Attribute zu definieren. Schnittstellen und Attribute ermöglichen die Wiederverwendung von Code. Das Metadatensystem ist flexibel und weist keine Einschränkungen auf.

Vergleichen von Terminalmiddleware mit Routing

Im folgenden Beispiel werden sowohl Terminalmiddleware als auch Routing veranschaulicht:

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Beim in Approach 1: gezeigten Stil von Middleware handelt es sich um Terminalmiddleware. Sie wird als Terminalmiddleware bezeichnet, da sie einen Abgleich durchgeführt.

  • Der Abgleich im vorangehenden Beispiel ist Path == "/" für die Middleware und Path == "/Routing" für das Routing.
  • Bei einem erfolgreichen Abgleich führt sie bestimmte Funktionen aus und kehrt zurück, anstatt die next-Middleware aufzurufen.

Sie wird „Terminal Middleware“ genannt, da sie die Suche beendet, einige Funktionen ausführt und dann zurückkehrt.

In der folgenden Liste werden Terminalmiddleware und Routing verglichen:

  • Beide Ansätze ermöglichen das Beenden der Verarbeitungspipeline:
    • Die Middleware beendet die Pipeline, indem Sie zurückkehrt, anstatt next aufzurufen.
    • Endpunkte beenden immer die Verarbeitung.
  • Terminalmiddleware ermöglicht die Positionierung der Middleware an einer beliebigen Stelle in der Pipeline:
    • Endpunkte werden an der Position von UseEndpoints ausgeführt.
  • Mit Terminalmiddleware kann beliebiger Code beendet werden, wenn die Middleware übereinstimmt:
    • Ein benutzerdefinierter Routenabgleichscode kann sehr umständlich und schwierig zu schreiben sein.
    • Das Routing bietet unkomplizierte Lösungen für typische Apps. Die meisten Apps erfordern keinen benutzerdefinierten Routenabgleichscode.
  • Endpunkte haben eine Schnittstelle mit Middleware wie UseAuthorization und UseCors.
    • Die Verwendung einer Terminalmiddleware mit UseAuthorization oder UseCors erfordert eine manuelle Verknüpfung mit dem Autorisierungssystem.

Ein Endpunkt definiert beides:

  • Einen Delegaten zum Verarbeiten von Anforderungen.
  • Eine Sammlung beliebiger Metadaten. Die Metadaten werden zur Implementierung von übergreifenden Belangen verwendet, die auf Richtlinien und der Konfiguration basieren, die den einzelnen Endpunkten angefügt sind.

Terminalmiddleware kann sehr nützlich sein, erfordert aber möglicherweise auch:

  • Einen großen Codierungs- und Testaufwand.
  • Eine manuelle Integration in andere Systeme für die gewünschte Flexibilität.

Ziehen Sie daher zunächst die Integration von Routingfunktionen in Betracht, bevor Sie damit beginnen, Terminalmiddleware zu schreiben.

Vorhandene Terminalmiddleware, die in Map oder MapWhen integriert ist, kann in der Regel in einen routingfähigen Endpunkt umgewandelt werden. MapHealthChecks zeigt das Muster für eine Routinglösung:

  • Schreiben Sie eine Erweiterungsmethode in der IEndpointRouteBuilder-Schnittstelle.
  • Erstellen Sie eine geschachtelte Middlewarepipeline mit CreateApplicationBuilder.
  • Fügen Sie die Middleware an die neue Pipeline an. In diesem Fall UseHealthChecks.
  • Verwenden Sie Build, um die Middlewarepipeline in einem RequestDelegate-Delegaten zu erstellen.
  • Rufen Sie Map auf, und stellen Sie die neue Middlewarepipeline bereit.
  • Geben Sie das Generatorobjekt zurück, das von Map aus der Erweiterungsmethode bereitgestellt wurde.

Im folgenden Code ist die Verwendung von MapHealthChecks gezeigt:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

Das vorangehende Beispiel zeigt, warum das Zurückgeben des Generatorobjekts wichtig ist. Wenn das Generatorobjekt zurückgegeben wird, kann der App-Entwickler Richtlinien konfigurieren, z. B. die Autorisierung für den Endpunkt. In diesem Beispiel ist die Middleware für Integritätsprüfungen nicht direkt in das Autorisierungssystem integriert.

Das Metadatensystem wurde als Antwort auf die Probleme erstellt, die von Erweiterbarkeitsautoren mithilfe von Terminalmiddleware aufgetreten sind. Für jede Middleware ist es problematisch, deren eigene Integration in das Autorisierungssystem umzusetzen.

URL-Zuordnung

  • Bei diesem Prozess werden eingehende Anforderungen durch Routing mit einem Endpunkt abgeglichen.
  • Basiert auf Daten im URL-Pfad und den URL-Headern.
  • Kann erweitert werden, um beliebige Daten in der Anforderung zu überprüfen.

Wenn eine Routingmiddleware ausgeführt wird, legt sie ein Endpoint-Element fest und leitet Werte an eine Anforderungsfunktion in der HttpContext-Klasse der aktuellen Anforderung weiter:

  • Durch Aufrufen von HttpContext.GetEndPoint wird der Endpunkt abgerufen.
  • HttpRequest.RouteValues ruft die Sammlung der Routenwerte ab.

Middleware, die nach der Routingmiddleware ausgeführt wird, kann den Endpunkt untersuchen und Maßnahmen ergreifen. So kann beispielsweise eine Autorisierungsmiddleware die Erfassung der Metadaten des Endpunkts für eine Autorisierungsrichtlinie abfragen. Nachdem die gesamte Middleware in der Anforderungsverarbeitungspipeline ausgeführt wurde, wird der Delegat des ausgewählten Endpunkts aufgerufen.

Das Routingsystem ist beim Endpunktrouting für alle Weiterleitungsentscheidungen zuständig. Da die Middleware Richtlinien auf der Grundlage des ausgewählten Endpunkts anwendet, ist Folgendes wichtig:

  • Alle Entscheidungen, die sich auf die Verteilung oder die Anwendung von Sicherheitsrichtlinien auswirken können, werden im Routingsystem getroffen.

Warnung

Wenn für Abwärtskompatibilität ein Controller- oder Razor Pages-Endpunktdelegat ausgeführt wird, werden die Eigenschaften von RouteContext.RouteData auf Grundlage der bisher verarbeiteten Anforderungen auf entsprechende Werte festgelegt.

Der Typ RouteContext wird in einer zukünftigen Version als veraltet markiert:

  • Migrieren Sie RouteData.Values zu HttpRequest.RouteValues.
  • Migrieren Sie RouteData.DataTokens, um IDataTokensMetadata aus den Endpunktmetadaten abzurufen.

Der URL-Abgleich erfolgt in mehreren Phasen und kann konfiguriert werden. In jeder Phase werden mehrere Übereinstimmungen ausgegeben. Diese Übereinstimmungen lassen sich in der nächsten Phase weiter eingrenzen. Die Routingimplementierung garantiert keine Verarbeitungsreihenfolge für übereinstimmende Endpunkte. Alle möglichen Übereinstimmungen werden gleichzeitig verarbeitet. Für die URL-Abgleichsphasen gilt folgende Reihenfolge. ASP.NET Core:

  1. Verarbeitet den URL-Pfad mit mehreren Endpunkten und ihren Routenvorlagen und sammelt alle Übereinstimmungen.
  2. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den angewendeten Routeneinschränkungen passen.
  3. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den MatcherPolicy-Instanzen passen.
  4. Verwendet EndpointSelector, um eine abschließende Entscheidung anhand der vorangehenden Liste zu treffen.

Die Liste der Endpunkte wird entsprechend den folgenden Punkten priorisiert:

Alle übereinstimmenden Endpunkte werden in jeder Phase verarbeitet, bis EndpointSelector erreicht ist. EndpointSelector stellt die abschließende Phase dar. Darin wird der Endpunkt mit der höchsten Priorität aus den Übereinstimmungen als beste Übereinstimmung ausgewählt. Wenn es andere Übereinstimmungen mit derselben Priorität wie die beste Übereinstimmung gibt, wird ein Ausnahmefehler wegen einer nicht eindeutigen Übereinstimmung ausgelöst.

Die Routenpriorität wird anhand einer spezifischeren Routenvorlage berechnet, der eine höhere Priorität eingeräumt wird. Dies wird z. B. anhand der Vorlagen /hello und /{message} deutlich:

  • Beide stimmen mit dem URL-Pfad /hello überein.
  • /hello ist spezifischer und hat daher höhere Priorität.

Im Allgemeinen eignet sich die Routenpriorität gut, um die beste Übereinstimmung für die in der Praxis verwendeten URL-Schemata zu finden. Verwenden Sie Order nur bei Bedarf, um eine Mehrdeutigkeit zu vermeiden.

Aufgrund der Erweiterungsmöglichkeiten, die das Routing bietet, kann das Routingsystem die mehrdeutigen Routen nicht im Voraus berechnen. Betrachten Sie ein Beispiel wie die Routenvorlagen /{message:alpha} und /{message:int}:

  • Die alpha-Einschränkung gleicht nur alphabetische Zeichen ab.
  • Die int-Einschränkung gleicht nur Zahlen ab.
  • Diese Vorlagen haben die gleiche Routenpriorität, aber es gibt keine einzige URL, auf die sie beide zutreffen.
  • Wenn das Routingsystem beim Starten einen Mehrdeutigkeitsfehler gemeldet hat, würde dieser den gültigen Anwendungsfall blockieren.

Warnung

Die Reihenfolge der Vorgänge in UseEndpoints wirkt sich nicht auf das Routingverhalten aus, mit einer Ausnahme. MapControllerRoute und MapAreaRoute weisen ihren Endpunkten automatisch einen Reihenfolgenwert zu, basierend auf der Reihenfolge, in der sie aufgerufen werden. Dadurch wird das Langzeitverhalten von Controllern simuliert, ohne dass das Routingsystem die gleichen Garantien bietet wie ältere Routingimplementierungen.

Endpunktrouting in ASP.NET Core:

  • Weist nicht das Konzept von Routen auf.
  • Bietet keine garantierte Reihenfolge. Alle Endpunkte werden gleichzeitig verarbeitet.

Routenvorlagenpriorität und Reihenfolge der Endpunktauswahl

Die Routenvorlagenpriorität ist ein System, bei dem jeder Routenvorlage ein Wert zugewiesen wird, je nachdem, wie spezifisch diese ist. Routenvorlagenpriorität:

  • Verhindert, dass die Reihenfolge der Endpunkte häufig angepasst werden muss.
  • Versucht, die allgemeinen Erwartungen an das Routingverhalten abzugleichen.

Dies wird z. B. anhand der Vorlagen /Products/List und /Products/{id} deutlich. Es wäre begründet, anzunehmen, dass /Products/List eine bessere Übereinstimmung als /Products/{id} für den URL-Pfad /Products/List ist. Dies funktioniert, weil das Literalsegment /List eine höhere Priorität als das Parametersegment /{id} hat.

Wie die Priorisierung im Einzelnen funktioniert, ist an die Definition der Routenvorlagen gekoppelt:

  • Vorlagen mit mehr Segmenten sind in der Regel spezifischer.
  • Ein Segment mit Literaltext gilt als spezifischer als ein Parametersegment.
  • Ein Parametersegment mit einer Einschränkung gilt als spezifischer als ein Parametersegment ohne Einschränkung.
  • Ein komplexes Segment wird als genauso spezifisch betrachtet wie ein Parametersegment mit einer Einschränkung.
  • Catch-All-Parameter, die am wenigsten spezifisch sind. Unter catch-all im Abschnitt Routenvorlagen finden Sie wichtige Informationen zu catch-all-Routen.

Konzepte zur URL-Generierung

URL-Generierung:

  • Der Prozess, bei dem durch Routing ein URL-Pfad basierend auf mehreren Routenwerten erstellt wird.
  • Sie ermöglicht eine logische Trennung zwischen den Endpunkten und den URLs, die auf diese zugreifen.

Das Endpunktrouting umfasst die API zur Linkgenerierung (LinkGenerator). LinkGenerator ist ein Singleton-Dienst, der in DI verfügbar ist. Die LinkGenerator-API kann außerhalb des Kontexts einer ausgeführten Anforderung verwendet werden. Mvc.IUrlHelper und Szenarios, die von IUrlHelper abhängig sind (z. B. Taghilfsprogramme, HTML-Hilfsprogramme und Aktionsergebnisse), verwenden die LinkGenerator-API, um entsprechende Funktionen bereitzustellen.

Die API zur Linkgenerierung wird von Konzepten wie Adressen und Adressschemas unterstützt. Sie können mithilfe eines Adressschemas die Endpunkte bestimmen, die bei der Linkgenerierung berücksichtigt werden sollen. Beispielsweise werden Routennamen und Routenwerte als Adressschemas implementiert. Diese Szenarios kennen viele Benutzer von Controllern und Razor Pages.

Die API zur Linkgenerierung kann Controller und Razor Pages über die folgenden Erweiterungsmethoden miteinander verknüpfen:

Beim Überladen dieser Methoden werden Argumente akzeptiert, die den HttpContext umfassen. Diese Methoden sind zwar in funktionaler Hinsicht äquivalent zu Url.Action und Url.Page, bieten aber zusätzliche Flexibilität und Optionen.

Die GetPath*-Methoden sind Url.Action und Url.Page in der Hinsicht ähnlich, dass sie einen URI mit einem absoluten Pfad generieren. Die GetUri*-Methoden generieren immer einen absoluten URI mit einem Schema und einem Host. Die Methoden, die einen HttpContext akzeptieren, generieren im Kontext der ausgeführten Anforderung einen URI. Die Umgebungsroutenwerte, der URL-basierte Pfad, das Schema und der Host von der ausführenden Anforderung werden so lange verwendet, bis sie außer Kraft gesetzt werden.

LinkGenerator wird mit einer Adresse aufgerufen. Ein URI wird in zwei Schritten generiert:

  1. Eine Adresse wird an eine Liste von Endpunkten gebunden, die der Adresse zugeordnet werden können.
  2. Jedes RoutePattern eines Endpunkts wird bewertet, bis ein Routenmuster gefunden wird, das den angegebenen Werten zugeordnet werden kann. Die daraus resultierende Ausgabe wird mit URI-Teilen kombiniert, die für die API zur Linkgenerierung bereitgestellt wird, und zurückgegeben.

Die von LinkGenerator bereitgestellten Methoden unterstützen die Standardfunktionen zur Generierung von Links für jeden beliebigen Adresstypen. Am praktischsten ist es, die API zur Linkgenerierung mit Erweiterungsmethoden zu verwenden, die Vorgänge für einen bestimmten Adresstypen ausführen:

Erweiterungsmethode Beschreibung
GetPathByAddress Generiert einen URI mit einem absoluten Pfad, der auf den angegebenen Werten basiert.
GetUriByAddress Generiert einen absoluten URI, der auf den angegebenen Werten basiert.

Warnung

Beachten Sie die folgenden Implikationen zum Aufrufen von LinkGenerator-Methoden:

  • Verwenden Sie GetUri*-Erweiterungsmethoden in App-Konfigurationen, die den Host-Header von eingehenden Anforderungen nicht überprüfen, mit Bedacht. Wenn der Host-Header von eingehenden Anforderungen nicht überprüft wird, können nicht vertrauenswürdige Anforderungseingaben zurück an den Client in URIs einer Ansicht bzw. Seite zurückgesendet werden. Es wird empfohlen, dass alle Produktions-Apps ihren Server so konfigurieren, dass der Host-Header auf bekannte gültige Werte überprüft wird.

  • Verwenden Sie LinkGenerator in Kombination mit Map oder MapWhen in Middleware mit Bedacht. Map* ändert den Basispfad der ausgeführten Anforderung. Dies beeinflusst die Ausgabe der Linkgenerierung. Für alle LinkGenerator-APIs ist die Angabe eines Basispfads zulässig. Geben Sie einen leeren Basispfad an, um die Auswirkungen von Map* auf die Linkgenerierung rückgängig zu machen.

Middlewarebeispiel

Im folgenden Beispiel verwendet eine Middleware die LinkGenerator-API, um eine Verknüpfung zu einer Aktionsmethode herzustellen, die Speicherprodukte aufführt. Sie können für jede beliebige Klasse in einer App die API zur Linkgenerierung verwenden, indem Sie diese in eine Klasse einfügen und GenerateLink aufrufen:

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}

Routenvorlagen

Token in {} definieren Routenparameter, die beim Abgleich der Route gebunden werden. In einem Routensegment können mehrere Routenparameter definiert werden, müssen aber durch einen Literalwert getrennt werden. Beispiel:

{controller=Home}{action=Index}

ist keine gültige Route, da sich zwischen {controller} und {action} kein Literalwert befindet. Routenparameter müssen einen Namen besitzen und können zusätzliche Attribute aufweisen.

Eine Literalzeichenfolge, die nicht den Routenparametern entspricht (z.B. {id}), muss zusammen mit dem Pfadtrennzeichen / mit dem URL-Text übereinstimmen. Beim Abgleich von Text wird nicht zwischen Groß-/Kleinbuchstaben unterschieden, und die Übereinstimmung basiert auf der decodierten Repräsentation des URL-Pfads. Damit das Trennzeichen ({ oder }) der Routenparameter-Literalzeichenfolge bei einem Abgleich gefunden wird, muss es doppelt vorhanden sein, was einem Escapezeichen entspricht. Beispielsweise {{ oder }}.

Sternchen * oder Doppelsternchen **:

  • Kann als Präfix für einen Routenparameter verwendet werden, um an den rest des URI zu binden.
  • Werden Catch-All-Parameter genannt. Zum Beispiel blog/{**slug}:
    • Entspricht einem beliebigen URI, der mit blog/ beginnt und einem beliebigen Wert folgt.
    • Der Wert nach blog/ wird dem Slug-Routenwert zugewiesen.

Warnung

Ein catch-all-Parameter kann aufgrund eines Fehlers beim Routing nicht ordnungsgemäß mit Routen übereinstimmen. Apps, die von diesem Fehler betroffen sind, weisen die folgenden Merkmale auf:

  • Eine catch-all-Route, zum Beispiel {**slug}"
  • Die catch-all-Route kann nicht mit Anforderungen abgeglichen werden, die abgeglichen werden sollen.
  • Durch das Entfernen anderer Routen funktioniert die catch-all-Route.

Weitere Beispiele zu diesem Fehler finden Sie in den GitHub-Issues 18677 und 16579.

Eine Opt-in-Behebung für diesen Fehler ist im .NET Core 3.1.301 SDK und höher enthalten. Der folgende Code legt einen internen Switch fest, mit dem dieser Fehler behoben wird:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Durch Catch-All-Parameter können auch leere Zeichenfolgen gefunden werden.

Der Catch-All-Parameter schützt die entsprechenden Zeichen (Escaping), wenn die Route verwendet wird, um eine URL, einschließlich Pfadtrennzeichen (/) zu generieren. Z.B. generiert die Route foo/{*path} mit den Routenwerten { path = "my/path" }foo/my%2Fpath. Beachten Sie den umgekehrten Schrägstrich mit Escapezeichen. Um Trennzeichen für Roundtrips einsetzen zu können, verwenden Sie das Routenparameterpräfix **. Die Route foo/{**path} mit { path = "my/path" } generiert foo/my/path.

Bei einem URL-Muster, durch das ein Dateiname mit einer optionalen Erweiterung erfasst werden soll, sind noch weitere Aspekte zu berücksichtigen. Dies wird z.B. anhand der Vorlage files/{filename}.{ext?} deutlich. Wenn sowohl für filename als auch für ext Werte vorhanden sind, werden beide Werte angegeben. Wenn nur für filename ein Wert in der URL vorhanden ist, wird für die Route eine Übereinstimmung ermittelt, da der nachstehende Punkt (.) optional ist. Für die folgenden URLs wird eine Übereinstimmung für die Route ermittelt:

  • /files/myFile.txt
  • /files/myFile

Routenparameter können über mehrere Standardwerte verfügen, die nach dem Parameternamen angegeben werden und durch ein Gleichheitszeichen (=) voneinander getrennt werden. Mit {controller=Home} wird beispielsweise Home als Standardwert für controller definiert. Der Standardwert wird verwendet, wenn kein Wert in der Parameter-URL vorhanden ist. Routenparameter sind optional, wenn am Ende des Parameternamens ein Fragezeichen (?) angefügt wird. Beispielsweise id?. Zwischen optionalen Werten und Standardroutenparametern besteht folgender Unterschied:

  • Ein Routenparameter mit einem Standardwert erzeugt immer einen Wert.
  • Ein optionaler Parameter hat nur dann einen Wert, wenn ein Wert von der Anforderungs-URL bereitgestellt wird.

Routenparameter können Einschränkungen aufweisen, die mit dem gebundenen Routenwert der URL übereinstimmen müssen. Eine Inline-Einschränkung für einen Routenparameter geben Sie an, indem Sie hinter dem Namen des Routenparameters einen Doppelpunkt (:) und einen Einschränkungsnamen hinzufügen. Wenn für die Einschränkung Argumente erforderlich sind, werden diese nach dem Einschränkungsnamen in Klammern ((...)) eingeschlossen. Mehrere Inline-Einschränkungen können festgelegt werden, indem ein weiterer Doppelpunkt (:) und Einschränkungsname hinzugefügt werden.

Der Einschränkungsname und die Argumente werden dem IInlineConstraintResolver-Dienst übergeben, wodurch eine Instanz von IRouteConstraint für die URL-Verarbeitung erstellt werden kann. In der Routenvorlage blog/{article:minlength(10)} wird beispielsweise die Einschränkung minlength mit dem Argument 10 festgelegt. Weitere Informationen zu Routeneinschränkungen und eine Liste der vom Framework bereitgestellten Einschränkungen finden Sie im Abschnitt Routeneinschränkungen.

Routenparameter können darüber hinaus über Parametertransformatoren verfügen. Diese wandeln den Wert eines Parameters beim Generieren von Links um und passen Aktionen und Seiten an URLs an. Wie Einschränkungen können auch Parametertransformatoren einem Routenparameter inline hinzugefügt werden, indem ein Doppelpunkt (:) und der Name des Transformators hinter dem Namen des Routenparameters hinzugefügt werden. In der Routenvorlage blog/{article:slugify} wird beispielsweise der Transformator slugify festgelegt. Weitere Informationen zu Parametertransformatoren finden Sie im Abschnitt Parametertransformatoren.

Die folgende Tabelle enthält Beispielvorlagen für Routen und deren Verhalten:

Routenvorlage Beispiel-URI für Übereinstimmung Der Anforderungs-URI
hello /hello Nur für den Pfad /hello wird eine Übereinstimmung ermittelt.
{Page=Home} / Eine Übereinstimmung wird ermittelt, und Page wird auf Home festgelegt.
{Page=Home} /Contact Eine Übereinstimmung wird ermittelt, und Page wird auf Contact festgelegt.
{controller}/{action}/{id?} /Products/List Stimmt mit dem Products-Controller und der List-Aktion überein.
{controller}/{action}/{id?} /Products/Details/123 Wird dem Controller Products und der Aktion Details zugeordnet, bei der id auf 123 festgelegt ist.
{controller=Home}/{action=Index}/{id?} / Stimmt mit dem Home-Controller und der Index-Methode überein. id wird ignoriert.
{controller=Home}/{action=Index}/{id?} /Products Stimmt mit dem Products-Controller und der Index-Methode überein. id wird ignoriert.

Mit Vorlagen lässt sich Routing besonders leicht durchführen. Einschränkungen und Standardwerte können auch außerhalb der Routenvorlage angegeben werden.

Komplexe Segmente

Komplexe Segmente werden von rechts nach links auf eine nicht gierige Weise durch entsprechende Literaltrennzeichen verarbeitet. Beispielsweise ist [Route("/a{b}c{d}")] ein komplexes Segment. Komplexe Segmente funktionieren auf eine bestimmte Weise, die für eine erfolgreiche Verwendung verstanden werden muss. Das Beispiel in diesem Abschnitt zeigt, warum komplexe Segmente nur dann wirklich gut funktionieren, wenn der Trennzeichentext nicht innerhalb der Parameterwerte erscheint. Für komplexere Fälle ist die Verwendung eines RegEx und das anschließende manuelle Extrahieren der Werte erforderlich.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Dies ist eine Zusammenfassung der Schritte, die beim Routing mit der Vorlage /a{b}c{d} und dem URL-Pfad /abcd ausgeführt werden. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen:

  • Das erste Literal von rechts nach links ist c. Daher wird /abcd von rechts durchsucht und /ab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /ab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Es ist kein verbleibender Text und keine verbleibende Routenvorlage vorhanden. Folglich ist dies eine Übereinstimmung.

Nachfolgend ist ein Beispiel für einen negativen Fall mit derselben Vorlage /a{b}c{d} und dem URL-Pfad /aabcd. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen: Bei diesem Fall handelt es sich nicht um eine Übereinstimmung, was durch denselben Algorithmus belegt wird:

  • Das erste Literal von rechts nach links ist c. Daher wird /aabcd von rechts durchsucht und /aab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /aab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /a|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Zu diesem Zeitpunkt gibt es noch verbleibenden Text a, aber es gibt keine Routenvorlage mehr, die der Algorithmus analysieren kann, weshalb dies keine Übereinstimmung ist.

Da der übereinstimmende Algorithmus nicht gierig ist:

  • Entspricht er der kleinstmöglichen Textmenge in jedem Schritt.
  • Alle Fälle, in denen der Trennzeichenwert in den Parameterwerten angezeigt wird, stimmen nicht überein.

Reguläre Ausdrücke bieten eine viel bessere Kontrolle über das Abgleichsverhalten.

Beim Greedy-Abgleich, auch als maximaler Abgleich bezeichnet, wird nach der längsten möglichen Übereinstimmung im Eingabetext gesucht, die dem RegEx-Muster entspricht. Beim Non-Greedy-Abgleich, auch bekannt als Lazy Matching, wird nach der kürzesten möglichen Übereinstimmung im Eingabetext gesucht, die dem RegEx-Muster entspricht.

Routing mit Sonderzeichen

Ein Routing mit Sonderzeichen kann zu unerwarteten Ergebnissen führen. Stellen Sie sich z. B. einen Controller mit der folgenden Aktionsmethode vor:

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

Wenn string id die folgenden codierten Werte enthält, können unerwartete Ergebnisse auftreten:

ASCII Codiert
/ %2F
+

Routenparameter sind nicht immer URL-decodiert. Dieses Problem wird möglicherweise in Zukunft behoben. Weitere Informationen finden Sie in diesem GitHub-Issue.

Routeneinschränkungen

Routeneinschränkungen werden angewendet, wenn eine Übereinstimmung mit der eingehenden URL gefunden wurde und der URL-Pfad in Routenwerten mit Token versehen wird. In der Regel wird mit Routeneinschränkungen der Routenwert der zugehörigen Vorlage geprüft. Dabei wird anhand einer True/False-Entscheidung bestimmt, ob der Wert gültig ist. Für einige Routeneinschränkungen werden anstelle des Routenwerts andere Daten verwendet, um zu ermitteln, ob das Routing einer Anforderung möglich ist. HttpMethodRouteConstraint kann beispielsweise auf der Grundlage des HTTP-Verbs eine Anforderung entweder annehmen oder ablehnen. Einschränkungen werden in Routinganforderungen und bei der Linkgenerierung verwendet.

Warnung

Verwenden Sie keine Einschränkungen für die Eingabeüberprüfung. Wenn Einschränkungen für die Eingabevalidierung verwendet werden, führt eine ungültige Eingabe zu einem 404-Fehler (Nicht gefunden). Eine ungültige Eingabe sollte zu einer ungültigen Anforderung (400) mit einer entsprechenden Fehlermeldung führen. Verwenden Sie Routeneinschränkungen nicht, um Eingaben für eine bestimmte Route zu überprüfen, sondern um ähnliche Routen zu unterscheiden.

In der folgenden Tabelle werden Beispiele für Routeneinschränkungen und deren zu erwartendes Verhalten beschrieben:

Einschränkung Beispiel Beispiele für Übereinstimmungen Hinweise
int {id:int} 123456789, -123456789 Für jeden Integer wird eine Übereinstimmung ermittelt.
bool {active:bool} true, FALSE Entspricht true oder false. Groß-/Kleinschreibung nicht beachten
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Entspricht einem gültigen DateTime-Wert in der invarianten Kultur. Siehe vorherige Warnung.
decimal {price:decimal} 49.99, -1,000.01 Entspricht einem gültigen decimal-Wert in der invarianten Kultur. Siehe vorherige Warnung.
double {weight:double} 1.234, -1,001.01e8 Entspricht einem gültigen double-Wert in der invarianten Kultur. Siehe vorherige Warnung.
float {weight:float} 1.234, -1,001.01e8 Entspricht einem gültigen float-Wert in der invarianten Kultur. Siehe vorherige Warnung.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Für einen gültigen Guid-Wert wird eine Übereinstimmung ermittelt.
long {ticks:long} 123456789, -123456789 Für einen gültigen long-Wert wird eine Übereinstimmung ermittelt.
minlength(value) {username:minlength(4)} Rick Die Zeichenfolge muss mindestens eine Länge von 4 Zeichen aufweisen.
maxlength(value) {filename:maxlength(8)} MyFile Die Zeichenfolge darf maximal eine Länge von 8 Zeichen aufweisen.
length(length) {filename:length(12)} somefile.txt Die Zeichenfolge muss genau 12 Zeichen aufweisen.
length(min,max) {filename:length(8,16)} somefile.txt Die Zeichenfolge muss mindestens eine Länge von 8 und darf maximal eine Länge von 16 Zeichen aufweisen.
min(value) {age:min(18)} 19 Der Integerwert muss mindestens 18 sein.
max(value) {age:max(120)} 91 Der Integerwert darf nicht größer als 120 sein.
range(min,max) {age:range(18,120)} 91 Der Integerwert muss zwischen 18 und 120 liegen.
alpha {name:alpha} Rick Die Zeichenfolge muss aus mindestens einem alphabetische Zeichen bestehen, a-z und ohne Unterscheidung zwischen Groß-/Kleinbuchstaben.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 Die Zeichenfolge muss mit dem regulären Ausdruck übereinstimmen. Weitere Informationen finden Sie unter Tipps zum Definieren eines regulären Ausdrucks.
required {name:required} Rick Hierdurch wird erzwungen, dass ein Wert, der kein Parameter ist, für die URL-Generierung vorhanden sein muss.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Auf einen einzelnen Parameter können mehrere durch Doppelpunkte getrennte Einschränkungen angewendet werden. Durch die folgende Einschränkung wird ein Parameter beispielsweise auf einen Integerwert größer oder gleich 1 beschränkt:

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Warnung

Für Routeneinschränkungen, mit denen die URL überprüft wird und die in den CLR-Typ umgewandelt werden, wird immer die invariante Kultur verwendet. Dies gilt z. B. für die Konvertierung in den CLR-Typ int oder DateTime. Diese Einschränkungen setzen voraus, dass die URL nicht lokalisierbar ist. Die vom Framework bereitgestellten Routeneinschränkungen ändern nicht die Werte, die in Routenwerten gespeichert sind. Alle Routenwerte, die aus der URL analysiert werden, werden als Zeichenfolgen gespeichert. Durch die float-Einschränkung wird beispielsweise versucht, den Routenwert in einen Gleitkommawert zu konvertieren. Mit dem konvertierten Wert wird allerdings nur überprüft, ob eine Umwandlung überhaupt möglich ist.

Reguläre Ausdrücke in Einschränkungen

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Reguläre Ausdrücke können mithilfe der regex(...)-Routeneinschränkung als Inline-Einschränkungen angegeben werden. Methoden der MapControllerRoute-Familie akzeptieren auch ein Objektliteral von Einschränkungen. Wenn dieses Formular verwendet wird, werden Zeichenfolgenwerte als reguläre Ausdrücke interpretiert.

Der folgende Code verwendet eine Inline-RegEx-Einschränkung:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

Der folgende Code verwendet ein Objektliteral, um eine RegEx-Einschränkung anzugeben:

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

Im ASP.NET Core-Framework wird dem Konstruktor für reguläre Ausdrücke RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant hinzugefügt. Eine Beschreibung dieser Member finden Sie unter RegexOptions.

In regulären Ausdrücken werden Trennzeichen und Token verwendet, die auch beim Routing und in der Programmiersprache C# in ähnlicher Weise verwendet werden. Token, die reguläre Ausdrücke enthalten, müssen mit einem Escapezeichen versehen werden. Wenn Sie den regulären Ausdruck ^\d{3}-\d{2}-\d{4}$ in einer Inline-Einschränkung verwenden möchten, nutzen Sie eine der folgenden Optionen:

  • Ersetzen Sie \-Zeichen in der Zeichenfolge durch \\-Zeichen in der C#-Quelldatei, um das Escapezeichen für die Zeichenfolge \ zu setzen.
  • Ausführliche Zeichenfolgeliterale

Wenn Sie Trennzeichen für Routenparameter mit Escapezeichen versehen möchten ({, }, [, ]), geben Sie jedes Zeichen im Ausdruck doppelt ein (z. B. {{, }}, [[, ]]). In der folgenden Tabelle werden reguläre Ausdrücke und Ausdrücke aufgeführt, die mit Escapezeichen versehen sind:

Regulärer Ausdruck Mit Escapezeichen versehener regulärer Ausdruck
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Beim Routing verwendete reguläre Ausdrücke beginnen oft mit dem ^-Zeichen und stellen die Startposition der Zeichenfolge dar. Die Ausdrücke enden häufig mit einem Dollarzeichen ($) und stellen das Ende der Zeichenfolge dar. Mit den Zeichen ^ und $ wird sichergestellt, dass der reguläre Ausdruck mit dem vollständigen Routenparameterwert übereinstimmt. Ohne die Zeichen ^ und $ werden mit dem regulären Ausdruck alle Teilzeichenfolgen ermittelt, was häufig nicht gewünscht ist. In der folgenden Tabelle finden Sie Beispiele für reguläre Ausdrücke. Außerdem wird erklärt, warum ein Abgleich erfolgreich ist oder fehlschlägt:

expression Zeichenfolge Match Kommentar
[a-z]{2} hello Ja Teilzeichenfolge stimmt überein
[a-z]{2} 123abc456 Ja Teilzeichenfolge stimmt überein
[a-z]{2} mz Ja Ausdruck stimmt überein
[a-z]{2} MZ Ja keine Unterscheidung zwischen Groß-/Kleinbuchstaben
^[a-z]{2}$ hello Nein siehe Erläuterungen zu ^ und $ oben
^[a-z]{2}$ 123abc456 Nein siehe Erläuterungen zu ^ und $ oben

Weitere Informationen zur Syntax von regulären Ausdrücken finden Sie unter Sprachelemente für reguläre Ausdrücke – Kurzübersicht.

Einen regulären Ausdruck können Sie verwenden, um einen Parameter auf zulässige Werte einzuschränken. Mit {action:regex(^(list|get|create)$)} werden beispielsweise für den action-Routenwert nur die Werte list, get oder create abgeglichen. Wenn die Zeichenfolge ^(list|get|create)$ dem Einschränkungswörterbuch übergeben wird, führt dies zum gleichen Ergebnis. Auch Einschränkungen, die dem zugehörigen Wörterbuch hinzugefügt werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden als reguläre Ausdrücke behandelt. Einschränkungen, die innerhalb einer Vorlage übergeben werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden nicht als reguläre Ausdrücke behandelt.

Benutzerdefinierte Routeneinschränkungen

Benutzerdefinierte Routeneinschränkungen können durch Implementierung der IRouteConstraint-Schnittstelle erstellt werden. Die IRouteConstraint-Schnittstelle umfasst die Match-Methode, die true zurückgibt, wenn die Einschränkung erfüllt wird, und andernfalls false.

Benutzerdefinierte Routeneinschränkungen werden nur selten benötigt. Bevor Sie eine benutzerdefinierte Routeneinschränkung implementieren, sollten Sie Alternativen in Betracht ziehen, wie z. B. Modellbindung.

Der ASP.NET Core-Ordner Constraints bietet nützliche Beispiele für die Erstellung von Einschränkungen. Beispiel: GuidRouteConstraint.

Zum Verwenden eines benutzerdefinierten IRouteConstraint-Elements muss der Routeneinschränkungstyp bei der ConstraintMap-Eigenschaft der App im Dienstcontainer registriert werden. Eine ConstraintMap ist ein Wörterbuch, das Routeneinschränkungsschlüssel IRouteConstraint-Implementierungen zuordnet, die diese Einschränkungen überprüfen. Die ConstraintMap einer App kann in Program.cs entweder als Teil eines AddRouting-Aufrufs oder durch direktes Konfigurieren von RouteOptions mit builder.Services.Configure<RouteOptions> aktualisiert werden. Zum Beispiel:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

Die vorangehende Einschränkung wird im folgenden Code angewendet:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

Die Implementierung von NoZeroesRouteConstraint verhindert die Verwendung von 0 in einem Routenparameter:

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Der vorangehende Code:

  • Verhindert, dass 0 im {id}-Segment der Route vorhanden ist.
  • Dient als einfaches Beispiel für die Implementierung einer benutzerdefinierten Einschränkung. Es sollte nicht in einer Produktions-App eingesetzt werden.

Der folgende Code bietet einen besseren Ansatz, um zu verhindern, dass eine id mit einer 0 verarbeitet wird:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

Der vorangehende Code bietet im Vergleich zum NoZeroesRouteConstraint-Ansatz folgende Vorteile:

  • Eine benutzerdefinierte Einschränkung ist nicht erforderlich.
  • Es wird ein beschreibender Fehler zurückgegeben, wenn der Routenparameter 0 enthält.

Parametertransformatoren

Parametertransformatoren:

Beispielsweise generiert ein benutzerdefinierter Parametertransformator slugify im Routenmuster blog\{article:slugify} mit Url.Action(new { article = "MyTestArticle" })blog\my-test-article.

Betrachten Sie die folgende Implementierung von IOutboundParameterTransformer:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}

Um einen Parametertransformator in einem Routenmuster zu verwenden, konfigurieren Sie ihn mit ConstraintMap in Program.cs:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

Das ASP.NET Core-Framework verwendet Parametertransformatoren, um den URI zu transformieren, zu dem ein Endpunkt aufgelöst wird. Beispielsweise wandeln Parametertransformatoren die Routenwerte um, die zum Zuordnen folgender Elemente verwendet werden: area, controller, action und page.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Mit der vorstehenden Routenvorlage wird die Aktion SubscriptionManagementController.GetAll dem URI /subscription-management/get-all zugeordnet. Ein Parametertransformator ändert nicht die zum Generieren eines Links verwendeten Routenwerte. Beispielsweise gibt Url.Action("GetAll", "SubscriptionManagement")/subscription-management/get-all aus.

ASP.NET Core bietet API-Konventionen für die Verwendung von Parametertransformatoren mit generierten Routen:

Referenz für URL-Generierung

Dieser Abschnitt enthält eine Referenz für den Algorithmus, der durch die URL-Generierung implementiert wird. In der Praxis werden bei den meisten komplexen Beispielen für die URL-Generierung Controller oder Razor Pages verwendet. Weitere Informationen finden Sie unter Routing in Controllern.

Die URL-Generierung beginnt mit einem Aufruf von LinkGenerator.GetPathByAddress oder einer ähnlichen Methode. Die Methode wird mit einer Adresse, mehreren Routenwerten und optional mit Informationen zur aktuellen Anforderung von HttpContext versehen.

Im ersten Schritt wird die Adresse verwendet, um bestimmte Endpunktkandidaten mithilfe einer IEndpointAddressScheme<TAddress>-Schnittstelle aufzulösen, die dem Adresstyp entspricht.

Sobald eine Kandidatengruppe anhand des Adressschemas gefunden wurde, werden die Endpunkte geordnet und iterativ verarbeitet, bis die URL-Generierung erfolgreich abgeschlossen ist. Bei der URL-Generierung wird nicht auf Mehrdeutigkeiten geprüft, daher ist das erste zurückgegebene Ergebnis das Endergebnis.

Behandeln von Problemen mit der Protokollierung bei der URL-Generierung

Der erste Schritt bei der Behebung von Problemen bei der URL-Generierung ist die Einstellung des Protokolliergrads von Microsoft.AspNetCore.Routing auf TRACE. LinkGenerator protokolliert viele Details über die Verarbeitung, die bei der Problembehebung nützlich sein können.

Ausführliche Informationen zur URL-Generierung finden Sie unter Referenz für URL-Generierung.

Adressen

Mithilfe von Adressen wird bei der URL-Generierung ein Aufruf in der API zur Linkgenerierung an mehrere Endpunktkandidaten gebunden.

Adressen sind ein erweiterbares Konzept, das standardmäßig mit zwei Implementierungen bereitgestellt wird:

  • Die Verwendung von Endpunktname (string) als Adresse:
    • Bietet ähnliche Funktionalität wie der Routenname von MVC.
    • Wird der IEndpointNameMetadata-Metadatentyp verwendet.
    • Löst die bereitgestellte Zeichenfolge anhand der Metadaten aller registrierten Endpunkte auf.
    • Löst beim Start eine Ausnahme aus, wenn mehrere Endpunkte den gleichen Namen aufweisen.
    • Wird für die allgemeine Verwendung außerhalb von Controllern und Razor Pages empfohlen.
  • Die Verwendung von Routenwerten (RouteValuesAddress) als Adresse:
    • Bietet eine ähnliche Funktionalität wie die veraltete Funktion zur URL-Generierung von Controllern und Razor Pages.
    • Lässt sich nur schwer erweitern und debuggen.
    • Bietet die Implementierung, die von IUrlHelper, Taghilfsprogrammen, HTML-Hilfsprogrammen, Aktionsergebnissen usw. verwendet wird.

Aufgabe des Adressschemas ist es, die Verbindung zwischen der Adresse und den übereinstimmenden Endpunkten anhand von beliebigen Kriterien herzustellen:

  • Das Schema für Endpunktenamen führt eine allgemeine Wörterbuchsuche durch.
  • Das Schema der Routenwerte weist eine komplexe beste Teilmenge des Mengenalgorithmus auf.

Umgebungswerte und explizite Werte

Aus der aktuellen Anforderung greift das Routing auf die Routenwerte der aktuellen Anforderung HttpContext.Request.RouteValues zu. Die mit der aktuellen Anforderung verbundenen Werte werden als Umgebungswerte bezeichnet. Aus Gründen der Übersichtlichkeit werden in der Dokumentation die an die Methoden übergebenen Routenwerte als explizite Werte bezeichnet.

Das folgende Beispiel zeigt Umgebungswerte und explizite Werte. Er liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte:

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

Der vorangehende Code:

Der folgende Code liefert nur explizite Werte und keine Umgebungswerte:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

Die vorhergehende Methode gibt /Home/Subscribe/17 zurück.

Der folgende Code in WidgetController gibt /Widget/Subscribe/17 zurück:

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

Der folgende Code stellt den Controller aus den Umgebungswerten in der aktuellen Anforderung und explizite Werte dar:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

Für den Code oben gilt:

  • /Gadget/Edit/17 wird zurückgegeben.
  • Url ruft die IUrlHelper-Schnittstelle ab.
  • Action generiert eine URL mit einem absoluten Pfad für eine Aktionsmethode. Die URL enthält den angegebenen action-Namen und route-Werte.

Sie liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

Im vorangehenden Code wird url auf /Edit/17 festgelegt, wenn die Option zum Bearbeiten der Razor Page die folgende Seitenanweisung enthält:

@page "{id:int}"

Wenn die Routenvorlage "{id:int}" nicht in der Seite „Bearbeiten“ enthalten ist, ist url gleich /Edit?id=17.

Das Verhalten der IUrlHelper-Schnittstelle von MVC fügt zusätzlich zu den hier beschriebenen Regeln eine weitere Komplexitätsebene hinzu:

  • IUrlHelper liefert immer die Routenwerte aus der aktuellen Anforderung als Umgebungswerte.
  • IUrlHelper.action kopiert immer die aktuellen Routenwerte action und controller als explizite Werte, sofern sie nicht vom Entwickler außer Kraft gesetzt werden.
  • IUrlHelper.page kopiert immer den aktuellen Routenwert page als expliziten Wert, sofern er nicht außer Kraft gesetzt wird.
  • IUrlHelper.Page setzt immer den aktuellen Routenwert handler mit null als expliziten Wert außer Kraft, sofern er nicht außer Kraft gesetzt wird.

Benutzer sind oft von den Verhaltensdetails der Umgebungswerte überrascht, da MVC anscheinend nicht den eigenen Regeln folgt. Aus Verlaufs- und Kompatibilitätsgründen weisen bestimmte Routenwerte wie action, controller, page und handler ein spezielles Verhalten auf.

Die äquivalente Funktionalität, die durch LinkGenerator.GetPathByAction und LinkGenerator.GetPathByPage bereitgestellt wird, verdoppelt diese Anomalien von IUrlHelper aus Kompatibilitätsgründen.

URL-Generierungsprozess

Sobald die Gruppe der Endpunktkandidaten ermittelt ist, wird der URL-Generierungsalgorithmus angewendet:

  • Die Endpunkte werden iterativ verarbeitet.
  • Das erste erfolgreiche Ergebnis wird zurückgegeben.

Der erste Schritt in diesem Prozess wird als Routenwertinvalidierung bezeichnet. Die Routenwertinvalidierung ist der Prozess, bei dem das Routing entscheidet, welche Routenwerte aus den Umgebungswerten verwendet und welche ignoriert werden sollen. Jeder Umgebungswert wird berücksichtigt und entweder mit den expliziten Werten kombiniert oder aber ignoriert.

Denken Sie daran, dass Umgebungswerte Anwendungsentwicklern in allgemeinen Fällen das Schreiben von Code sparen können. In der Regel sind die Szenarios, in denen Umgebungswerte hilfreich sind, mit MVC verknüpft:

  • Bei der Verknüpfung mit einer anderen Aktion im gleichen Controller muss der Controllername nicht angegeben werden.
  • Bei der Verknüpfung mit einem anderen Controller im gleichen Bereich muss der Bereich nicht angegeben werden.
  • Bei der Verknüpfung mit der gleichen Aktionsmethode müssen keine Routenwerte angegeben werden.
  • Bei der Verknüpfung mit einem anderen Teil der App sollen keine Routenwerte übertragen werden, die für diesen Teil der App irrelevant sind.

Aufrufe an LinkGenerator oder IUrlHelper, die null zurückgeben, sind meist dadurch bedingt, dass die Routenwertinvalidierung nicht verstanden wurde. Beheben Sie die Routenwertinvalidierung, indem Sie explizit mehr Routenwerte angeben, um zu prüfen, ob das Problem dadurch gelöst wird.

Bei der Routenwertinvalidierung wird davon ausgegangen, dass das URL-Schema der Anwendung hierarchisch ist, mit einer von links nach rechts gebildeten Hierarchie. Sehen Sie sich die einfache Controllerroutenvorlage {controller}/{action}/{id?} an, um ein Gespür dafür zu bekommen, wie dies in der Praxis funktioniert. Durch eine Änderung auf einen Wert werden alle rechts angezeigten Routenwerte ungültig. Dies spricht für die These von der Hierarchie. Wenn die App einen Umgebungswert für id hat und der Vorgang einen anderen Wert für controller angibt:

  • id wird nicht wiederverwendet, weil {controller} links von {id?} steht.

Einige Beispiele veranschaulichen dieses Prinzip:

  • Wenn die expliziten Werte einen Wert für id enthalten, wird der Umgebungswert für id ignoriert. Die Umgebungswerte für controller und action können verwendet werden.
  • Wenn die expliziten Werte einen Wert für action enthalten, wird jeder Umgebungswert für action ignoriert. Die Umgebungswerte für controller können verwendet werden. Wenn sich der explizite Wert für action von dem Umgebungswert für action unterscheidet, wird der Wert id nicht verwendet. Wenn der explizite Wert für action mit dem Umgebungswert für action übereinstimmt, kann der Wert id verwendet werden.
  • Wenn die expliziten Werte einen Wert für controller enthalten, wird jeder Umgebungswert für controller ignoriert. Wenn sich der explizite Wert für controller von dem Umgebungswert für controller unterscheidet, werden die Werte action und id nicht verwendet. Wenn der explizite Wert für controller mit dem Umgebungswert für controller übereinstimmt, können die Werte action und id verwendet werden.

Dieser Prozess wird zusätzlich durch die vorhandenen Attributrouten und dedizierten konventionellen Routen erschwert. Konventionelle Routen des Controllers wie {controller}/{action}/{id?} legen eine Hierarchie mithilfe von Routenparametern fest. Bei bestimmten konventionellen Routen und Attributrouten zu Controllern und Razor Pages:

  • Gibt es eine Hierarchie für Routenwerte.
  • Werden diese nicht in der Vorlage angezeigt.

Für diese Fälle definiert die URL-Generierung das Konzept der erforderlichen Werte. Bei Endpunkten, die von Controllern und Razor Pages erstellt wurden, sind erforderliche Werte angegeben, die eine Routenwertinvalidierung ermöglichen.

Der Algorithmus der Routenwertinvalidierung im Detail:

  • Die erforderlichen Wertnamen werden mit den Routenparametern kombiniert und dann von links nach rechts verarbeitet.
  • Für jeden Parameter werden der Umgebungswert und der explizite Wert verglichen:
    • Wenn der Umgebungswert und der explizite Wert gleich sind, wird der Prozess fortgesetzt.
    • Wenn der Umgebungswert vorhanden ist und der explizite Wert nicht, wird der Umgebungswert bei der URL-Generierung verwendet.
    • Wenn der Umgebungswert nicht vorhanden ist und der explizite Wert vorhanden ist, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.
    • Wenn der Umgebungswert und der explizite Wert vorhanden und die beiden Werte unterschiedlich sind, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.

An diesem Punkt ist der Vorgang zur URL-Generierung bereit, Routeneinschränkungen auszuwerten. Die akzeptierten Werte werden mit den Standardwerten der Parameter kombiniert, die für Einschränkungen bereitgestellt werden. Wenn alle Einschränkungen erfüllt sind, wird der Vorgang fortgesetzt.

Als Nächstes können die akzeptierten Werte verwendet werden, um die Routenvorlage zu erweitern. Die Routenvorlage wird verarbeitet:

  • Von links nach rechts.
  • Für jeden Parameter wird der akzeptierte Wert ersetzt.
  • In den folgenden Sonderfällen:
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter einen Standardwert hat, wird der Standardwert verwendet.
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter optional ist, wird die Verarbeitung fortgesetzt.
    • Wenn irgendein Routenparameter rechts neben einem fehlenden optionalen Parameter einen Wert hat, schlägt der Vorgang fehl.
    • Zusammenhängende Parameter mit Standardwerten und optionale Parameter werden, wenn möglich, reduziert dargestellt.

Explizit bereitgestellte Werte, für die keine Übereinstimmungen mit einem Routensegment ermittelt werden, werden der Abfragezeichenfolge hinzugefügt. In der folgenden Tabelle werden die Ergebnisse dargestellt, die aus der Verwendung der Routenvorlage {controller}/{action}/{id?} hervorgehen:

Umgebungswerte Explizite Werte Ergebnis
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

Reihenfolge der optionalen Routenparameter

Optionale Routenparameter kommen nach allen erforderlichen Routenparametern und Literalen. Im folgenden Code müssen die Parameter id und name nach dem Parameter color kommen:

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}

Probleme mit der Routenwertinvalidierung

Der folgende Code zeigt ein Beispiel für ein Schema zur URL-Generierung, das vom Routing nicht unterstützt wird:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

Im vorangehenden Code wird der culture-Routenparameter für die Lokalisierung verwendet. Ziel ist es, dass der culture-Parameter immer als Umgebungswert akzeptiert wird. Der culture-Parameter wird jedoch aufgrund der Art und Weise, wie die erforderlichen Werte funktionieren, nicht als Umgebungswert akzeptiert:

  • In der "default"-Routenvorlage befindet sich der culture-Routenparameter links von controller, sodass culture durch Änderungen an controller nicht ungültig wird.
  • In der "blog"-Routenvorlage wird der culture-Routenparameter rechts von controller betrachtet, der in den erforderlichen Werten aufgeführt ist.

Zerlegen von URL-Pfaden mit LinkParser

Die LinkParser Klasse fügt die Möglichkeit hinzu, einen URL-Pfads in einen Satz von Routenwerten zu zerlegen. Die ParsePathByEndpointName Methode verwendet einen Endpunktnamen und einen URL-Pfad und gibt einen Satz von aus dem URL-Pfad extrahierten Routenwerten zurück.

Im folgenden Beispielcontroller verwendet die GetProduct Aktion eine Routenvorlage von api/Products/{id} und hat eine Name von GetProduct:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

In derselben Controllerklasse erwartet die AddRelatedProduct Aktion einen URL-Pfad, pathToRelatedProduct, der als Abfragezeichenfolgen-Parameter bereitgestellt werden kann:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

Im vorherigen Beispiel extrahiert die AddRelatedProduct Aktion den id Routenwert aus dem URL-Pfad. Beispielsweise wird der relatedProductId Wert bei einem /api/Products/1 URL-Pfad auf 1 festgelegt. Dieser Ansatz ermöglicht es den Clients der API, URL-Pfade beim Verweisen auf Ressourcen zu verwenden, ohne wissen zu müssen, wie eine solche URL strukturiert ist.

Konfigurieren von Endpunktmetadaten

Die folgenden Links enthalten Informationen zum Konfigurieren von Endpunktmetadaten:

Hostabgleich in Routen mit RequireHost

RequireHost wendet eine Einschränkung auf die Route an, für die der angegebene Host erforderlich ist. Der Parameter RequireHost oder [Host] kann wie folgt lauten:

  • Host: www.domain.com, entspricht www.domain.com mit einem beliebigen Port.
  • Host mit Platzhalter: *.domain.com, entspricht www.domain.com, subdomain.domain.com oder www.subdomain.domain.com an einem beliebigen Port.
  • Port: *:5000, entspricht Port 5000 mit einem beliebigen Host.
  • Host und Port: www.domain.com:5000 oder *.domain.com:5000, entspricht dem Host und Port.

Es können mehrere Parameter mit RequireHost oder [Host] angegeben werden. Die Einschränkung gleicht die Hosts ab, die für einen der Parameter gültig sind. Beispielsweise entspricht [Host("domain.com", "*.domain.com")]domain.com, www.domain.com und subdomain.domain.com.

Im folgenden Code wird RequireHost verwendet, um den angegebenen Host auf der Route anzufordern:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

Im folgenden Code wird das [Host]-Attribut für den Controller verwendet, um die einzelnen angegebenen Hosts anzufordern:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

Wenn das [Host]-Attribut sowohl auf die Controller- als auch auf die Aktionsmethode angewendet wird, trifft Folgendes zu:

  • Das Attribut auf der Aktion wird verwendet.
  • Das Controllerattribut wird ignoriert.

Warnung

Die APIs, die auf dem Hostheader basieren, z. B. HttpRequest.Host und RequireHost, sind potenziellem Spoofing durch Clients ausgesetzt.

Verwenden Sie einen der folgenden Ansätze, um Host- und Portspoofing zu verhindern:

Routengruppen

Die MapGroup-Erweiterungsmethode hilft, Gruppen von Endpunkten mit einem gemeinsamen Präfix zu organisieren. Sie reduziert sich wiederholenden Code und ermöglicht die benutzerdefinierte Anpassung ganzer Gruppen von Endpunkten mit einem einzigen Aufruf von Methoden wie RequireAuthorization und WithMetadata,die Endpunktmetadaten hinzufügen.

Der folgende Code erstellt beispielsweise zwei ähnliche Endpunktgruppen:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

In diesem Szenario können Sie eine relative Adresse für den Location-Header im 201 Created-Ergebnis verwenden:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

Die erste Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /public/todos und ist ohne Authentifizierung zugänglich. Die zweite Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /private/todos und erfordert Authentifizierung.

Die QueryPrivateTodos-Endpunktfilterfactory ist eine lokale Funktion, die die TodoDb-Parameter des Routenhandlers so ändert, dass Zugriff auf private Aufgabendaten zulässig ist und diese gespeichert werden können.

Routengruppen unterstützen auch geschachtelte Gruppen und komplexe Präfixmuster mit Routenparametern und -einschränkungen. Im folgenden Beispiel kann der der user-Gruppe zugeordnete Routenhandler die Routenparameter {org} und {group} erfassen, die in den Präfixen der äußeren Gruppe definiert sind.

Das Präfix kann auch leer sein. Dies kann hilfreich sein, um Endpunktmetadaten oder Filter einer Gruppe von Endpunkten hinzuzufügen, ohne das Routenmuster zu ändern.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

Das Hinzufügen von Filtern oder Metadaten zu einer Gruppe verhält sich genauso wie das individuelle Hinzufügen zu jedem Endpunkt, bevor zusätzliche Filter oder Metadaten hinzugefügt werden, die einer inneren Gruppe oder einem bestimmten Endpunkt hinzugefügt wurden.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

Im Beispiel oben protokolliert der äußere Filter die eingehende Anforderung vor dem inneren Filter, obwohl er als zweiter hinzugefügt wurde. Da die Filter auf verschiedene Gruppen angewendet wurden, spielt die Reihenfolge, in der sie relativ zueinander hinzugefügt wurden, keine Rolle. Die Reihenfolge, in der Filter hinzugefügt werden, spielt eine Rolle, wenn sie auf dieselbe Gruppe oder einen bestimmten Endpunkt angewendet werden.

Eine Anforderung an /outer/inner/ protokolliert Folgendes:

/outer group filter
/inner group filter
MapGet filter

Leistungsleitfaden für das Routing

Wenn eine App Leistungsprobleme hat, wird die Ursache häufig beim Routing vermutet. Das Routing wird deshalb in Betracht gezogen, weil Frameworks wie Controller und Razor Pages in ihren Protokollierungsmeldungen die innerhalb des Frameworks verbrachte Zeit angeben. Wenn es einen signifikanten Unterschied zwischen der von den Controllern gemeldeten Zeit und der Gesamtzeit der Anforderung gibt:

  • Schließen Entwickler ihren App-Code als Ursache des Problems aus.
  • Wird in der Regel angenommen, dass das Routing die Ursache für das Problem ist.

Die Leistung des Routings wird anhand von Tausenden von Endpunkten getestet. Es ist unwahrscheinlich, dass eine typische App auf ein Leistungsproblem stößt, nur weil diese zu umfangreich ist. Die häufigste Ursache für eine langsames Routing ist üblicherweise eine schlecht funktionierende benutzerdefinierte Middleware.

Das folgende Codebeispiel veranschaulicht eine grundlegende Technik zur Eingrenzung der Verzögerungsquelle:

var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");

Auf das Zeitrouting:

  • Verschachteln Sie jede Middleware mit einer Kopie der im vorherigen Code gezeigten Zeitmiddleware.
  • Fügen Sie einen eindeutigen Bezeichner hinzu, um die Zeitdaten mit dem Code zu korrelieren.

Dies ist ein einfacher Weg, um die Verzögerung zu verringern, wenn sie signifikant ist, zum Beispiel größer als 10ms. Wenn Time 2 von Time 1 subtrahiert wird, ergibt sich die in der UseRouting-Middleware benötigte Zeit.

Der folgende Code verwendet einen kompakteren Ansatz als der vorangegangene Zeitcode:

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");

Potenziell teure Routingfeatures

Die folgende Liste gibt einen Einblick in Routingfeatures, die im Vergleich zu einfachen Routenvorlagen relativ teuer sind:

  • Reguläre Ausdrücke: Mit einer kleinen Menge an Eingaben ist möglich, reguläre Ausdrücke zu schreiben, die komplex sind oder eine lange Ausführungszeit haben.
  • Komplexe Segmente ({x}-{y}-{z}):
    • Sind wesentlich teurer als das Analysieren eines regulären URL-Pfadsegments.
    • Führen dazu, dass viel mehr Teilzeichenfolgen zugeordnet werden.
  • Synchroner Datenzugriff: Viele komplexe Apps verfügen im Rahmen Ihrer Routingfunktionen über Datenbankzugriff. Verwenden Sie Erweiterbarkeitspunkte wie MatcherPolicy und EndpointSelectorContext, die asynchron sind.

Leitfaden für große Routingtabellen

ASP.NET Core verwendet standardmäßig einen Routingalgorithmus, bei dem Arbeitsspeicher zugunsten von CPU-Zeit geopfert wird. Dies hat den angenehmen Effekt, dass die Zeit für den Routenabgleich nur von der Länge des abzugleichenden Pfads und nicht von der Routenanzahl abhängt. Dieser Ansatz kann jedoch in einigen Fällen problematisch sein, etwa dann, wenn die App eine große Anzahl von Routen umfasst (mehrere Tausend) und die Routen eine große Anzahl variabler Präfixe enthalten. Beispiel: Die Routen weisen Parameter in frühen Segmenten der Route auf, etwa {parameter}/some/literal.

Es ist unwahrscheinlich, dass eine App in eine Situation gerät, in der dies ein Problem darstellt, es sei denn:

  • Es ist eine große Anzahl von Routen in der App vorhanden, die dieses Muster verwenden.
  • Es ist eine große Anzahl von Routen in der App vorhanden.

Wie lässt sich feststellen, ob eine Anwendung mit dem Problem einer großer Routentabelle zu kämpfen hat?

  • Es gibt zwei Symptome, auf die Sie achten sollten:
    • Die App wird bei der ersten Anforderung langsam gestartet.
      • Dieses Symptom ist zwingend, reicht aber allein nicht aus. Es gibt viele andere, nicht routenbezogene Probleme, die zu einem langsamen App-Start führen können. Überprüfen Sie auch auf die folgende Bedingung, um genau zu bestimmen, ob diese Situation für die App vorliegt.
    • Die Anwendung verbraucht beim Start viel Speicher, und ein Speicherabbild zeigt eine große Anzahl von Microsoft.AspNetCore.Routing.Matching.DfaNode-Instanzen.

Beheben dieses Problems

Es gibt verschiedene Techniken und Optimierungen, die auf Routen angewendet werden können, um dieses Szenario weitgehend zu vermeiden:

  • Wenden Sie nach Möglichkeit Routeneinschränkungen auf Ihre Parameter an, z. B. {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}.
    • Dadurch kann der Routingalgorithmus die für den Abgleich verwendeten Strukturen intern optimieren und den verwendeten Arbeitsspeicher drastisch reduzieren.
    • In den meisten Fällen reicht dies aus, um zu einem akzeptablen Verhalten zurückzukehren.
  • Ändern Sie die Routen, um Parameter in spätere Segmente in der Vorlage zu verlagern.
    • Dadurch wird die Anzahl von möglichen „Pfaden“ reduziert, die bei Angabe eines Pfads mit einem Endpunkt übereinstimmen.
  • Verwenden Sie eine dynamische Route, und führen Sie die Zuordnung zu einem Controller/einer Seite dynamisch durch.
    • Dies kann mithilfe von MapDynamicControllerRoute und MapDynamicPageRoute erreicht werden.

Kurzschluss der Middleware nach dem Routing

Wenn das Routing mit einem Endpunkt übereinstimmt, lässt es in der Regel den rest der Middlewarepipeline ausführen, bevor die Endpunktlogik aufgerufen wird. Dienste können die Ressourcennutzung reduzieren, indem sie bekannte Anforderungen frühzeitig in der Pipeline herausfiltern. Verwenden Sie die ShortCircuit-Erweiterungsmethode, um das Routing zu veranlassen, die Endpunktlogik sofort aufzurufen und dann die Anforderung zu beenden. Beispielsweise muss eine bestimmte Route möglicherweise keine Authentifizierung oder CORS-Middleware durchlaufen. Im folgenden Beispiel werden Kurzschlüsse angefordert, die mit der /short-circuit-Route übereinstimmen:

app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();

Die ShortCircuit(IEndpointConventionBuilder, Nullable<Int32>)-Methode kann optional einen Statuscode verwenden.

Verwenden Sie die MapShortCircuit-Methode, um Kurzschlüsse für mehrere Routen gleichzeitig einzurichten, indem Sie ihr ein Parameterarray von URL-Präfixen übergeben. Beispielsweise testen Browser und Bots häufig Server auf bekannte Pfade wie robots.txt und favicon.ico. Wenn die App nicht über diese Dateien verfügt, kann eine Codezeile beide Routen konfigurieren:

app.MapShortCircuit(404, "robots.txt", "favicon.ico");

MapShortCircuit gibt IEndpointConventionBuilder zurück, sodass zusätzliche Routeneinschränkungen wie die Hostfilterung hinzugefügt werden können.

Die Methoden ShortCircuit und MapShortCircuit wirken sich nicht auf Middleware aus, die vor UseRouting platziert wird. Wenn Sie versuchen, diese Methoden mit Endpunkten zu verwenden, die ebenfalls über [Authorize]- oder [RequireCors]-Metadaten verfügen, führen Anforderungen mit einer InvalidOperationException zu einem Fehler. Diese Metadaten werden durch [Authorize]- oder [EnableCors]-Attribute oder RequireCors- oder RequireAuthorization-Methoden angewendet.

Um die Auswirkung des Middleware-Kurzschlusses anzuzeigen, legen Sie die Protokollierungskategorie „Microsoft“ in appsettings.Development.json auf „Information“ fest:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Führen Sie den folgenden Code aus:

var app = WebApplication.Create();

app.UseHttpLogging();

app.MapGet("/", () => "No short-circuiting!");
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
app.MapShortCircuit(404, "robots.txt", "favicon.ico");

app.Run();

Das folgende Beispiel stammt aus den Konsolenprotokollen, die beim Ausführen des /-Endpunkts erstellt werden. Es enthält die Ausgabe der Middleware für die Protokollierung:

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
      Date: Wed, 03 May 2023 21:05:59 GMT
      Server: Kestrel
      Alt-Svc: h3=":5182"; ma=86400
      Transfer-Encoding: chunked

Das folgende Beispiel wird vom /short-circuit-Endpunkt aus ausgeführt. Es enthält nichts von der Middleware für die Protokollierung, da die Middleware kurzgeschlossen wurde:

info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[4]
      The endpoint 'HTTP: GET /short-circuit' is being executed without running additional middleware.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[5]
      The endpoint 'HTTP: GET /short-circuit' has been executed without running additional middleware.

Leitfaden für Bibliotheksautoren

Dieser Abschnitt enthält Hinweise für Bibliotheksautoren, die auf dem Routing aufbauen. Diese Details sollen sicherstellen, dass App-Entwickler gute Erfahrungen mit Bibliotheken und Frameworks machen, die das Routing erweitern.

Definieren von Endpunkten

Wenn Sie ein Framework erstellen möchten, das das Routing für den URL-Abgleich verwendet, sollten Sie zunächst eine Benutzeroberfläche definieren, die auf UseEndpoints aufbaut.

Es wird empfohlen, dass Sie auf IEndpointRouteBuilder aufbauen. Auf diese Weise können Benutzer Ihr Framework mit anderen ASP.NET Core-Features problemlos zusammenstellen. Jede ASP.NET Core-Vorlage umfasst die Routingfunktionalität. Gehen Sie davon aus, dass das Routing vorhanden und den Benutzern vertraut ist.

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

Es wird empfohlen, dass Sie einen versiegelten konkreten Typ aus einem Aufruf an MapMyFramework(...) zurückgeben, der IEndpointConventionBuilder implementiert. Die meisten Map...-Methoden in Frameworks folgen diesem Muster. Die IEndpointConventionBuilder-Schnittstelle:

  • Ermöglicht das Zusammensetzen von Metadaten.
  • Wird von verschiedenen Erweiterungsmethoden angesteuert.

Wenn Sie Ihren eigenen Typ deklarieren, können Sie dem Generator Ihre eigene frameworkspezifische Funktionalität hinzufügen. Es ist in Ordnung, einen vom Framework deklarierten Generator zu umschließen und Aufrufe an ihn weiterzuleiten.

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

Ziehen Sie in Betracht, Ihre eigene EndpointDataSource-Klasse zu schreiben. Die EndpointDataSource-Klasse vom primitiven Typ auf niedriger Ebene eignet sich zum Deklarieren und Aktualisieren einer Sammlung von Endpunkten. EndpointDataSource ist eine leistungsstarke API, die von Controllern und Razor Pages verwendet wird. Weitere Informationen finden Sie unter Dynamisches Endpunktrouting.

Die Routingtests haben ein grundlegendes Beispiel für eine Datenquelle, die nicht aktualisiert wird.

ERWÄGEN Sie die Implementierung von GetGroupedEndpoints. Dadurch erhalten Sie vollständige Kontrolle über ausgeführte Gruppenkonventionen und die endgültigen Metadaten für die gruppierten Endpunkte. Dies ermöglicht z. B. benutzerdefinierten EndpointDataSource Implementierungen das Ausführen von Endpunktfiltern, die Gruppen hinzugefügt werden.

Versuchen Sie nicht, eine EndpointDataSource-Klasse standardmäßig zu registrieren. Fordern Sie die Benutzer auf, Ihr Framework in UseEndpoints zu registrieren. Der Grundgedanke beim Routing ist, dass standardmäßig nichts enthalten ist, und dass die Endpunkte bei UseEndpoints registriert werden müssen.

Erstellen von in das Routing integrierter Middleware

Ziehen Sie in Betracht, Metadatentypen als Schnittstelle zu definieren.

Ermöglichen Sie, Metadatentypen als Attribut für Klassen und Methoden zu verwenden.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Frameworks wie Controller und Razor Pages unterstützen die Anwendung von Metadatenattributen auf Typen und Methoden. Für das Deklarieren von Metadatentypen gilt:

  • Machen Sie diese als Attribute zugänglich.
  • Die meisten Benutzer sind mit der Anwendung von Attributen vertraut.

Durch die Deklaration eines Metadatentyps als Schnittstelle wird die Flexibilität zusätzlich erhöht:

  • Schnittstelle können zusammengesetzt werden.
  • Entwickler können ihre eigenen Typen deklarieren, die mehrere Richtlinien kombinieren.

Ermöglichen Sie, Metadaten außer Kraft zu setzen, wie in folgendem Beispiel gezeigt:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

Die beste Möglichkeit, diese Richtlinien zu befolgen, besteht darin, die Definition von Markermetadaten zu vermeiden:

  • Suchen Sie nicht nur nach dem Vorhandensein eines Metadatentyps.
  • Definieren Sie eine Eigenschaft für die Metadaten, und überprüfen Sie die Eigenschaft.

Die Metadatensammlung ist geordnet und unterstützt das Außerkraftsetzen nach Priorität. Im Fall von Controllern sind Metadaten für die Aktionsmethode am spezifischsten.

Nutzen Sie Middleware mit und ohne Routing.

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

Ein gutes Beispiel für diese Vorgabe ist die UseAuthorization-Middleware. Mithilfe der Autorisierungsmiddleware können Sie eine Fallbackrichtlinie einbauen. Die Fallbackrichtlinie gilt, falls angegeben, für beide:

  • Endpunkte ohne angegebene Richtlinie.
  • Anforderungen, die nicht mit einem Endpunkt übereinstimmen.

Dies macht die Autorisierungsmiddleware außerhalb des Routingkontexts sehr nützlich. Die Autorisierungsmiddleware kann für die traditionelle Middlewareprogrammierung verwendet werden.

Debugdiagnose

Legen Sie für eine ausführliche Routingdiagnoseausgabe Logging:LogLevel:Microsoft auf Debug fest. Legen Sie in der Entwicklungsumgebung die Protokollebene in appsettings.Development.json fest:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Zusätzliche Ressourcen

Das Routing wird für das Abgleichen von HTTP-Anforderungen und das Verteilen an ausführbare Endpunkte der App eingesetzt. Endpunkte sind die Einheiten des ausführbaren Codes für die Anforderungsverarbeitung in der App. Endpunkte werden in der App definiert und beim Start der App konfiguriert. Beim Endpunktabgleich können Werte aus der Anforderungs-URL extrahiert und für die Verarbeitung der Anforderung bereitgestellt werden. Mithilfe von Endpunktinformationen aus der App lassen sich durch das Routing URLs generieren, die Endpunkten zugeordnet werden.

Apps können das Routing mit folgenden Funktionen konfigurieren:

In diesem Artikel werden die grundlegenden Details zum ASP.NET Core-Routing beschrieben. Informationen zur Routingkonfiguration finden Sie wie folgt:

Routinggrundlagen

Der folgende Code veranschaulicht ein einfaches Beispiel für das Routing:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Das vorherige Beispiel enthält einen einzelnen Endpunkt unter Verwendung der MapGet-Methode:

  • Wenn eine GET-HTTP-Anforderung an die Stamm-URL /gesendet wird:
    • Der Anforderungsdelegat wird ausgeführt.
    • Hello World! wird in die HTTP-Antwort geschrieben.
  • Wenn die Anforderungsmethode nicht GET bzw. die Stamm-URL nicht / ist, gibt es keinen Routenabgleich und es wird ein HTTP-404-Fehler zurückgegeben.

Beim Routing wird ein Middlewarepaar verwendet, das durch UseRouting und UseEndpoints registriert wird:

  • UseRouting fügt der Middlewarepipeline einen Routenabgleich hinzu. Diese Middleware prüft die in der App definierten Endpunkte und wählt anhand der Anforderung die beste Übereinstimmung aus.
  • UseEndpoints fügt der Middlewarepipeline die Endpunktausführung hinzu. Dabei wird der mit dem ausgewählten Endpunkt verknüpfte Delegat ausgeführt.

Apps müssen UseRouting oder UseEndpoints normalerweise nicht aufrufen. WebApplicationBuilder konfiguriert eine Middlewarepipeline, die die in Program.cs hinzugefügte Middleware mit UseRouting und UseEndpoints umschließt. Apps können jedoch die Reihenfolge ändern, in der UseRouting und UseEndpoints ausgeführt werden, indem sie diese Methoden explizit aufrufen. Der folgende Code ruft beispielsweise UseRouting explizit auf:

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

Für den Code oben gilt:

  • Mit dem Aufruf von app.Use wird eine benutzerdefinierte Middleware registriert, die am Anfang der Pipeline ausgeführt wird.
  • Der Aufruf von UseRouting konfiguriert die Middleware für den Routenabgleich zur Ausführung nach der benutzerdefinierten Middleware.
  • Der mit MapGet registrierte Endpunkt wird am Ende der Pipeline ausgeführt.

Wenn das vorhergehende Beispiel keinen Aufruf an UseRouting enthält, wird die benutzerdefinierte Middleware nach der Middleware zum Routenabgleich ausgeführt.

Endpunkte

Die MapGet-Methode wird verwendet, um einen Endpunkt zu definieren. Ein Endpunkt kann Folgendes sein:

  • Ausgewählt, indem die URL und die HTTP-Methode abgeglichen werden.
  • Ausgeführt, indem ein Delegat ausgeführt wird.

Endpunkte, die von der App zugeordnet und ausgeführt werden können, sind in UseEndpoints konfiguriert. Mit MapGet, MapPost und ähnlichen Methoden werden beispielsweise Anforderungsdelegate mit dem Routingsystem verbunden. Zudem können weitere Methoden zur Verbindung von ASP.NET Core-Frameworkfunktionen mit dem Routingsystem verwendet werden:

Das folgende Beispiel zeigt das Routing mit einer anspruchsvolleren Routenvorlage:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

Die Zeichenfolge /hello/{name:alpha} ist eine Routenvorlage. Eine Routenvorlage, um zu konfigurieren, wie der Endpunkt abgeglichen wird. In diesem Fall gleicht die Vorlage Folgendes ab:

  • Eine URL wie /hello/Docs
  • Alle URL-Pfade, die mit /hello/ beginnen, gefolgt von einer Sequenz alphabetischer Zeichen. :alpha wendet eine Routeneinschränkung an, die nur alphabetische Zeichen abgleicht. Routeneinschränkungen werden weiter unten in diesem Artikel erläutert.

Das zweite Segment des URL-Pfads, {name:alpha}:

Das folgende Beispiel zeigt das Routing mit Integritätsprüfungen und Autorisierung:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

Im vorherigen Beispiel wird veranschaulicht, wie Sie:

  • Die Autorisierungsmiddleware für das Routing verwenden.
  • Endpunkte zum Konfigurieren des Autorisierungsverhaltens verwendet werden können.

Der MapHealthChecks-Aufruf fügt einen Endpunkt für eine Integritätsprüfung hinzu. Durch die Verkettung von RequireAuthorization mit diesem Aufruf wird eine Autorisierungsrichtlinie an den Endpunkt angefügt.

Der Aufruf von UseAuthentication und UseAuthorization wird die Authentifizierungs- und Autorisierungsmiddleware hinzugefügt. Diese Middleware wird zum Ausführen folgender Aktionen zwischen UseRouting und UseEndpoints platziert:

  • Anzeigen des von UseRouting ausgewählten Endpunkts.
  • Anwenden einer Autorisierungsrichtlinie vor dem Senden von UseEndpoints an den Endpunkt.

Endpunktmetadaten

Im vorangehenden Beispiel gibt es zwei Endpunkte, aber nur dem für die Integritätsprüfung ist eine Autorisierungsrichtlinie angefügt. Wenn die Anforderung mit dem Endpunkt der Integritätsprüfung, /healthz, übereinstimmt, wird eine Autorisierungsprüfung durchgeführt. Dadurch wird veranschaulicht, dass Endpunkten zusätzliche Daten zugeordnet werden können. Diese zusätzlichen Daten werden als Metadaten des Endpunkts bezeichnet:

  • Die Metadaten lassen sich von routingfähiger Middleware verarbeiten.
  • Die Metadaten können einen beliebigen .NET-Typ aufweisen.

Routingkonzepte

Durch Hinzufügen des effizienten Endpunkt-Konzepts stellt das Routingsystem eine Ergänzung der Middlewarepipeline dar. Endpunkte stehen für Funktionseinheiten der App, die sich in Bezug auf Routing, Autorisierung und die Anzahl der ASP.NET Core-Systeme voneinander unterscheiden.

ASP.NET Core-Endpunktdefinition

Ein ASP.NET Core-Endpunkt ist:

Der folgende Code zeigt, wie der Endpunkt, der mit der aktuellen Anforderung übereinstimmt, abgerufen und geprüft werden kann:

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

Der Endpunkt, falls ausgewählt, kann aus dem HttpContext-Element abgerufen werden. Seine Eigenschaften können geprüft werden. Endpunktobjekte sind unveränderlich und können nach der Erstellung nicht mehr geändert werden. Der häufigste Typ des Endpunkts ist eine RouteEndpoint-Klasse. RouteEndpoint enthält Informationen, die eine Auswahl durch das Routingsystem ermöglichen.

Im vorangehenden Code wird mit app.Use eine Middleware inline konfiguriert.

Der folgende Code zeigt, dass es, je nachdem, wo app.Use in der Pipeline aufgerufen wird, möglicherweise keinen Endpunkt gibt:

// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

Im vorangehenden Beispiel werden Console.WriteLine-Anweisungen hinzugefügt, die anzeigen, ob ein Endpunkt ausgewählt wurde oder nicht. Aus Gründen der Übersichtlichkeit wird in dem Beispiel dem bereitgestellten /-Endpunkt ein Anzeigename zugewiesen.

Das vorherige Beispiel enthält auch Aufrufe an UseRouting und UseEndpoints, um genau zu steuern, wann diese Middleware in der Pipeline ausgeführt wird.

Wenn Sie diesen Code mit einer URL / ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Wenn Sie diesen Code mit einer anderen URL ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Diese Ausgabe zeigt Folgendes:

  • Der Endpunkt ist immer NULL, bevor UseRouting aufgerufen wird.
  • Wenn eine Übereinstimmung gefunden wird, ist der Endpunkt zwischen UseRouting und UseEndpoints ungleich NULL.
  • Die UseEndpoints-Middleware ist eine Terminalmiddleware, wenn eine Übereinstimmung gefunden wird. Terminalmiddleware wird weiter unten in diesem Artikel definiert.
  • Die Middleware nach UseEndpoints wird nur ausgeführt, wenn keine Übereinstimmung gefunden wird.

Die UseRouting-Middleware verwendet die SetEndpoint-Methode, um den Endpunkt an den aktuellen Kontext anzufügen. Es ist möglich, die UseRouting-Middleware durch benutzerdefinierte Logik zu ersetzen und dennoch die Vorteile durch die Verwendung von Endpunkten zu nutzen. Endpunkte befinden sich auf niedriger Ebene, wie Middleware, und sind nicht an die Routingimplementierung gekoppelt. Die meisten Apps müssen UseRouting nicht durch eigene Logik ersetzen.

Die UseEndpoints-Middleware ist so konzipiert, dass Sie zusammen mit der UseRouting-Middleware verwendet werden kann. Die Hauptlogik zum Ausführen eines Endpunkts ist nicht kompliziert. Mit GetEndpoint können Sie einen Endpunkt abrufen und dann dessen RequestDelegate-Eigenschaft aufrufen.

Der folgende Code veranschaulicht, wie Middleware das Routing beeinflussen oder darauf reagieren kann:

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

Im vorherigen Beispiel werden zwei wichtige Konzepte dargestellt:

  • Die Middleware kann vor UseRouting ausgeführt werden, um die Daten zu ändern, auf denen das Routing basiert.
  • Die Middleware kann zwischen UseRouting und UseEndpoints ausgeführt werden, um die Ergebnisse des Routings vor der Ausführung des Endpunkts zu verarbeiten.
    • Middleware, die zwischen UseRouting und UseEndpoints ausgeführt wird:
      • Überprüft in der Regel die Metadaten, um die Endpunkte zu ermitteln.
      • Trifft häufig Sicherheitsentscheidungen, wie UseAuthorization und UseCors.
    • Durch die Kombination aus Middleware und Metadaten ist es möglich, für jeden einzelnen Endpunkt Richtlinien zu konfigurieren.

Der vorangehende Code zeigt ein Beispiel für eine benutzerdefinierte Middleware, die entpunktbezogene Richtlinien unterstützt. Die Middleware schreibt ein Überwachungsprotokoll für den Zugriff auf vertrauliche Daten in der Konsole. Die Middleware kann so konfiguriert werden, dass ein Endpunkt mit den RequiresAuditAttribute-Metadaten überwacht wird. In diesem Beispiel wird ein Opt-In-Muster veranschaulicht, bei dem nur Endpunkte überwacht werden, die als vertraulich markiert sind. Es ist möglich, diese Logik umgekehrt zu definieren, indem beispielsweise alles geprüft wird, was nicht als sicher markiert ist. Das Endpunktmetadaten-System ist flexibel. Diese Logik lässt sich für jeden Anwendungsfall passend schreiben.

Der vorherige Beispielcode soll die grundlegenden Konzepte von Endpunkten veranschaulichen. Das Beispiel ist nicht für Produktionsumgebungen vorgesehen. Eine vollständigere Version einer Middleware für Überwachungsprotokolle würde Folgendes bieten:

  • Protokollieren in einer Datei oder einer Datenbank.
  • Einschließen von Details wie Benutzer, IP-Adresse, Name des vertraulichen Endpunkts usw.

Die Metadaten der Überwachungsrichtlinie RequiresAuditAttribute sind als Attribute definiert, um die Verwendung mit klassenbasierten Frameworks wie Controllern und SignalR zu erleichtern. Bei Verwendung von Route-zu-Code:

  • Metadaten werden an eine Generator-API angefügt.
  • Klassenbasierte Frameworks enthalten beim Erstellen von Endpunkten alle Attribute der entsprechenden Methode und Klasse.

Die bewährten Methoden für Metadatentypen sind, sie entweder als Schnittstellen oder als Attribute zu definieren. Schnittstellen und Attribute ermöglichen die Wiederverwendung von Code. Das Metadatensystem ist flexibel und weist keine Einschränkungen auf.

Vergleichen von Terminalmiddleware mit Routing

Im folgenden Beispiel werden sowohl Terminalmiddleware als auch Routing veranschaulicht:

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Beim in Approach 1: gezeigten Stil von Middleware handelt es sich um Terminalmiddleware. Sie wird als Terminalmiddleware bezeichnet, da sie einen Abgleich durchgeführt.

  • Der Abgleich im vorangehenden Beispiel ist Path == "/" für die Middleware und Path == "/Routing" für das Routing.
  • Bei einem erfolgreichen Abgleich führt sie bestimmte Funktionen aus und kehrt zurück, anstatt die next-Middleware aufzurufen.

Sie wird „Terminal Middleware“ genannt, da sie die Suche beendet, einige Funktionen ausführt und dann zurückkehrt.

In der folgenden Liste werden Terminalmiddleware und Routing verglichen:

  • Beide Ansätze ermöglichen das Beenden der Verarbeitungspipeline:
    • Die Middleware beendet die Pipeline, indem Sie zurückkehrt, anstatt next aufzurufen.
    • Endpunkte beenden immer die Verarbeitung.
  • Terminalmiddleware ermöglicht die Positionierung der Middleware an einer beliebigen Stelle in der Pipeline:
    • Endpunkte werden an der Position von UseEndpoints ausgeführt.
  • Mit Terminalmiddleware kann beliebiger Code beendet werden, wenn die Middleware übereinstimmt:
    • Ein benutzerdefinierter Routenabgleichscode kann sehr umständlich und schwierig zu schreiben sein.
    • Das Routing bietet unkomplizierte Lösungen für typische Apps. Die meisten Apps erfordern keinen benutzerdefinierten Routenabgleichscode.
  • Endpunkte haben eine Schnittstelle mit Middleware wie UseAuthorization und UseCors.
    • Die Verwendung einer Terminalmiddleware mit UseAuthorization oder UseCors erfordert eine manuelle Verknüpfung mit dem Autorisierungssystem.

Ein Endpunkt definiert beides:

  • Einen Delegaten zum Verarbeiten von Anforderungen.
  • Eine Sammlung beliebiger Metadaten. Die Metadaten werden zur Implementierung von übergreifenden Belangen verwendet, die auf Richtlinien und der Konfiguration basieren, die den einzelnen Endpunkten angefügt sind.

Terminalmiddleware kann sehr nützlich sein, erfordert aber möglicherweise auch:

  • Einen großen Codierungs- und Testaufwand.
  • Eine manuelle Integration in andere Systeme für die gewünschte Flexibilität.

Ziehen Sie daher zunächst die Integration von Routingfunktionen in Betracht, bevor Sie damit beginnen, Terminalmiddleware zu schreiben.

Vorhandene Terminalmiddleware, die in Map oder MapWhen integriert ist, kann in der Regel in einen routingfähigen Endpunkt umgewandelt werden. MapHealthChecks zeigt das Muster für eine Routinglösung:

  • Schreiben Sie eine Erweiterungsmethode in der IEndpointRouteBuilder-Schnittstelle.
  • Erstellen Sie eine geschachtelte Middlewarepipeline mit CreateApplicationBuilder.
  • Fügen Sie die Middleware an die neue Pipeline an. In diesem Fall UseHealthChecks.
  • Verwenden Sie Build, um die Middlewarepipeline in einem RequestDelegate-Delegaten zu erstellen.
  • Rufen Sie Map auf, und stellen Sie die neue Middlewarepipeline bereit.
  • Geben Sie das Generatorobjekt zurück, das von Map aus der Erweiterungsmethode bereitgestellt wurde.

Im folgenden Code ist die Verwendung von MapHealthChecks gezeigt:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

Das vorangehende Beispiel zeigt, warum das Zurückgeben des Generatorobjekts wichtig ist. Wenn das Generatorobjekt zurückgegeben wird, kann der App-Entwickler Richtlinien konfigurieren, z. B. die Autorisierung für den Endpunkt. In diesem Beispiel ist die Middleware für Integritätsprüfungen nicht direkt in das Autorisierungssystem integriert.

Das Metadatensystem wurde als Antwort auf die Probleme erstellt, die von Erweiterbarkeitsautoren mithilfe von Terminalmiddleware aufgetreten sind. Für jede Middleware ist es problematisch, deren eigene Integration in das Autorisierungssystem umzusetzen.

URL-Zuordnung

  • Bei diesem Prozess werden eingehende Anforderungen durch Routing mit einem Endpunkt abgeglichen.
  • Basiert auf Daten im URL-Pfad und den URL-Headern.
  • Kann erweitert werden, um beliebige Daten in der Anforderung zu überprüfen.

Wenn eine Routingmiddleware ausgeführt wird, legt sie ein Endpoint-Element fest und leitet Werte an eine Anforderungsfunktion in der HttpContext-Klasse der aktuellen Anforderung weiter:

  • Durch Aufrufen von HttpContext.GetEndPoint wird der Endpunkt abgerufen.
  • HttpRequest.RouteValues ruft die Sammlung der Routenwerte ab.

Middleware wird nach der Routingmiddleware ausgeführt und kann den Endpunkt erkennen und Maßnahmen ergreifen. So kann beispielsweise eine Autorisierungsmiddleware die Erfassung der Metadaten des Endpunkts für eine Autorisierungsrichtlinie abfragen. Nachdem die gesamte Middleware in der Anforderungsverarbeitungspipeline ausgeführt wurde, wird der Delegat des ausgewählten Endpunkts aufgerufen.

Das Routingsystem ist beim Endpunktrouting für alle Weiterleitungsentscheidungen zuständig. Da die Middleware Richtlinien auf der Grundlage des ausgewählten Endpunkts anwendet, ist Folgendes wichtig:

  • Alle Entscheidungen, die sich auf die Verteilung oder die Anwendung von Sicherheitsrichtlinien auswirken können, werden im Routingsystem getroffen.

Warnung

Wenn für Abwärtskompatibilität ein Controller- oder Razor Pages-Endpunktdelegat ausgeführt wird, werden die Eigenschaften von RouteContext.RouteData auf Grundlage der bisher verarbeiteten Anforderungen auf entsprechende Werte festgelegt.

Der Typ RouteContext wird in einer zukünftigen Version als veraltet markiert:

  • Migrieren Sie RouteData.Values zu HttpRequest.RouteValues.
  • Migrieren Sie RouteData.DataTokens, um IDataTokensMetadata aus den Endpunktmetadaten abzurufen.

Der URL-Abgleich erfolgt in mehreren Phasen und kann konfiguriert werden. In jeder Phase werden mehrere Übereinstimmungen ausgegeben. Diese Übereinstimmungen lassen sich in der nächsten Phase weiter eingrenzen. Die Routingimplementierung garantiert keine Verarbeitungsreihenfolge für übereinstimmende Endpunkte. Alle möglichen Übereinstimmungen werden gleichzeitig verarbeitet. Für die URL-Abgleichsphasen gilt folgende Reihenfolge. ASP.NET Core:

  1. Verarbeitet den URL-Pfad mit mehreren Endpunkten und ihren Routenvorlagen und sammelt alle Übereinstimmungen.
  2. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den angewendeten Routeneinschränkungen passen.
  3. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den MatcherPolicy-Instanzen passen.
  4. Verwendet EndpointSelector, um eine abschließende Entscheidung anhand der vorangehenden Liste zu treffen.

Die Liste der Endpunkte wird entsprechend den folgenden Punkten priorisiert:

Alle übereinstimmenden Endpunkte werden in jeder Phase verarbeitet, bis EndpointSelector erreicht ist. EndpointSelector stellt die abschließende Phase dar. Darin wird der Endpunkt mit der höchsten Priorität aus den Übereinstimmungen als beste Übereinstimmung ausgewählt. Wenn es andere Übereinstimmungen mit derselben Priorität wie die beste Übereinstimmung gibt, wird ein Ausnahmefehler wegen einer nicht eindeutigen Übereinstimmung ausgelöst.

Die Routenpriorität wird anhand einer spezifischeren Routenvorlage berechnet, der eine höhere Priorität eingeräumt wird. Dies wird z. B. anhand der Vorlagen /hello und /{message} deutlich:

  • Beide stimmen mit dem URL-Pfad /hello überein.
  • /hello ist spezifischer und hat daher höhere Priorität.

Im Allgemeinen eignet sich die Routenpriorität gut, um die beste Übereinstimmung für die in der Praxis verwendeten URL-Schemata zu finden. Verwenden Sie Order nur bei Bedarf, um eine Mehrdeutigkeit zu vermeiden.

Aufgrund der Erweiterungsmöglichkeiten, die das Routing bietet, kann das Routingsystem die mehrdeutigen Routen nicht im Voraus berechnen. Betrachten Sie ein Beispiel wie die Routenvorlagen /{message:alpha} und /{message:int}:

  • Die alpha-Einschränkung gleicht nur alphabetische Zeichen ab.
  • Die int-Einschränkung gleicht nur Zahlen ab.
  • Diese Vorlagen haben die gleiche Routenpriorität, aber es gibt keine einzige URL, auf die sie beide zutreffen.
  • Wenn das Routingsystem beim Starten einen Mehrdeutigkeitsfehler gemeldet hat, würde dieser den gültigen Anwendungsfall blockieren.

Warnung

Die Reihenfolge der Vorgänge in UseEndpoints wirkt sich nicht auf das Routingverhalten aus, mit einer Ausnahme. MapControllerRoute und MapAreaRoute weisen ihren Endpunkten automatisch einen Reihenfolgenwert zu, basierend auf der Reihenfolge, in der sie aufgerufen werden. Dadurch wird das Langzeitverhalten von Controllern simuliert, ohne dass das Routingsystem die gleichen Garantien bietet wie ältere Routingimplementierungen.

Endpunktrouting in ASP.NET Core:

  • Weist nicht das Konzept von Routen auf.
  • Bietet keine garantierte Reihenfolge. Alle Endpunkte werden gleichzeitig verarbeitet.

Routenvorlagenpriorität und Reihenfolge der Endpunktauswahl

Die Routenvorlagenpriorität ist ein System, bei dem jeder Routenvorlage ein Wert zugewiesen wird, je nachdem, wie spezifisch diese ist. Routenvorlagenpriorität:

  • Verhindert, dass die Reihenfolge der Endpunkte häufig angepasst werden muss.
  • Versucht, die allgemeinen Erwartungen an das Routingverhalten abzugleichen.

Dies wird z. B. anhand der Vorlagen /Products/List und /Products/{id} deutlich. Es wäre begründet, anzunehmen, dass /Products/List eine bessere Übereinstimmung als /Products/{id} für den URL-Pfad /Products/List ist. Dies funktioniert, weil das Literalsegment /List eine höhere Priorität als das Parametersegment /{id} hat.

Wie die Priorisierung im Einzelnen funktioniert, ist an die Definition der Routenvorlagen gekoppelt:

  • Vorlagen mit mehr Segmenten sind in der Regel spezifischer.
  • Ein Segment mit Literaltext gilt als spezifischer als ein Parametersegment.
  • Ein Parametersegment mit einer Einschränkung gilt als spezifischer als ein Parametersegment ohne Einschränkung.
  • Ein komplexes Segment wird als genauso spezifisch betrachtet wie ein Parametersegment mit einer Einschränkung.
  • Catch-All-Parameter, die am wenigsten spezifisch sind. Unter catch-all im Abschnitt Routenvorlagen finden Sie wichtige Informationen zu catch-all-Routen.

Konzepte zur URL-Generierung

URL-Generierung:

  • Der Prozess, bei dem durch Routing ein URL-Pfad basierend auf mehreren Routenwerten erstellt wird.
  • Sie ermöglicht eine logische Trennung zwischen den Endpunkten und den URLs, die auf diese zugreifen.

Das Endpunktrouting umfasst die API zur Linkgenerierung (LinkGenerator). LinkGenerator ist ein Singleton-Dienst, der in DI verfügbar ist. Die LinkGenerator-API kann außerhalb des Kontexts einer ausgeführten Anforderung verwendet werden. Mvc.IUrlHelper und Szenarios, die von IUrlHelper abhängig sind (z. B. Taghilfsprogramme, HTML-Hilfsprogramme und Aktionsergebnisse), verwenden die LinkGenerator-API, um entsprechende Funktionen bereitzustellen.

Die API zur Linkgenerierung wird von Konzepten wie Adressen und Adressschemas unterstützt. Sie können mithilfe eines Adressschemas die Endpunkte bestimmen, die bei der Linkgenerierung berücksichtigt werden sollen. Beispielsweise werden Routennamen und Routenwerte als Adressschemas implementiert. Diese Szenarios kennen viele Benutzer von Controllern und Razor Pages.

Die API zur Linkgenerierung kann Controller und Razor Pages über die folgenden Erweiterungsmethoden miteinander verknüpfen:

Beim Überladen dieser Methoden werden Argumente akzeptiert, die den HttpContext umfassen. Diese Methoden sind zwar in funktionaler Hinsicht äquivalent zu Url.Action und Url.Page, bieten aber zusätzliche Flexibilität und Optionen.

Die GetPath*-Methoden sind Url.Action und Url.Page in der Hinsicht ähnlich, dass sie einen URI mit einem absoluten Pfad generieren. Die GetUri*-Methoden generieren immer einen absoluten URI mit einem Schema und einem Host. Die Methoden, die einen HttpContext akzeptieren, generieren im Kontext der ausgeführten Anforderung einen URI. Die Umgebungsroutenwerte, der URL-basierte Pfad, das Schema und der Host von der ausführenden Anforderung werden so lange verwendet, bis sie außer Kraft gesetzt werden.

LinkGenerator wird mit einer Adresse aufgerufen. Ein URI wird in zwei Schritten generiert:

  1. Eine Adresse wird an eine Liste von Endpunkten gebunden, die der Adresse zugeordnet werden können.
  2. Jedes RoutePattern eines Endpunkts wird bewertet, bis ein Routenmuster gefunden wird, das den angegebenen Werten zugeordnet werden kann. Die daraus resultierende Ausgabe wird mit URI-Teilen kombiniert, die für die API zur Linkgenerierung bereitgestellt wird, und zurückgegeben.

Die von LinkGenerator bereitgestellten Methoden unterstützen die Standardfunktionen zur Generierung von Links für jeden beliebigen Adresstypen. Am praktischsten ist es, die API zur Linkgenerierung mit Erweiterungsmethoden zu verwenden, die Vorgänge für einen bestimmten Adresstypen ausführen:

Erweiterungsmethode Beschreibung
GetPathByAddress Generiert einen URI mit einem absoluten Pfad, der auf den angegebenen Werten basiert.
GetUriByAddress Generiert einen absoluten URI, der auf den angegebenen Werten basiert.

Warnung

Beachten Sie die folgenden Implikationen zum Aufrufen von LinkGenerator-Methoden:

  • Verwenden Sie GetUri*-Erweiterungsmethoden in App-Konfigurationen, die den Host-Header von eingehenden Anforderungen nicht überprüfen, mit Bedacht. Wenn der Host-Header von eingehenden Anforderungen nicht überprüft wird, können nicht vertrauenswürdige Anforderungseingaben zurück an den Client in URIs einer Ansicht bzw. Seite zurückgesendet werden. Es wird empfohlen, dass alle Produktions-Apps ihren Server so konfigurieren, dass der Host-Header auf bekannte gültige Werte überprüft wird.

  • Verwenden Sie LinkGenerator in Kombination mit Map oder MapWhen in Middleware mit Bedacht. Map* ändert den Basispfad der ausgeführten Anforderung. Dies beeinflusst die Ausgabe der Linkgenerierung. Für alle LinkGenerator-APIs ist die Angabe eines Basispfads zulässig. Geben Sie einen leeren Basispfad an, um die Auswirkungen von Map* auf die Linkgenerierung rückgängig zu machen.

Middlewarebeispiel

Im folgenden Beispiel verwendet eine Middleware die LinkGenerator-API, um eine Verknüpfung zu einer Aktionsmethode herzustellen, die Speicherprodukte aufführt. Sie können für jede beliebige Klasse in einer App die API zur Linkgenerierung verwenden, indem Sie diese in eine Klasse einfügen und GenerateLink aufrufen:

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}

Routenvorlagen

Token in {} definieren Routenparameter, die beim Abgleich der Route gebunden werden. In einem Routensegment können mehrere Routenparameter definiert werden, müssen aber durch einen Literalwert getrennt werden. Beispiel:

{controller=Home}{action=Index}

ist keine gültige Route, da sich zwischen {controller} und {action} kein Literalwert befindet. Routenparameter müssen einen Namen besitzen und können zusätzliche Attribute aufweisen.

Eine Literalzeichenfolge, die nicht den Routenparametern entspricht (z.B. {id}), muss zusammen mit dem Pfadtrennzeichen / mit dem URL-Text übereinstimmen. Beim Abgleich von Text wird nicht zwischen Groß-/Kleinbuchstaben unterschieden, und die Übereinstimmung basiert auf der decodierten Repräsentation des URL-Pfads. Damit das Trennzeichen ({ oder }) der Routenparameter-Literalzeichenfolge bei einem Abgleich gefunden wird, muss es doppelt vorhanden sein, was einem Escapezeichen entspricht. Beispielsweise {{ oder }}.

Sternchen * oder Doppelsternchen **:

  • Kann als Präfix für einen Routenparameter verwendet werden, um an den rest des URI zu binden.
  • Werden Catch-All-Parameter genannt. Zum Beispiel blog/{**slug}:
    • Entspricht einem beliebigen URI, der mit blog/ beginnt und einem beliebigen Wert folgt.
    • Der Wert nach blog/ wird dem Slug-Routenwert zugewiesen.

Warnung

Ein catch-all-Parameter kann aufgrund eines Fehlers beim Routing nicht ordnungsgemäß mit Routen übereinstimmen. Apps, die von diesem Fehler betroffen sind, weisen die folgenden Merkmale auf:

  • Eine catch-all-Route, zum Beispiel {**slug}"
  • Die catch-all-Route kann nicht mit Anforderungen abgeglichen werden, die abgeglichen werden sollen.
  • Durch das Entfernen anderer Routen funktioniert die catch-all-Route.

Weitere Beispiele zu diesem Fehler finden Sie in den GitHub-Issues 18677 und 16579.

Eine Opt-in-Behebung für diesen Fehler ist im .NET Core 3.1.301 SDK und höher enthalten. Der folgende Code legt einen internen Switch fest, mit dem dieser Fehler behoben wird:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Durch Catch-All-Parameter können auch leere Zeichenfolgen gefunden werden.

Der Catch-All-Parameter schützt die entsprechenden Zeichen (Escaping), wenn die Route verwendet wird, um eine URL, einschließlich Pfadtrennzeichen (/) zu generieren. Z.B. generiert die Route foo/{*path} mit den Routenwerten { path = "my/path" }foo/my%2Fpath. Beachten Sie den umgekehrten Schrägstrich mit Escapezeichen. Um Trennzeichen für Roundtrips einsetzen zu können, verwenden Sie das Routenparameterpräfix **. Die Route foo/{**path} mit { path = "my/path" } generiert foo/my/path.

Bei einem URL-Muster, durch das ein Dateiname mit einer optionalen Erweiterung erfasst werden soll, sind noch weitere Aspekte zu berücksichtigen. Dies wird z.B. anhand der Vorlage files/{filename}.{ext?} deutlich. Wenn sowohl für filename als auch für ext Werte vorhanden sind, werden beide Werte angegeben. Wenn nur für filename ein Wert in der URL vorhanden ist, wird für die Route eine Übereinstimmung ermittelt, da der nachstehende Punkt (.) optional ist. Für die folgenden URLs wird eine Übereinstimmung für die Route ermittelt:

  • /files/myFile.txt
  • /files/myFile

Routenparameter können über mehrere Standardwerte verfügen, die nach dem Parameternamen angegeben werden und durch ein Gleichheitszeichen (=) voneinander getrennt werden. Mit {controller=Home} wird beispielsweise Home als Standardwert für controller definiert. Der Standardwert wird verwendet, wenn kein Wert in der Parameter-URL vorhanden ist. Routenparameter sind optional, wenn am Ende des Parameternamens ein Fragezeichen (?) angefügt wird. Beispielsweise id?. Zwischen optionalen Werten und Standardroutenparametern besteht folgender Unterschied:

  • Ein Routenparameter mit einem Standardwert erzeugt immer einen Wert.
  • Ein optionaler Parameter hat nur dann einen Wert, wenn ein Wert von der Anforderungs-URL bereitgestellt wird.

Routenparameter können Einschränkungen aufweisen, die mit dem gebundenen Routenwert der URL übereinstimmen müssen. Eine Inline-Einschränkung für einen Routenparameter geben Sie an, indem Sie hinter dem Namen des Routenparameters einen Doppelpunkt (:) und einen Einschränkungsnamen hinzufügen. Wenn für die Einschränkung Argumente erforderlich sind, werden diese nach dem Einschränkungsnamen in Klammern ((...)) eingeschlossen. Mehrere Inline-Einschränkungen können festgelegt werden, indem ein weiterer Doppelpunkt (:) und Einschränkungsname hinzugefügt werden.

Der Einschränkungsname und die Argumente werden dem IInlineConstraintResolver-Dienst übergeben, wodurch eine Instanz von IRouteConstraint für die URL-Verarbeitung erstellt werden kann. In der Routenvorlage blog/{article:minlength(10)} wird beispielsweise die Einschränkung minlength mit dem Argument 10 festgelegt. Weitere Informationen zu Routeneinschränkungen und eine Liste der vom Framework bereitgestellten Einschränkungen finden Sie im Abschnitt Routeneinschränkungen.

Routenparameter können darüber hinaus über Parametertransformatoren verfügen. Diese wandeln den Wert eines Parameters beim Generieren von Links um und passen Aktionen und Seiten an URLs an. Wie Einschränkungen können auch Parametertransformatoren einem Routenparameter inline hinzugefügt werden, indem ein Doppelpunkt (:) und der Name des Transformators hinter dem Namen des Routenparameters hinzugefügt werden. In der Routenvorlage blog/{article:slugify} wird beispielsweise der Transformator slugify festgelegt. Weitere Informationen zu Parametertransformatoren finden Sie im Abschnitt Parametertransformatoren.

Die folgende Tabelle enthält Beispielvorlagen für Routen und deren Verhalten:

Routenvorlage Beispiel-URI für Übereinstimmung Der Anforderungs-URI
hello /hello Nur für den Pfad /hello wird eine Übereinstimmung ermittelt.
{Page=Home} / Eine Übereinstimmung wird ermittelt, und Page wird auf Home festgelegt.
{Page=Home} /Contact Eine Übereinstimmung wird ermittelt, und Page wird auf Contact festgelegt.
{controller}/{action}/{id?} /Products/List Stimmt mit dem Products-Controller und der List-Aktion überein.
{controller}/{action}/{id?} /Products/Details/123 Wird dem Controller Products und der Aktion Details zugeordnet, bei der id auf 123 festgelegt ist.
{controller=Home}/{action=Index}/{id?} / Stimmt mit dem Home-Controller und der Index-Methode überein. id wird ignoriert.
{controller=Home}/{action=Index}/{id?} /Products Stimmt mit dem Products-Controller und der Index-Methode überein. id wird ignoriert.

Mit Vorlagen lässt sich Routing besonders leicht durchführen. Einschränkungen und Standardwerte können auch außerhalb der Routenvorlage angegeben werden.

Komplexe Segmente

Komplexe Segmente werden von rechts nach links auf eine nicht gierige Weise durch entsprechende Literaltrennzeichen verarbeitet. Beispielsweise ist [Route("/a{b}c{d}")] ein komplexes Segment. Komplexe Segmente funktionieren auf eine bestimmte Weise, die für eine erfolgreiche Verwendung verstanden werden muss. Das Beispiel in diesem Abschnitt zeigt, warum komplexe Segmente nur dann wirklich gut funktionieren, wenn der Trennzeichentext nicht innerhalb der Parameterwerte erscheint. Für komplexere Fälle ist die Verwendung eines RegEx und das anschließende manuelle Extrahieren der Werte erforderlich.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Dies ist eine Zusammenfassung der Schritte, die beim Routing mit der Vorlage /a{b}c{d} und dem URL-Pfad /abcd ausgeführt werden. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen:

  • Das erste Literal von rechts nach links ist c. Daher wird /abcd von rechts durchsucht und /ab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /ab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Es ist kein verbleibender Text und keine verbleibende Routenvorlage vorhanden. Folglich ist dies eine Übereinstimmung.

Nachfolgend ist ein Beispiel für einen negativen Fall mit derselben Vorlage /a{b}c{d} und dem URL-Pfad /aabcd. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen: Bei diesem Fall handelt es sich nicht um eine Übereinstimmung, was durch denselben Algorithmus belegt wird:

  • Das erste Literal von rechts nach links ist c. Daher wird /aabcd von rechts durchsucht und /aab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /aab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /a|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Zu diesem Zeitpunkt gibt es noch verbleibenden Text a, aber es gibt keine Routenvorlage mehr, die der Algorithmus analysieren kann, weshalb dies keine Übereinstimmung ist.

Da der übereinstimmende Algorithmus nicht gierig ist:

  • Entspricht er der kleinstmöglichen Textmenge in jedem Schritt.
  • Alle Fälle, in denen der Trennzeichenwert in den Parameterwerten angezeigt wird, stimmen nicht überein.

Reguläre Ausdrücke bieten eine viel bessere Kontrolle über das Abgleichsverhalten.

Beim gierigen Abgleich, auch als Lazy Matching bezeichnet, wird die größtmögliche Zeichenfolge abgeglichen. Beim nicht gierigen Abgleich ist dies die kürzeste Zeichenfolge.

Routing mit Sonderzeichen

Ein Routing mit Sonderzeichen kann zu unerwarteten Ergebnissen führen. Stellen Sie sich z. B. einen Controller mit der folgenden Aktionsmethode vor:

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

Wenn string id die folgenden codierten Werte enthält, können unerwartete Ergebnisse auftreten:

ASCII Codiert
/ %2F
+

Routenparameter sind nicht immer URL-decodiert. Dieses Problem wird möglicherweise in Zukunft behoben. Weitere Informationen finden Sie in diesem GitHub-Issue.

Routeneinschränkungen

Routeneinschränkungen werden angewendet, wenn eine Übereinstimmung mit der eingehenden URL gefunden wurde und der URL-Pfad in Routenwerten mit Token versehen wird. In der Regel wird mit Routeneinschränkungen der Routenwert der zugehörigen Vorlage geprüft. Dabei wird anhand einer True/False-Entscheidung bestimmt, ob der Wert gültig ist. Für einige Routeneinschränkungen werden anstelle des Routenwerts andere Daten verwendet, um zu ermitteln, ob das Routing einer Anforderung möglich ist. HttpMethodRouteConstraint kann beispielsweise auf der Grundlage des HTTP-Verbs eine Anforderung entweder annehmen oder ablehnen. Einschränkungen werden in Routinganforderungen und bei der Linkgenerierung verwendet.

Warnung

Verwenden Sie keine Einschränkungen für die Eingabeüberprüfung. Wenn Einschränkungen für die Eingabevalidierung verwendet werden, führt eine ungültige Eingabe zu einem 404-Fehler (Nicht gefunden). Eine ungültige Eingabe sollte zu einer ungültigen Anforderung (400) mit einer entsprechenden Fehlermeldung führen. Verwenden Sie Routeneinschränkungen nicht, um Eingaben für eine bestimmte Route zu überprüfen, sondern um ähnliche Routen zu unterscheiden.

In der folgenden Tabelle werden Beispiele für Routeneinschränkungen und deren zu erwartendes Verhalten beschrieben:

Einschränkung Beispiel Beispiele für Übereinstimmungen Hinweise
int {id:int} 123456789, -123456789 Für jeden Integer wird eine Übereinstimmung ermittelt.
bool {active:bool} true, FALSE Entspricht true oder false. Groß-/Kleinschreibung nicht beachten
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Entspricht einem gültigen DateTime-Wert in der invarianten Kultur. Siehe vorherige Warnung.
decimal {price:decimal} 49.99, -1,000.01 Entspricht einem gültigen decimal-Wert in der invarianten Kultur. Siehe vorherige Warnung.
double {weight:double} 1.234, -1,001.01e8 Entspricht einem gültigen double-Wert in der invarianten Kultur. Siehe vorherige Warnung.
float {weight:float} 1.234, -1,001.01e8 Entspricht einem gültigen float-Wert in der invarianten Kultur. Siehe vorherige Warnung.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Für einen gültigen Guid-Wert wird eine Übereinstimmung ermittelt.
long {ticks:long} 123456789, -123456789 Für einen gültigen long-Wert wird eine Übereinstimmung ermittelt.
minlength(value) {username:minlength(4)} Rick Die Zeichenfolge muss mindestens eine Länge von 4 Zeichen aufweisen.
maxlength(value) {filename:maxlength(8)} MyFile Die Zeichenfolge darf maximal eine Länge von 8 Zeichen aufweisen.
length(length) {filename:length(12)} somefile.txt Die Zeichenfolge muss genau 12 Zeichen aufweisen.
length(min,max) {filename:length(8,16)} somefile.txt Die Zeichenfolge muss mindestens eine Länge von 8 und darf maximal eine Länge von 16 Zeichen aufweisen.
min(value) {age:min(18)} 19 Der Integerwert muss mindestens 18 sein.
max(value) {age:max(120)} 91 Der Integerwert darf nicht größer als 120 sein.
range(min,max) {age:range(18,120)} 91 Der Integerwert muss zwischen 18 und 120 liegen.
alpha {name:alpha} Rick Die Zeichenfolge muss aus mindestens einem alphabetische Zeichen bestehen, a-z und ohne Unterscheidung zwischen Groß-/Kleinbuchstaben.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 Die Zeichenfolge muss mit dem regulären Ausdruck übereinstimmen. Weitere Informationen finden Sie unter Tipps zum Definieren eines regulären Ausdrucks.
required {name:required} Rick Hierdurch wird erzwungen, dass ein Wert, der kein Parameter ist, für die URL-Generierung vorhanden sein muss.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Auf einen einzelnen Parameter können mehrere durch Doppelpunkte getrennte Einschränkungen angewendet werden. Durch die folgende Einschränkung wird ein Parameter beispielsweise auf einen Integerwert größer oder gleich 1 beschränkt:

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Warnung

Für Routeneinschränkungen, mit denen die URL überprüft wird und die in den CLR-Typ umgewandelt werden, wird immer die invariante Kultur verwendet. Dies gilt z. B. für die Konvertierung in den CLR-Typ int oder DateTime. Diese Einschränkungen setzen voraus, dass die URL nicht lokalisierbar ist. Die vom Framework bereitgestellten Routeneinschränkungen ändern nicht die Werte, die in Routenwerten gespeichert sind. Alle Routenwerte, die aus der URL analysiert werden, werden als Zeichenfolgen gespeichert. Durch die float-Einschränkung wird beispielsweise versucht, den Routenwert in einen Gleitkommawert zu konvertieren. Mit dem konvertierten Wert wird allerdings nur überprüft, ob eine Umwandlung überhaupt möglich ist.

Reguläre Ausdrücke in Einschränkungen

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Reguläre Ausdrücke können mithilfe der regex(...)-Routeneinschränkung als Inline-Einschränkungen angegeben werden. Methoden der MapControllerRoute-Familie akzeptieren auch ein Objektliteral von Einschränkungen. Wenn dieses Formular verwendet wird, werden Zeichenfolgenwerte als reguläre Ausdrücke interpretiert.

Der folgende Code verwendet eine Inline-RegEx-Einschränkung:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

Der folgende Code verwendet ein Objektliteral, um eine RegEx-Einschränkung anzugeben:

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

Im ASP.NET Core-Framework wird dem Konstruktor für reguläre Ausdrücke RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant hinzugefügt. Eine Beschreibung dieser Member finden Sie unter RegexOptions.

In regulären Ausdrücken werden Trennzeichen und Token verwendet, die auch beim Routing und in der Programmiersprache C# in ähnlicher Weise verwendet werden. Token, die reguläre Ausdrücke enthalten, müssen mit einem Escapezeichen versehen werden. Wenn Sie den regulären Ausdruck ^\d{3}-\d{2}-\d{4}$ in einer Inline-Einschränkung verwenden möchten, nutzen Sie eine der folgenden Optionen:

  • Ersetzen Sie \-Zeichen in der Zeichenfolge durch \\-Zeichen in der C#-Quelldatei, um das Escapezeichen für die Zeichenfolge \ zu setzen.
  • Ausführliche Zeichenfolgeliterale

Wenn Sie Trennzeichen für Routenparameter mit Escapezeichen versehen möchten ({, }, [, ]), geben Sie jedes Zeichen im Ausdruck doppelt ein (z. B. {{, }}, [[, ]]). In der folgenden Tabelle werden reguläre Ausdrücke und Ausdrücke aufgeführt, die mit Escapezeichen versehen sind:

Regulärer Ausdruck Mit Escapezeichen versehener regulärer Ausdruck
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Beim Routing verwendete reguläre Ausdrücke beginnen oft mit dem ^-Zeichen und stellen die Startposition der Zeichenfolge dar. Die Ausdrücke enden häufig mit einem Dollarzeichen ($) und stellen das Ende der Zeichenfolge dar. Mit den Zeichen ^ und $ wird sichergestellt, dass der reguläre Ausdruck mit dem vollständigen Routenparameterwert übereinstimmt. Ohne die Zeichen ^ und $ werden mit dem regulären Ausdruck alle Teilzeichenfolgen ermittelt, was häufig nicht gewünscht ist. In der folgenden Tabelle finden Sie Beispiele für reguläre Ausdrücke. Außerdem wird erklärt, warum ein Abgleich erfolgreich ist oder fehlschlägt:

expression Zeichenfolge Match Kommentar
[a-z]{2} hello Ja Teilzeichenfolge stimmt überein
[a-z]{2} 123abc456 Ja Teilzeichenfolge stimmt überein
[a-z]{2} mz Ja Ausdruck stimmt überein
[a-z]{2} MZ Ja keine Unterscheidung zwischen Groß-/Kleinbuchstaben
^[a-z]{2}$ hello Nein siehe Erläuterungen zu ^ und $ oben
^[a-z]{2}$ 123abc456 Nein siehe Erläuterungen zu ^ und $ oben

Weitere Informationen zur Syntax von regulären Ausdrücken finden Sie unter Sprachelemente für reguläre Ausdrücke – Kurzübersicht.

Einen regulären Ausdruck können Sie verwenden, um einen Parameter auf zulässige Werte einzuschränken. Mit {action:regex(^(list|get|create)$)} werden beispielsweise für den action-Routenwert nur die Werte list, get oder create abgeglichen. Wenn die Zeichenfolge ^(list|get|create)$ dem Einschränkungswörterbuch übergeben wird, führt dies zum gleichen Ergebnis. Auch Einschränkungen, die dem zugehörigen Wörterbuch hinzugefügt werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden als reguläre Ausdrücke behandelt. Einschränkungen, die innerhalb einer Vorlage übergeben werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden nicht als reguläre Ausdrücke behandelt.

Benutzerdefinierte Routeneinschränkungen

Benutzerdefinierte Routeneinschränkungen können durch Implementierung der IRouteConstraint-Schnittstelle erstellt werden. Die IRouteConstraint-Schnittstelle umfasst die Match-Methode, die true zurückgibt, wenn die Einschränkung erfüllt wird, und andernfalls false.

Benutzerdefinierte Routeneinschränkungen werden nur selten benötigt. Bevor Sie eine benutzerdefinierte Routeneinschränkung implementieren, sollten Sie Alternativen in Betracht ziehen, wie z. B. Modellbindung.

Der ASP.NET Core-Ordner Constraints bietet nützliche Beispiele für die Erstellung von Einschränkungen. Beispiel: GuidRouteConstraint.

Zum Verwenden eines benutzerdefinierten IRouteConstraint-Elements muss der Routeneinschränkungstyp bei der ConstraintMap-Eigenschaft der App im Dienstcontainer registriert werden. Eine ConstraintMap ist ein Wörterbuch, das Routeneinschränkungsschlüssel IRouteConstraint-Implementierungen zuordnet, die diese Einschränkungen überprüfen. Die ConstraintMap einer App kann in Program.cs entweder als Teil eines AddRouting-Aufrufs oder durch direktes Konfigurieren von RouteOptions mit builder.Services.Configure<RouteOptions> aktualisiert werden. Zum Beispiel:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

Die vorangehende Einschränkung wird im folgenden Code angewendet:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

Die Implementierung von NoZeroesRouteConstraint verhindert die Verwendung von 0 in einem Routenparameter:

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Der vorangehende Code:

  • Verhindert, dass 0 im {id}-Segment der Route vorhanden ist.
  • Dient als einfaches Beispiel für die Implementierung einer benutzerdefinierten Einschränkung. Es sollte nicht in einer Produktions-App eingesetzt werden.

Der folgende Code bietet einen besseren Ansatz, um zu verhindern, dass eine id mit einer 0 verarbeitet wird:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

Der vorangehende Code bietet im Vergleich zum NoZeroesRouteConstraint-Ansatz folgende Vorteile:

  • Eine benutzerdefinierte Einschränkung ist nicht erforderlich.
  • Es wird ein beschreibender Fehler zurückgegeben, wenn der Routenparameter 0 enthält.

Parametertransformatoren

Parametertransformatoren:

Beispielsweise generiert ein benutzerdefinierter Parametertransformator slugify im Routenmuster blog\{article:slugify} mit Url.Action(new { article = "MyTestArticle" })blog\my-test-article.

Betrachten Sie die folgende Implementierung von IOutboundParameterTransformer:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}

Um einen Parametertransformator in einem Routenmuster zu verwenden, konfigurieren Sie ihn mit ConstraintMap in Program.cs:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

Das ASP.NET Core-Framework verwendet Parametertransformatoren, um den URI zu transformieren, zu dem ein Endpunkt aufgelöst wird. Beispielsweise wandeln Parametertransformatoren die Routenwerte um, die zum Zuordnen folgender Elemente verwendet werden: area, controller, action und page.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Mit der vorstehenden Routenvorlage wird die Aktion SubscriptionManagementController.GetAll dem URI /subscription-management/get-all zugeordnet. Ein Parametertransformator ändert nicht die zum Generieren eines Links verwendeten Routenwerte. Beispielsweise gibt Url.Action("GetAll", "SubscriptionManagement")/subscription-management/get-all aus.

ASP.NET Core bietet API-Konventionen für die Verwendung von Parametertransformatoren mit generierten Routen:

Referenz für URL-Generierung

Dieser Abschnitt enthält eine Referenz für den Algorithmus, der durch die URL-Generierung implementiert wird. In der Praxis werden bei den meisten komplexen Beispielen für die URL-Generierung Controller oder Razor Pages verwendet. Weitere Informationen finden Sie unter Routing in Controllern.

Die URL-Generierung beginnt mit einem Aufruf von LinkGenerator.GetPathByAddress oder einer ähnlichen Methode. Die Methode wird mit einer Adresse, mehreren Routenwerten und optional mit Informationen zur aktuellen Anforderung von HttpContext versehen.

Im ersten Schritt wird die Adresse verwendet, um bestimmte Endpunktkandidaten mithilfe einer IEndpointAddressScheme<TAddress>-Schnittstelle aufzulösen, die dem Adresstyp entspricht.

Sobald eine Kandidatengruppe anhand des Adressschemas gefunden wurde, werden die Endpunkte geordnet und iterativ verarbeitet, bis die URL-Generierung erfolgreich abgeschlossen ist. Bei der URL-Generierung wird nicht auf Mehrdeutigkeiten geprüft, daher ist das erste zurückgegebene Ergebnis das Endergebnis.

Behandeln von Problemen mit der Protokollierung bei der URL-Generierung

Der erste Schritt bei der Behebung von Problemen bei der URL-Generierung ist die Einstellung des Protokolliergrads von Microsoft.AspNetCore.Routing auf TRACE. LinkGenerator protokolliert viele Details über die Verarbeitung, die bei der Problembehebung nützlich sein können.

Ausführliche Informationen zur URL-Generierung finden Sie unter Referenz für URL-Generierung.

Adressen

Mithilfe von Adressen wird bei der URL-Generierung ein Aufruf in der API zur Linkgenerierung an mehrere Endpunktkandidaten gebunden.

Adressen sind ein erweiterbares Konzept, das standardmäßig mit zwei Implementierungen bereitgestellt wird:

  • Die Verwendung von Endpunktname (string) als Adresse:
    • Bietet ähnliche Funktionalität wie der Routenname von MVC.
    • Wird der IEndpointNameMetadata-Metadatentyp verwendet.
    • Löst die bereitgestellte Zeichenfolge anhand der Metadaten aller registrierten Endpunkte auf.
    • Löst beim Start eine Ausnahme aus, wenn mehrere Endpunkte den gleichen Namen aufweisen.
    • Wird für die allgemeine Verwendung außerhalb von Controllern und Razor Pages empfohlen.
  • Die Verwendung von Routenwerten (RouteValuesAddress) als Adresse:
    • Bietet eine ähnliche Funktionalität wie die veraltete Funktion zur URL-Generierung von Controllern und Razor Pages.
    • Lässt sich nur schwer erweitern und debuggen.
    • Bietet die Implementierung, die von IUrlHelper, Taghilfsprogrammen, HTML-Hilfsprogrammen, Aktionsergebnissen usw. verwendet wird.

Aufgabe des Adressschemas ist es, die Verbindung zwischen der Adresse und den übereinstimmenden Endpunkten anhand von beliebigen Kriterien herzustellen:

  • Das Schema für Endpunktenamen führt eine allgemeine Wörterbuchsuche durch.
  • Das Schema der Routenwerte weist eine komplexe beste Teilmenge des Mengenalgorithmus auf.

Umgebungswerte und explizite Werte

Aus der aktuellen Anforderung greift das Routing auf die Routenwerte der aktuellen Anforderung HttpContext.Request.RouteValues zu. Die mit der aktuellen Anforderung verbundenen Werte werden als Umgebungswerte bezeichnet. Aus Gründen der Übersichtlichkeit werden in der Dokumentation die an die Methoden übergebenen Routenwerte als explizite Werte bezeichnet.

Das folgende Beispiel zeigt Umgebungswerte und explizite Werte. Er liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte:

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

Der vorangehende Code:

Der folgende Code liefert nur explizite Werte und keine Umgebungswerte:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

Die vorhergehende Methode gibt /Home/Subscribe/17 zurück.

Der folgende Code in WidgetController gibt /Widget/Subscribe/17 zurück:

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

Der folgende Code stellt den Controller aus den Umgebungswerten in der aktuellen Anforderung und explizite Werte dar:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

Für den Code oben gilt:

  • /Gadget/Edit/17 wird zurückgegeben.
  • Url ruft die IUrlHelper-Schnittstelle ab.
  • Action generiert eine URL mit einem absoluten Pfad für eine Aktionsmethode. Die URL enthält den angegebenen action-Namen und route-Werte.

Sie liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

Im vorangehenden Code wird url auf /Edit/17 festgelegt, wenn die Option zum Bearbeiten der Razor Page die folgende Seitenanweisung enthält:

@page "{id:int}"

Wenn die Routenvorlage "{id:int}" nicht in der Seite „Bearbeiten“ enthalten ist, ist url gleich /Edit?id=17.

Das Verhalten der IUrlHelper-Schnittstelle von MVC fügt zusätzlich zu den hier beschriebenen Regeln eine weitere Komplexitätsebene hinzu:

  • IUrlHelper liefert immer die Routenwerte aus der aktuellen Anforderung als Umgebungswerte.
  • IUrlHelper.action kopiert immer die aktuellen Routenwerte action und controller als explizite Werte, sofern sie nicht vom Entwickler außer Kraft gesetzt werden.
  • IUrlHelper.page kopiert immer den aktuellen Routenwert page als expliziten Wert, sofern er nicht außer Kraft gesetzt wird.
  • IUrlHelper.Page setzt immer den aktuellen Routenwert handler mit null als expliziten Wert außer Kraft, sofern er nicht außer Kraft gesetzt wird.

Benutzer sind oft von den Verhaltensdetails der Umgebungswerte überrascht, da MVC anscheinend nicht den eigenen Regeln folgt. Aus Verlaufs- und Kompatibilitätsgründen weisen bestimmte Routenwerte wie action, controller, page und handler ein spezielles Verhalten auf.

Die äquivalente Funktionalität, die durch LinkGenerator.GetPathByAction und LinkGenerator.GetPathByPage bereitgestellt wird, verdoppelt diese Anomalien von IUrlHelper aus Kompatibilitätsgründen.

URL-Generierungsprozess

Sobald die Gruppe der Endpunktkandidaten ermittelt ist, wird der URL-Generierungsalgorithmus angewendet:

  • Die Endpunkte werden iterativ verarbeitet.
  • Das erste erfolgreiche Ergebnis wird zurückgegeben.

Der erste Schritt in diesem Prozess wird als Routenwertinvalidierung bezeichnet. Die Routenwertinvalidierung ist der Prozess, bei dem das Routing entscheidet, welche Routenwerte aus den Umgebungswerten verwendet und welche ignoriert werden sollen. Jeder Umgebungswert wird berücksichtigt und entweder mit den expliziten Werten kombiniert oder aber ignoriert.

Denken Sie daran, dass Umgebungswerte Anwendungsentwicklern in allgemeinen Fällen das Schreiben von Code sparen können. In der Regel sind die Szenarios, in denen Umgebungswerte hilfreich sind, mit MVC verknüpft:

  • Bei der Verknüpfung mit einer anderen Aktion im gleichen Controller muss der Controllername nicht angegeben werden.
  • Bei der Verknüpfung mit einem anderen Controller im gleichen Bereich muss der Bereich nicht angegeben werden.
  • Bei der Verknüpfung mit der gleichen Aktionsmethode müssen keine Routenwerte angegeben werden.
  • Bei der Verknüpfung mit einem anderen Teil der App sollen keine Routenwerte übertragen werden, die für diesen Teil der App irrelevant sind.

Aufrufe an LinkGenerator oder IUrlHelper, die null zurückgeben, sind meist dadurch bedingt, dass die Routenwertinvalidierung nicht verstanden wurde. Beheben Sie die Routenwertinvalidierung, indem Sie explizit mehr Routenwerte angeben, um zu prüfen, ob das Problem dadurch gelöst wird.

Bei der Routenwertinvalidierung wird davon ausgegangen, dass das URL-Schema der Anwendung hierarchisch ist, mit einer von links nach rechts gebildeten Hierarchie. Sehen Sie sich die einfache Controllerroutenvorlage {controller}/{action}/{id?} an, um ein Gespür dafür zu bekommen, wie dies in der Praxis funktioniert. Durch eine Änderung auf einen Wert werden alle rechts angezeigten Routenwerte ungültig. Dies spricht für die These von der Hierarchie. Wenn die App einen Umgebungswert für id hat und der Vorgang einen anderen Wert für controller angibt:

  • id wird nicht wiederverwendet, weil {controller} links von {id?} steht.

Einige Beispiele veranschaulichen dieses Prinzip:

  • Wenn die expliziten Werte einen Wert für id enthalten, wird der Umgebungswert für id ignoriert. Die Umgebungswerte für controller und action können verwendet werden.
  • Wenn die expliziten Werte einen Wert für action enthalten, wird jeder Umgebungswert für action ignoriert. Die Umgebungswerte für controller können verwendet werden. Wenn sich der explizite Wert für action von dem Umgebungswert für action unterscheidet, wird der Wert id nicht verwendet. Wenn der explizite Wert für action mit dem Umgebungswert für action übereinstimmt, kann der Wert id verwendet werden.
  • Wenn die expliziten Werte einen Wert für controller enthalten, wird jeder Umgebungswert für controller ignoriert. Wenn sich der explizite Wert für controller von dem Umgebungswert für controller unterscheidet, werden die Werte action und id nicht verwendet. Wenn der explizite Wert für controller mit dem Umgebungswert für controller übereinstimmt, können die Werte action und id verwendet werden.

Dieser Prozess wird zusätzlich durch die vorhandenen Attributrouten und dedizierten konventionellen Routen erschwert. Konventionelle Routen des Controllers wie {controller}/{action}/{id?} legen eine Hierarchie mithilfe von Routenparametern fest. Bei bestimmten konventionellen Routen und Attributrouten zu Controllern und Razor Pages:

  • Gibt es eine Hierarchie für Routenwerte.
  • Werden diese nicht in der Vorlage angezeigt.

Für diese Fälle definiert die URL-Generierung das Konzept der erforderlichen Werte. Bei Endpunkten, die von Controllern und Razor Pages erstellt wurden, sind erforderliche Werte angegeben, die eine Routenwertinvalidierung ermöglichen.

Der Algorithmus der Routenwertinvalidierung im Detail:

  • Die erforderlichen Wertnamen werden mit den Routenparametern kombiniert und dann von links nach rechts verarbeitet.
  • Für jeden Parameter werden der Umgebungswert und der explizite Wert verglichen:
    • Wenn der Umgebungswert und der explizite Wert gleich sind, wird der Prozess fortgesetzt.
    • Wenn der Umgebungswert vorhanden ist und der explizite Wert nicht, wird der Umgebungswert bei der URL-Generierung verwendet.
    • Wenn der Umgebungswert nicht vorhanden ist und der explizite Wert vorhanden ist, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.
    • Wenn der Umgebungswert und der explizite Wert vorhanden und die beiden Werte unterschiedlich sind, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.

An diesem Punkt ist der Vorgang zur URL-Generierung bereit, Routeneinschränkungen auszuwerten. Die akzeptierten Werte werden mit den Standardwerten der Parameter kombiniert, die für Einschränkungen bereitgestellt werden. Wenn alle Einschränkungen erfüllt sind, wird der Vorgang fortgesetzt.

Als Nächstes können die akzeptierten Werte verwendet werden, um die Routenvorlage zu erweitern. Die Routenvorlage wird verarbeitet:

  • Von links nach rechts.
  • Für jeden Parameter wird der akzeptierte Wert ersetzt.
  • In den folgenden Sonderfällen:
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter einen Standardwert hat, wird der Standardwert verwendet.
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter optional ist, wird die Verarbeitung fortgesetzt.
    • Wenn irgendein Routenparameter rechts neben einem fehlenden optionalen Parameter einen Wert hat, schlägt der Vorgang fehl.
    • Zusammenhängende Parameter mit Standardwerten und optionale Parameter werden, wenn möglich, reduziert dargestellt.

Explizit bereitgestellte Werte, für die keine Übereinstimmungen mit einem Routensegment ermittelt werden, werden der Abfragezeichenfolge hinzugefügt. In der folgenden Tabelle werden die Ergebnisse dargestellt, die aus der Verwendung der Routenvorlage {controller}/{action}/{id?} hervorgehen:

Umgebungswerte Explizite Werte Ergebnis
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

Reihenfolge der optionalen Routenparameter

Optionale Routenparameter müssen nach allen erforderlichen Routenparametern kommen. Im folgenden Code müssen die Parameter id und name nach dem Parameter color kommen:

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}

Probleme mit der Routenwertinvalidierung

Der folgende Code zeigt ein Beispiel für ein Schema zur URL-Generierung, das vom Routing nicht unterstützt wird:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

Im vorangehenden Code wird der culture-Routenparameter für die Lokalisierung verwendet. Ziel ist es, dass der culture-Parameter immer als Umgebungswert akzeptiert wird. Der culture-Parameter wird jedoch aufgrund der Art und Weise, wie die erforderlichen Werte funktionieren, nicht als Umgebungswert akzeptiert:

  • In der "default"-Routenvorlage befindet sich der culture-Routenparameter links von controller, sodass culture durch Änderungen an controller nicht ungültig wird.
  • In der "blog"-Routenvorlage wird der culture-Routenparameter rechts von controller betrachtet, der in den erforderlichen Werten aufgeführt ist.

Zerlegen von URL-Pfaden mit LinkParser

Die LinkParser Klasse fügt die Möglichkeit hinzu, einen URL-Pfads in einen Satz von Routenwerten zu zerlegen. Die ParsePathByEndpointName Methode verwendet einen Endpunktnamen und einen URL-Pfad und gibt einen Satz von aus dem URL-Pfad extrahierten Routenwerten zurück.

Im folgenden Beispielcontroller verwendet die GetProduct Aktion eine Routenvorlage von api/Products/{id} und hat eine Name von GetProduct:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

In derselben Controllerklasse erwartet die AddRelatedProduct Aktion einen URL-Pfad, pathToRelatedProduct, der als Abfragezeichenfolgen-Parameter bereitgestellt werden kann:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

Im vorherigen Beispiel extrahiert die AddRelatedProduct Aktion den id Routenwert aus dem URL-Pfad. Beispielsweise wird der relatedProductId Wert bei einem /api/Products/1 URL-Pfad auf 1 festgelegt. Dieser Ansatz ermöglicht es den Clients der API, URL-Pfade beim Verweisen auf Ressourcen zu verwenden, ohne wissen zu müssen, wie eine solche URL strukturiert ist.

Konfigurieren von Endpunktmetadaten

Die folgenden Links enthalten Informationen zum Konfigurieren von Endpunktmetadaten:

Hostabgleich in Routen mit RequireHost

RequireHost wendet eine Einschränkung auf die Route an, für die der angegebene Host erforderlich ist. Der Parameter RequireHost oder [Host] kann wie folgt lauten:

  • Host: www.domain.com, entspricht www.domain.com mit einem beliebigen Port.
  • Host mit Platzhalter: *.domain.com, entspricht www.domain.com, subdomain.domain.com oder www.subdomain.domain.com an einem beliebigen Port.
  • Port: *:5000, entspricht Port 5000 mit einem beliebigen Host.
  • Host und Port: www.domain.com:5000 oder *.domain.com:5000, entspricht dem Host und Port.

Es können mehrere Parameter mit RequireHost oder [Host] angegeben werden. Die Einschränkung gleicht die Hosts ab, die für einen der Parameter gültig sind. Beispielsweise entspricht [Host("domain.com", "*.domain.com")]domain.com, www.domain.com und subdomain.domain.com.

Im folgenden Code wird RequireHost verwendet, um den angegebenen Host auf der Route anzufordern:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

Im folgenden Code wird das [Host]-Attribut für den Controller verwendet, um die einzelnen angegebenen Hosts anzufordern:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

Wenn das [Host]-Attribut sowohl auf die Controller- als auch auf die Aktionsmethode angewendet wird, trifft Folgendes zu:

  • Das Attribut auf der Aktion wird verwendet.
  • Das Controllerattribut wird ignoriert.

Routengruppen

Die MapGroup-Erweiterungsmethode hilft, Gruppen von Endpunkten mit einem gemeinsamen Präfix zu organisieren. Sie reduziert sich wiederholenden Code und ermöglicht die benutzerdefinierte Anpassung ganzer Gruppen von Endpunkten mit einem einzigen Aufruf von Methoden wie RequireAuthorization und WithMetadata,die Endpunktmetadaten hinzufügen.

Der folgende Code erstellt beispielsweise zwei ähnliche Endpunktgruppen:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

In diesem Szenario können Sie eine relative Adresse für den Location-Header im 201 Created-Ergebnis verwenden:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

Die erste Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /public/todos und ist ohne Authentifizierung zugänglich. Die zweite Gruppe von Endpunkten entspricht nur Anforderungen mit dem Präfix /private/todos und erfordert Authentifizierung.

Die QueryPrivateTodos-Endpunktfilterfactory ist eine lokale Funktion, die die TodoDb-Parameter des Routenhandlers so ändert, dass Zugriff auf private Aufgabendaten zulässig ist und diese gespeichert werden können.

Routengruppen unterstützen auch geschachtelte Gruppen und komplexe Präfixmuster mit Routenparametern und -einschränkungen. Im folgenden Beispiel kann der der user-Gruppe zugeordnete Routenhandler die Routenparameter {org} und {group} erfassen, die in den Präfixen der äußeren Gruppe definiert sind.

Das Präfix kann auch leer sein. Dies kann hilfreich sein, um Endpunktmetadaten oder Filter einer Gruppe von Endpunkten hinzuzufügen, ohne das Routenmuster zu ändern.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

Das Hinzufügen von Filtern oder Metadaten zu einer Gruppe verhält sich genauso wie das individuelle Hinzufügen zu jedem Endpunkt, bevor zusätzliche Filter oder Metadaten hinzugefügt werden, die einer inneren Gruppe oder einem bestimmten Endpunkt hinzugefügt wurden.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

Im Beispiel oben protokolliert der äußere Filter die eingehende Anforderung vor dem inneren Filter, obwohl er als zweiter hinzugefügt wurde. Da die Filter auf verschiedene Gruppen angewendet wurden, spielt die Reihenfolge, in der sie relativ zueinander hinzugefügt wurden, keine Rolle. Die Reihenfolge, in der Filter hinzugefügt werden, spielt eine Rolle, wenn sie auf dieselbe Gruppe oder einen bestimmten Endpunkt angewendet werden.

Eine Anforderung an /outer/inner/ protokolliert Folgendes:

/outer group filter
/inner group filter
MapGet filter

Leistungsleitfaden für das Routing

Wenn eine App Leistungsprobleme hat, wird die Ursache häufig beim Routing vermutet. Das Routing wird deshalb in Betracht gezogen, weil Frameworks wie Controller und Razor Pages in ihren Protokollierungsmeldungen die innerhalb des Frameworks verbrachte Zeit angeben. Wenn es einen signifikanten Unterschied zwischen der von den Controllern gemeldeten Zeit und der Gesamtzeit der Anforderung gibt:

  • Schließen Entwickler ihren App-Code als Ursache des Problems aus.
  • Wird in der Regel angenommen, dass das Routing die Ursache für das Problem ist.

Die Leistung des Routings wird anhand von Tausenden von Endpunkten getestet. Es ist unwahrscheinlich, dass eine typische App auf ein Leistungsproblem stößt, nur weil diese zu umfangreich ist. Die häufigste Ursache für eine langsames Routing ist üblicherweise eine schlecht funktionierende benutzerdefinierte Middleware.

Das folgende Codebeispiel veranschaulicht eine grundlegende Technik zur Eingrenzung der Verzögerungsquelle:

var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");

Auf das Zeitrouting:

  • Verschachteln Sie jede Middleware mit einer Kopie der im vorherigen Code gezeigten Zeitmiddleware.
  • Fügen Sie einen eindeutigen Bezeichner hinzu, um die Zeitdaten mit dem Code zu korrelieren.

Dies ist ein einfacher Weg, um die Verzögerung zu verringern, wenn sie signifikant ist, zum Beispiel größer als 10ms. Wenn Time 2 von Time 1 subtrahiert wird, ergibt sich die in der UseRouting-Middleware benötigte Zeit.

Der folgende Code verwendet einen kompakteren Ansatz als der vorangegangene Zeitcode:

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");

Potenziell teure Routingfeatures

Die folgende Liste gibt einen Einblick in Routingfeatures, die im Vergleich zu einfachen Routenvorlagen relativ teuer sind:

  • Reguläre Ausdrücke: Mit einer kleinen Menge an Eingaben ist möglich, reguläre Ausdrücke zu schreiben, die komplex sind oder eine lange Ausführungszeit haben.
  • Komplexe Segmente ({x}-{y}-{z}):
    • Sind wesentlich teurer als das Analysieren eines regulären URL-Pfadsegments.
    • Führen dazu, dass viel mehr Teilzeichenfolgen zugeordnet werden.
  • Synchroner Datenzugriff: Viele komplexe Apps verfügen im Rahmen Ihrer Routingfunktionen über Datenbankzugriff. Verwenden Sie Erweiterbarkeitspunkte wie MatcherPolicy und EndpointSelectorContext, die asynchron sind.

Leitfaden für große Routingtabellen

ASP.NET Core verwendet standardmäßig einen Routingalgorithmus, bei dem Arbeitsspeicher zugunsten von CPU-Zeit geopfert wird. Dies hat den angenehmen Effekt, dass die Zeit für den Routenabgleich nur von der Länge des abzugleichenden Pfads und nicht von der Routenanzahl abhängt. Dieser Ansatz kann jedoch in einigen Fällen problematisch sein, etwa dann, wenn die App eine große Anzahl von Routen umfasst (mehrere Tausend) und die Routen eine große Anzahl variabler Präfixe enthalten. Beispiel: Die Routen weisen Parameter in frühen Segmenten der Route auf, etwa {parameter}/some/literal.

Es ist unwahrscheinlich, dass eine App in eine Situation gerät, in der dies ein Problem darstellt, es sei denn:

  • Es ist eine große Anzahl von Routen in der App vorhanden, die dieses Muster verwenden.
  • Es ist eine große Anzahl von Routen in der App vorhanden.

Wie lässt sich feststellen, ob eine Anwendung mit dem Problem einer großer Routentabelle zu kämpfen hat?

  • Es gibt zwei Symptome, auf die Sie achten sollten:
    • Die App wird bei der ersten Anforderung langsam gestartet.
      • Dieses Symptom ist zwingend, reicht aber allein nicht aus. Es gibt viele andere, nicht routenbezogene Probleme, die zu einem langsamen App-Start führen können. Überprüfen Sie auch auf die folgende Bedingung, um genau zu bestimmen, ob diese Situation für die App vorliegt.
    • Die Anwendung verbraucht beim Start viel Speicher, und ein Speicherabbild zeigt eine große Anzahl von Microsoft.AspNetCore.Routing.Matching.DfaNode-Instanzen.

Beheben dieses Problems

Es gibt verschiedene Techniken und Optimierungen, die auf Routen angewendet werden können, um dieses Szenario weitgehend zu vermeiden:

  • Wenden Sie nach Möglichkeit Routeneinschränkungen auf Ihre Parameter an, z. B. {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}.
    • Dadurch kann der Routingalgorithmus die für den Abgleich verwendeten Strukturen intern optimieren und den verwendeten Arbeitsspeicher drastisch reduzieren.
    • In den meisten Fällen reicht dies aus, um zu einem akzeptablen Verhalten zurückzukehren.
  • Ändern Sie die Routen, um Parameter in spätere Segmente in der Vorlage zu verlagern.
    • Dadurch wird die Anzahl von möglichen „Pfaden“ reduziert, die bei Angabe eines Pfads mit einem Endpunkt übereinstimmen.
  • Verwenden Sie eine dynamische Route, und führen Sie die Zuordnung zu einem Controller/einer Seite dynamisch durch.
    • Dies kann mithilfe von MapDynamicControllerRoute und MapDynamicPageRoute erreicht werden.

Leitfaden für Bibliotheksautoren

Dieser Abschnitt enthält Hinweise für Bibliotheksautoren, die auf dem Routing aufbauen. Diese Details sollen sicherstellen, dass App-Entwickler gute Erfahrungen mit Bibliotheken und Frameworks machen, die das Routing erweitern.

Definieren von Endpunkten

Wenn Sie ein Framework erstellen möchten, das das Routing für den URL-Abgleich verwendet, sollten Sie zunächst eine Benutzeroberfläche definieren, die auf UseEndpoints aufbaut.

Es wird empfohlen, dass Sie auf IEndpointRouteBuilder aufbauen. Auf diese Weise können Benutzer Ihr Framework mit anderen ASP.NET Core-Features problemlos zusammenstellen. Jede ASP.NET Core-Vorlage umfasst die Routingfunktionalität. Gehen Sie davon aus, dass das Routing vorhanden und den Benutzern vertraut ist.

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

Es wird empfohlen, dass Sie einen versiegelten konkreten Typ aus einem Aufruf an MapMyFramework(...) zurückgeben, der IEndpointConventionBuilder implementiert. Die meisten Map...-Methoden in Frameworks folgen diesem Muster. Die IEndpointConventionBuilder-Schnittstelle:

  • Ermöglicht das Zusammensetzen von Metadaten.
  • Wird von verschiedenen Erweiterungsmethoden angesteuert.

Wenn Sie Ihren eigenen Typ deklarieren, können Sie dem Generator Ihre eigene frameworkspezifische Funktionalität hinzufügen. Es ist in Ordnung, einen vom Framework deklarierten Generator zu umschließen und Aufrufe an ihn weiterzuleiten.

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

Ziehen Sie in Betracht, Ihre eigene EndpointDataSource-Klasse zu schreiben. Die EndpointDataSource-Klasse vom primitiven Typ auf niedriger Ebene eignet sich zum Deklarieren und Aktualisieren einer Sammlung von Endpunkten. EndpointDataSource ist eine leistungsstarke API, die von Controllern und Razor Pages verwendet wird.

Die Routingtests haben ein grundlegendes Beispiel für eine Datenquelle, die nicht aktualisiert wird.

ERWÄGEN Sie die Implementierung von GetGroupedEndpoints. Dadurch erhalten Sie vollständige Kontrolle über ausgeführte Gruppenkonventionen und die endgültigen Metadaten für die gruppierten Endpunkte. Dies ermöglicht z. B. benutzerdefinierten EndpointDataSource Implementierungen das Ausführen von Endpunktfiltern, die Gruppen hinzugefügt werden.

Versuchen Sie nicht, eine EndpointDataSource-Klasse standardmäßig zu registrieren. Fordern Sie die Benutzer auf, Ihr Framework in UseEndpoints zu registrieren. Der Grundgedanke beim Routing ist, dass standardmäßig nichts enthalten ist, und dass die Endpunkte bei UseEndpoints registriert werden müssen.

Erstellen von in das Routing integrierter Middleware

Ziehen Sie in Betracht, Metadatentypen als Schnittstelle zu definieren.

Ermöglichen Sie, Metadatentypen als Attribut für Klassen und Methoden zu verwenden.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Frameworks wie Controller und Razor Pages unterstützen die Anwendung von Metadatenattributen auf Typen und Methoden. Für das Deklarieren von Metadatentypen gilt:

  • Machen Sie diese als Attribute zugänglich.
  • Die meisten Benutzer sind mit der Anwendung von Attributen vertraut.

Durch die Deklaration eines Metadatentyps als Schnittstelle wird die Flexibilität zusätzlich erhöht:

  • Schnittstelle können zusammengesetzt werden.
  • Entwickler können ihre eigenen Typen deklarieren, die mehrere Richtlinien kombinieren.

Ermöglichen Sie, Metadaten außer Kraft zu setzen, wie in folgendem Beispiel gezeigt:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

Die beste Möglichkeit, diese Richtlinien zu befolgen, besteht darin, die Definition von Markermetadaten zu vermeiden:

  • Suchen Sie nicht nur nach dem Vorhandensein eines Metadatentyps.
  • Definieren Sie eine Eigenschaft für die Metadaten, und überprüfen Sie die Eigenschaft.

Die Metadatensammlung ist geordnet und unterstützt das Außerkraftsetzen nach Priorität. Im Fall von Controllern sind Metadaten für die Aktionsmethode am spezifischsten.

Nutzen Sie Middleware mit und ohne Routing.

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

Ein gutes Beispiel für diese Vorgabe ist die UseAuthorization-Middleware. Mithilfe der Autorisierungsmiddleware können Sie eine Fallbackrichtlinie einbauen. Die Fallbackrichtlinie gilt, falls angegeben, für beide:

  • Endpunkte ohne angegebene Richtlinie.
  • Anforderungen, die nicht mit einem Endpunkt übereinstimmen.

Dies macht die Autorisierungsmiddleware außerhalb des Routingkontexts sehr nützlich. Die Autorisierungsmiddleware kann für die traditionelle Middlewareprogrammierung verwendet werden.

Debugdiagnose

Legen Sie für eine ausführliche Routingdiagnoseausgabe Logging:LogLevel:Microsoft auf Debug fest. Legen Sie in der Entwicklungsumgebung die Protokollebene in appsettings.Development.json fest:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Zusätzliche Ressourcen

Das Routing wird für das Abgleichen von HTTP-Anforderungen und das Verteilen an ausführbare Endpunkte der App eingesetzt. Endpunkte sind die Einheiten des ausführbaren Codes für die Anforderungsverarbeitung in der App. Endpunkte werden in der App definiert und beim Start der App konfiguriert. Beim Endpunktabgleich können Werte aus der Anforderungs-URL extrahiert und für die Verarbeitung der Anforderung bereitgestellt werden. Mithilfe von Endpunktinformationen aus der App lassen sich durch das Routing URLs generieren, die Endpunkten zugeordnet werden.

Apps können das Routing mit folgenden Funktionen konfigurieren:

In diesem Artikel werden die grundlegenden Details zum ASP.NET Core-Routing beschrieben. Informationen zur Routingkonfiguration finden Sie wie folgt:

Routinggrundlagen

Der folgende Code veranschaulicht ein einfaches Beispiel für das Routing:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Das vorherige Beispiel enthält einen einzelnen Endpunkt unter Verwendung der MapGet-Methode:

  • Wenn eine GET-HTTP-Anforderung an die Stamm-URL /gesendet wird:
    • Der Anforderungsdelegat wird ausgeführt.
    • Hello World! wird in die HTTP-Antwort geschrieben.
  • Wenn die Anforderungsmethode nicht GET bzw. die Stamm-URL nicht / ist, gibt es keinen Routenabgleich und es wird ein HTTP-404-Fehler zurückgegeben.

Beim Routing wird ein Middlewarepaar verwendet, das durch UseRouting und UseEndpoints registriert wird:

  • UseRouting fügt der Middlewarepipeline einen Routenabgleich hinzu. Diese Middleware prüft die in der App definierten Endpunkte und wählt anhand der Anforderung die beste Übereinstimmung aus.
  • UseEndpoints fügt der Middlewarepipeline die Endpunktausführung hinzu. Dabei wird der mit dem ausgewählten Endpunkt verknüpfte Delegat ausgeführt.

Apps müssen UseRouting oder UseEndpoints normalerweise nicht aufrufen. WebApplicationBuilder konfiguriert eine Middlewarepipeline, die die in Program.cs hinzugefügte Middleware mit UseRouting und UseEndpoints umschließt. Apps können jedoch die Reihenfolge ändern, in der UseRouting und UseEndpoints ausgeführt werden, indem sie diese Methoden explizit aufrufen. Der folgende Code ruft beispielsweise UseRouting explizit auf:

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

Für den Code oben gilt:

  • Mit dem Aufruf von app.Use wird eine benutzerdefinierte Middleware registriert, die am Anfang der Pipeline ausgeführt wird.
  • Der Aufruf von UseRouting konfiguriert die Middleware für den Routenabgleich zur Ausführung nach der benutzerdefinierten Middleware.
  • Der mit MapGet registrierte Endpunkt wird am Ende der Pipeline ausgeführt.

Wenn das vorhergehende Beispiel keinen Aufruf an UseRouting enthält, wird die benutzerdefinierte Middleware nach der Middleware zum Routenabgleich ausgeführt.

Endpunkte

Die MapGet-Methode wird verwendet, um einen Endpunkt zu definieren. Ein Endpunkt kann Folgendes sein:

  • Ausgewählt, indem die URL und die HTTP-Methode abgeglichen werden.
  • Ausgeführt, indem ein Delegat ausgeführt wird.

Endpunkte, die von der App zugeordnet und ausgeführt werden können, sind in UseEndpoints konfiguriert. Mit MapGet, MapPost und ähnlichen Methoden werden beispielsweise Anforderungsdelegate mit dem Routingsystem verbunden. Zudem können weitere Methoden zur Verbindung von ASP.NET Core-Frameworkfunktionen mit dem Routingsystem verwendet werden:

Das folgende Beispiel zeigt das Routing mit einer anspruchsvolleren Routenvorlage:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

Die Zeichenfolge /hello/{name:alpha} ist eine Routenvorlage. Eine Routenvorlage, um zu konfigurieren, wie der Endpunkt abgeglichen wird. In diesem Fall gleicht die Vorlage Folgendes ab:

  • Eine URL wie /hello/Docs
  • Alle URL-Pfade, die mit /hello/ beginnen, gefolgt von einer Sequenz alphabetischer Zeichen. :alpha wendet eine Routeneinschränkung an, die nur alphabetische Zeichen abgleicht. Routeneinschränkungen werden weiter unten in diesem Artikel erläutert.

Das zweite Segment des URL-Pfads, {name:alpha}:

Das folgende Beispiel zeigt das Routing mit Integritätsprüfungen und Autorisierung:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

Im vorherigen Beispiel wird veranschaulicht, wie Sie:

  • Die Autorisierungsmiddleware für das Routing verwenden.
  • Endpunkte zum Konfigurieren des Autorisierungsverhaltens verwendet werden können.

Der MapHealthChecks-Aufruf fügt einen Endpunkt für eine Integritätsprüfung hinzu. Durch die Verkettung von RequireAuthorization mit diesem Aufruf wird eine Autorisierungsrichtlinie an den Endpunkt angefügt.

Der Aufruf von UseAuthentication und UseAuthorization wird die Authentifizierungs- und Autorisierungsmiddleware hinzugefügt. Diese Middleware wird zum Ausführen folgender Aktionen zwischen UseRouting und UseEndpoints platziert:

  • Anzeigen des von UseRouting ausgewählten Endpunkts.
  • Anwenden einer Autorisierungsrichtlinie vor dem Senden von UseEndpoints an den Endpunkt.

Endpunktmetadaten

Im vorangehenden Beispiel gibt es zwei Endpunkte, aber nur dem für die Integritätsprüfung ist eine Autorisierungsrichtlinie angefügt. Wenn die Anforderung mit dem Endpunkt der Integritätsprüfung, /healthz, übereinstimmt, wird eine Autorisierungsprüfung durchgeführt. Dadurch wird veranschaulicht, dass Endpunkten zusätzliche Daten zugeordnet werden können. Diese zusätzlichen Daten werden als Metadaten des Endpunkts bezeichnet:

  • Die Metadaten lassen sich von routingfähiger Middleware verarbeiten.
  • Die Metadaten können einen beliebigen .NET-Typ aufweisen.

Routingkonzepte

Durch Hinzufügen des effizienten Endpunkt-Konzepts stellt das Routingsystem eine Ergänzung der Middlewarepipeline dar. Endpunkte stehen für Funktionseinheiten der App, die sich in Bezug auf Routing, Autorisierung und die Anzahl der ASP.NET Core-Systeme voneinander unterscheiden.

ASP.NET Core-Endpunktdefinition

Ein ASP.NET Core-Endpunkt ist:

Der folgende Code zeigt, wie der Endpunkt, der mit der aktuellen Anforderung übereinstimmt, abgerufen und geprüft werden kann:

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

Der Endpunkt, falls ausgewählt, kann aus dem HttpContext-Element abgerufen werden. Seine Eigenschaften können geprüft werden. Endpunktobjekte sind unveränderlich und können nach der Erstellung nicht mehr geändert werden. Der häufigste Typ des Endpunkts ist eine RouteEndpoint-Klasse. RouteEndpoint enthält Informationen, die eine Auswahl durch das Routingsystem ermöglichen.

Im vorangehenden Code wird mit app.Use eine Middleware inline konfiguriert.

Der folgende Code zeigt, dass es, je nachdem, wo app.Use in der Pipeline aufgerufen wird, möglicherweise keinen Endpunkt gibt:

// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

Im vorangehenden Beispiel werden Console.WriteLine-Anweisungen hinzugefügt, die anzeigen, ob ein Endpunkt ausgewählt wurde oder nicht. Aus Gründen der Übersichtlichkeit wird in dem Beispiel dem bereitgestellten /-Endpunkt ein Anzeigename zugewiesen.

Das vorherige Beispiel enthält auch Aufrufe an UseRouting und UseEndpoints, um genau zu steuern, wann diese Middleware in der Pipeline ausgeführt wird.

Wenn Sie diesen Code mit einer URL / ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Wenn Sie diesen Code mit einer anderen URL ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Diese Ausgabe zeigt Folgendes:

  • Der Endpunkt ist immer NULL, bevor UseRouting aufgerufen wird.
  • Wenn eine Übereinstimmung gefunden wird, ist der Endpunkt zwischen UseRouting und UseEndpoints ungleich NULL.
  • Die UseEndpoints-Middleware ist eine Terminalmiddleware, wenn eine Übereinstimmung gefunden wird. Terminalmiddleware wird weiter unten in diesem Artikel definiert.
  • Die Middleware nach UseEndpoints wird nur ausgeführt, wenn keine Übereinstimmung gefunden wird.

Die UseRouting-Middleware verwendet die SetEndpoint-Methode, um den Endpunkt an den aktuellen Kontext anzufügen. Es ist möglich, die UseRouting-Middleware durch benutzerdefinierte Logik zu ersetzen und dennoch die Vorteile durch die Verwendung von Endpunkten zu nutzen. Endpunkte befinden sich auf niedriger Ebene, wie Middleware, und sind nicht an die Routingimplementierung gekoppelt. Die meisten Apps müssen UseRouting nicht durch eigene Logik ersetzen.

Die UseEndpoints-Middleware ist so konzipiert, dass Sie zusammen mit der UseRouting-Middleware verwendet werden kann. Die Hauptlogik zum Ausführen eines Endpunkts ist nicht kompliziert. Mit GetEndpoint können Sie einen Endpunkt abrufen und dann dessen RequestDelegate-Eigenschaft aufrufen.

Der folgende Code veranschaulicht, wie Middleware das Routing beeinflussen oder darauf reagieren kann:

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

Im vorherigen Beispiel werden zwei wichtige Konzepte dargestellt:

  • Die Middleware kann vor UseRouting ausgeführt werden, um die Daten zu ändern, auf denen das Routing basiert.
  • Die Middleware kann zwischen UseRouting und UseEndpoints ausgeführt werden, um die Ergebnisse des Routings vor der Ausführung des Endpunkts zu verarbeiten.
    • Middleware, die zwischen UseRouting und UseEndpoints ausgeführt wird:
      • Überprüft in der Regel die Metadaten, um die Endpunkte zu ermitteln.
      • Trifft häufig Sicherheitsentscheidungen, wie UseAuthorization und UseCors.
    • Durch die Kombination aus Middleware und Metadaten ist es möglich, für jeden einzelnen Endpunkt Richtlinien zu konfigurieren.

Der vorangehende Code zeigt ein Beispiel für eine benutzerdefinierte Middleware, die entpunktbezogene Richtlinien unterstützt. Die Middleware schreibt ein Überwachungsprotokoll für den Zugriff auf vertrauliche Daten in der Konsole. Die Middleware kann so konfiguriert werden, dass ein Endpunkt mit den RequiresAuditAttribute-Metadaten überwacht wird. In diesem Beispiel wird ein Opt-In-Muster veranschaulicht, bei dem nur Endpunkte überwacht werden, die als vertraulich markiert sind. Es ist möglich, diese Logik umgekehrt zu definieren, indem beispielsweise alles geprüft wird, was nicht als sicher markiert ist. Das Endpunktmetadaten-System ist flexibel. Diese Logik lässt sich für jeden Anwendungsfall passend schreiben.

Der vorherige Beispielcode soll die grundlegenden Konzepte von Endpunkten veranschaulichen. Das Beispiel ist nicht für Produktionsumgebungen vorgesehen. Eine vollständigere Version einer Middleware für Überwachungsprotokolle würde Folgendes bieten:

  • Protokollieren in einer Datei oder einer Datenbank.
  • Einschließen von Details wie Benutzer, IP-Adresse, Name des vertraulichen Endpunkts usw.

Die Metadaten der Überwachungsrichtlinie RequiresAuditAttribute sind als Attribute definiert, um die Verwendung mit klassenbasierten Frameworks wie Controllern und SignalR zu erleichtern. Bei Verwendung von Route-zu-Code:

  • Metadaten werden an eine Generator-API angefügt.
  • Klassenbasierte Frameworks enthalten beim Erstellen von Endpunkten alle Attribute der entsprechenden Methode und Klasse.

Die bewährten Methoden für Metadatentypen sind, sie entweder als Schnittstellen oder als Attribute zu definieren. Schnittstellen und Attribute ermöglichen die Wiederverwendung von Code. Das Metadatensystem ist flexibel und weist keine Einschränkungen auf.

Vergleichen von Terminalmiddleware mit Routing

Im folgenden Beispiel werden sowohl Terminalmiddleware als auch Routing veranschaulicht:

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Beim in Approach 1: gezeigten Stil von Middleware handelt es sich um Terminalmiddleware. Sie wird als Terminalmiddleware bezeichnet, da sie einen Abgleich durchgeführt.

  • Der Abgleich im vorangehenden Beispiel ist Path == "/" für die Middleware und Path == "/Routing" für das Routing.
  • Bei einem erfolgreichen Abgleich führt sie bestimmte Funktionen aus und kehrt zurück, anstatt die next-Middleware aufzurufen.

Sie wird „Terminal Middleware“ genannt, da sie die Suche beendet, einige Funktionen ausführt und dann zurückkehrt.

In der folgenden Liste werden Terminalmiddleware und Routing verglichen:

  • Beide Ansätze ermöglichen das Beenden der Verarbeitungspipeline:
    • Die Middleware beendet die Pipeline, indem Sie zurückkehrt, anstatt next aufzurufen.
    • Endpunkte beenden immer die Verarbeitung.
  • Terminalmiddleware ermöglicht die Positionierung der Middleware an einer beliebigen Stelle in der Pipeline:
    • Endpunkte werden an der Position von UseEndpoints ausgeführt.
  • Mit Terminalmiddleware kann beliebiger Code beendet werden, wenn die Middleware übereinstimmt:
    • Ein benutzerdefinierter Routenabgleichscode kann sehr umständlich und schwierig zu schreiben sein.
    • Das Routing bietet unkomplizierte Lösungen für typische Apps. Die meisten Apps erfordern keinen benutzerdefinierten Routenabgleichscode.
  • Endpunkte haben eine Schnittstelle mit Middleware wie UseAuthorization und UseCors.
    • Die Verwendung einer Terminalmiddleware mit UseAuthorization oder UseCors erfordert eine manuelle Verknüpfung mit dem Autorisierungssystem.

Ein Endpunkt definiert beides:

  • Einen Delegaten zum Verarbeiten von Anforderungen.
  • Eine Sammlung beliebiger Metadaten. Die Metadaten werden zur Implementierung von übergreifenden Belangen verwendet, die auf Richtlinien und der Konfiguration basieren, die den einzelnen Endpunkten angefügt sind.

Terminalmiddleware kann sehr nützlich sein, erfordert aber möglicherweise auch:

  • Einen großen Codierungs- und Testaufwand.
  • Eine manuelle Integration in andere Systeme für die gewünschte Flexibilität.

Ziehen Sie daher zunächst die Integration von Routingfunktionen in Betracht, bevor Sie damit beginnen, Terminalmiddleware zu schreiben.

Vorhandene Terminalmiddleware, die in Map oder MapWhen integriert ist, kann in der Regel in einen routingfähigen Endpunkt umgewandelt werden. MapHealthChecks zeigt das Muster für eine Routinglösung:

  • Schreiben Sie eine Erweiterungsmethode in der IEndpointRouteBuilder-Schnittstelle.
  • Erstellen Sie eine geschachtelte Middlewarepipeline mit CreateApplicationBuilder.
  • Fügen Sie die Middleware an die neue Pipeline an. In diesem Fall UseHealthChecks.
  • Verwenden Sie Build, um die Middlewarepipeline in einem RequestDelegate-Delegaten zu erstellen.
  • Rufen Sie Map auf, und stellen Sie die neue Middlewarepipeline bereit.
  • Geben Sie das Generatorobjekt zurück, das von Map aus der Erweiterungsmethode bereitgestellt wurde.

Im folgenden Code ist die Verwendung von MapHealthChecks gezeigt:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

Das vorangehende Beispiel zeigt, warum das Zurückgeben des Generatorobjekts wichtig ist. Wenn das Generatorobjekt zurückgegeben wird, kann der App-Entwickler Richtlinien konfigurieren, z. B. die Autorisierung für den Endpunkt. In diesem Beispiel ist die Middleware für Integritätsprüfungen nicht direkt in das Autorisierungssystem integriert.

Das Metadatensystem wurde als Antwort auf die Probleme erstellt, die von Erweiterbarkeitsautoren mithilfe von Terminalmiddleware aufgetreten sind. Für jede Middleware ist es problematisch, deren eigene Integration in das Autorisierungssystem umzusetzen.

URL-Zuordnung

  • Bei diesem Prozess werden eingehende Anforderungen durch Routing mit einem Endpunkt abgeglichen.
  • Basiert auf Daten im URL-Pfad und den URL-Headern.
  • Kann erweitert werden, um beliebige Daten in der Anforderung zu überprüfen.

Wenn eine Routingmiddleware ausgeführt wird, legt sie ein Endpoint-Element fest und leitet Werte an eine Anforderungsfunktion in der HttpContext-Klasse der aktuellen Anforderung weiter:

  • Durch Aufrufen von HttpContext.GetEndPoint wird der Endpunkt abgerufen.
  • HttpRequest.RouteValues ruft die Sammlung der Routenwerte ab.

Middleware wird nach der Routingmiddleware ausgeführt und kann den Endpunkt erkennen und Maßnahmen ergreifen. So kann beispielsweise eine Autorisierungsmiddleware die Erfassung der Metadaten des Endpunkts für eine Autorisierungsrichtlinie abfragen. Nachdem die gesamte Middleware in der Anforderungsverarbeitungspipeline ausgeführt wurde, wird der Delegat des ausgewählten Endpunkts aufgerufen.

Das Routingsystem ist beim Endpunktrouting für alle Weiterleitungsentscheidungen zuständig. Da die Middleware Richtlinien auf der Grundlage des ausgewählten Endpunkts anwendet, ist Folgendes wichtig:

  • Alle Entscheidungen, die sich auf die Verteilung oder die Anwendung von Sicherheitsrichtlinien auswirken können, werden im Routingsystem getroffen.

Warnung

Wenn für Abwärtskompatibilität ein Controller- oder Razor Pages-Endpunktdelegat ausgeführt wird, werden die Eigenschaften von RouteContext.RouteData auf Grundlage der bisher verarbeiteten Anforderungen auf entsprechende Werte festgelegt.

Der Typ RouteContext wird in einer zukünftigen Version als veraltet markiert:

  • Migrieren Sie RouteData.Values zu HttpRequest.RouteValues.
  • Migrieren Sie RouteData.DataTokens, um IDataTokensMetadata aus den Endpunktmetadaten abzurufen.

Der URL-Abgleich erfolgt in mehreren Phasen und kann konfiguriert werden. In jeder Phase werden mehrere Übereinstimmungen ausgegeben. Diese Übereinstimmungen lassen sich in der nächsten Phase weiter eingrenzen. Die Routingimplementierung garantiert keine Verarbeitungsreihenfolge für übereinstimmende Endpunkte. Alle möglichen Übereinstimmungen werden gleichzeitig verarbeitet. Für die URL-Abgleichsphasen gilt folgende Reihenfolge. ASP.NET Core:

  1. Verarbeitet den URL-Pfad mit mehreren Endpunkten und ihren Routenvorlagen und sammelt alle Übereinstimmungen.
  2. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den angewendeten Routeneinschränkungen passen.
  3. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den MatcherPolicy-Instanzen passen.
  4. Verwendet EndpointSelector, um eine abschließende Entscheidung anhand der vorangehenden Liste zu treffen.

Die Liste der Endpunkte wird entsprechend den folgenden Punkten priorisiert:

Alle übereinstimmenden Endpunkte werden in jeder Phase verarbeitet, bis EndpointSelector erreicht ist. EndpointSelector stellt die abschließende Phase dar. Darin wird der Endpunkt mit der höchsten Priorität aus den Übereinstimmungen als beste Übereinstimmung ausgewählt. Wenn es andere Übereinstimmungen mit derselben Priorität wie die beste Übereinstimmung gibt, wird ein Ausnahmefehler wegen einer nicht eindeutigen Übereinstimmung ausgelöst.

Die Routenpriorität wird anhand einer spezifischeren Routenvorlage berechnet, der eine höhere Priorität eingeräumt wird. Dies wird z. B. anhand der Vorlagen /hello und /{message} deutlich:

  • Beide stimmen mit dem URL-Pfad /hello überein.
  • /hello ist spezifischer und hat daher höhere Priorität.

Im Allgemeinen eignet sich die Routenpriorität gut, um die beste Übereinstimmung für die in der Praxis verwendeten URL-Schemata zu finden. Verwenden Sie Order nur bei Bedarf, um eine Mehrdeutigkeit zu vermeiden.

Aufgrund der Erweiterungsmöglichkeiten, die das Routing bietet, kann das Routingsystem die mehrdeutigen Routen nicht im Voraus berechnen. Betrachten Sie ein Beispiel wie die Routenvorlagen /{message:alpha} und /{message:int}:

  • Die alpha-Einschränkung gleicht nur alphabetische Zeichen ab.
  • Die int-Einschränkung gleicht nur Zahlen ab.
  • Diese Vorlagen haben die gleiche Routenpriorität, aber es gibt keine einzige URL, auf die sie beide zutreffen.
  • Wenn das Routingsystem beim Starten einen Mehrdeutigkeitsfehler gemeldet hat, würde dieser den gültigen Anwendungsfall blockieren.

Warnung

Die Reihenfolge der Vorgänge in UseEndpoints wirkt sich nicht auf das Routingverhalten aus, mit einer Ausnahme. MapControllerRoute und MapAreaRoute weisen ihren Endpunkten automatisch einen Reihenfolgenwert zu, basierend auf der Reihenfolge, in der sie aufgerufen werden. Dadurch wird das Langzeitverhalten von Controllern simuliert, ohne dass das Routingsystem die gleichen Garantien bietet wie ältere Routingimplementierungen.

Endpunktrouting in ASP.NET Core:

  • Weist nicht das Konzept von Routen auf.
  • Bietet keine garantierte Reihenfolge. Alle Endpunkte werden gleichzeitig verarbeitet.

Routenvorlagenpriorität und Reihenfolge der Endpunktauswahl

Die Routenvorlagenpriorität ist ein System, bei dem jeder Routenvorlage ein Wert zugewiesen wird, je nachdem, wie spezifisch diese ist. Routenvorlagenpriorität:

  • Verhindert, dass die Reihenfolge der Endpunkte häufig angepasst werden muss.
  • Versucht, die allgemeinen Erwartungen an das Routingverhalten abzugleichen.

Dies wird z. B. anhand der Vorlagen /Products/List und /Products/{id} deutlich. Es wäre begründet, anzunehmen, dass /Products/List eine bessere Übereinstimmung als /Products/{id} für den URL-Pfad /Products/List ist. Dies funktioniert, weil das Literalsegment /List eine höhere Priorität als das Parametersegment /{id} hat.

Wie die Priorisierung im Einzelnen funktioniert, ist an die Definition der Routenvorlagen gekoppelt:

  • Vorlagen mit mehr Segmenten sind in der Regel spezifischer.
  • Ein Segment mit Literaltext gilt als spezifischer als ein Parametersegment.
  • Ein Parametersegment mit einer Einschränkung gilt als spezifischer als ein Parametersegment ohne Einschränkung.
  • Ein komplexes Segment wird als genauso spezifisch betrachtet wie ein Parametersegment mit einer Einschränkung.
  • Catch-All-Parameter, die am wenigsten spezifisch sind. Unter catch-all im Abschnitt Routenvorlagen finden Sie wichtige Informationen zu catch-all-Routen.

Konzepte zur URL-Generierung

URL-Generierung:

  • Der Prozess, bei dem durch Routing ein URL-Pfad basierend auf mehreren Routenwerten erstellt wird.
  • Sie ermöglicht eine logische Trennung zwischen den Endpunkten und den URLs, die auf diese zugreifen.

Das Endpunktrouting umfasst die API zur Linkgenerierung (LinkGenerator). LinkGenerator ist ein Singleton-Dienst, der in DI verfügbar ist. Die LinkGenerator-API kann außerhalb des Kontexts einer ausgeführten Anforderung verwendet werden. Mvc.IUrlHelper und Szenarios, die von IUrlHelper abhängig sind (z. B. Taghilfsprogramme, HTML-Hilfsprogramme und Aktionsergebnisse), verwenden die LinkGenerator-API, um entsprechende Funktionen bereitzustellen.

Die API zur Linkgenerierung wird von Konzepten wie Adressen und Adressschemas unterstützt. Sie können mithilfe eines Adressschemas die Endpunkte bestimmen, die bei der Linkgenerierung berücksichtigt werden sollen. Beispielsweise werden Routennamen und Routenwerte als Adressschemas implementiert. Diese Szenarios kennen viele Benutzer von Controllern und Razor Pages.

Die API zur Linkgenerierung kann Controller und Razor Pages über die folgenden Erweiterungsmethoden miteinander verknüpfen:

Beim Überladen dieser Methoden werden Argumente akzeptiert, die den HttpContext umfassen. Diese Methoden sind zwar in funktionaler Hinsicht äquivalent zu Url.Action und Url.Page, bieten aber zusätzliche Flexibilität und Optionen.

Die GetPath*-Methoden sind Url.Action und Url.Page in der Hinsicht ähnlich, dass sie einen URI mit einem absoluten Pfad generieren. Die GetUri*-Methoden generieren immer einen absoluten URI mit einem Schema und einem Host. Die Methoden, die einen HttpContext akzeptieren, generieren im Kontext der ausgeführten Anforderung einen URI. Die Umgebungsroutenwerte, der URL-basierte Pfad, das Schema und der Host von der ausführenden Anforderung werden so lange verwendet, bis sie außer Kraft gesetzt werden.

LinkGenerator wird mit einer Adresse aufgerufen. Ein URI wird in zwei Schritten generiert:

  1. Eine Adresse wird an eine Liste von Endpunkten gebunden, die der Adresse zugeordnet werden können.
  2. Jedes RoutePattern eines Endpunkts wird bewertet, bis ein Routenmuster gefunden wird, das den angegebenen Werten zugeordnet werden kann. Die daraus resultierende Ausgabe wird mit URI-Teilen kombiniert, die für die API zur Linkgenerierung bereitgestellt wird, und zurückgegeben.

Die von LinkGenerator bereitgestellten Methoden unterstützen die Standardfunktionen zur Generierung von Links für jeden beliebigen Adresstypen. Am praktischsten ist es, die API zur Linkgenerierung mit Erweiterungsmethoden zu verwenden, die Vorgänge für einen bestimmten Adresstypen ausführen:

Erweiterungsmethode Beschreibung
GetPathByAddress Generiert einen URI mit einem absoluten Pfad, der auf den angegebenen Werten basiert.
GetUriByAddress Generiert einen absoluten URI, der auf den angegebenen Werten basiert.

Warnung

Beachten Sie die folgenden Implikationen zum Aufrufen von LinkGenerator-Methoden:

  • Verwenden Sie GetUri*-Erweiterungsmethoden in App-Konfigurationen, die den Host-Header von eingehenden Anforderungen nicht überprüfen, mit Bedacht. Wenn der Host-Header von eingehenden Anforderungen nicht überprüft wird, können nicht vertrauenswürdige Anforderungseingaben zurück an den Client in URIs einer Ansicht bzw. Seite zurückgesendet werden. Es wird empfohlen, dass alle Produktions-Apps ihren Server so konfigurieren, dass der Host-Header auf bekannte gültige Werte überprüft wird.

  • Verwenden Sie LinkGenerator in Kombination mit Map oder MapWhen in Middleware mit Bedacht. Map* ändert den Basispfad der ausgeführten Anforderung. Dies beeinflusst die Ausgabe der Linkgenerierung. Für alle LinkGenerator-APIs ist die Angabe eines Basispfads zulässig. Geben Sie einen leeren Basispfad an, um die Auswirkungen von Map* auf die Linkgenerierung rückgängig zu machen.

Middlewarebeispiel

Im folgenden Beispiel verwendet eine Middleware die LinkGenerator-API, um eine Verknüpfung zu einer Aktionsmethode herzustellen, die Speicherprodukte aufführt. Sie können für jede beliebige Klasse in einer App die API zur Linkgenerierung verwenden, indem Sie diese in eine Klasse einfügen und GenerateLink aufrufen:

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}

Routenvorlagen

Token in {} definieren Routenparameter, die beim Abgleich der Route gebunden werden. In einem Routensegment können mehrere Routenparameter definiert werden, müssen aber durch einen Literalwert getrennt werden. Beispiel:

{controller=Home}{action=Index}

ist keine gültige Route, da sich zwischen {controller} und {action} kein Literalwert befindet. Routenparameter müssen einen Namen besitzen und können zusätzliche Attribute aufweisen.

Eine Literalzeichenfolge, die nicht den Routenparametern entspricht (z.B. {id}), muss zusammen mit dem Pfadtrennzeichen / mit dem URL-Text übereinstimmen. Beim Abgleich von Text wird nicht zwischen Groß-/Kleinbuchstaben unterschieden, und die Übereinstimmung basiert auf der decodierten Repräsentation des URL-Pfads. Damit das Trennzeichen ({ oder }) der Routenparameter-Literalzeichenfolge bei einem Abgleich gefunden wird, muss es doppelt vorhanden sein, was einem Escapezeichen entspricht. Beispielsweise {{ oder }}.

Sternchen * oder Doppelsternchen **:

  • Kann als Präfix für einen Routenparameter verwendet werden, um an den rest des URI zu binden.
  • Werden Catch-All-Parameter genannt. Zum Beispiel blog/{**slug}:
    • Entspricht einem beliebigen URI, der mit blog/ beginnt und einem beliebigen Wert folgt.
    • Der Wert nach blog/ wird dem Slug-Routenwert zugewiesen.

Warnung

Ein catch-all-Parameter kann aufgrund eines Fehlers beim Routing nicht ordnungsgemäß mit Routen übereinstimmen. Apps, die von diesem Fehler betroffen sind, weisen die folgenden Merkmale auf:

  • Eine catch-all-Route, zum Beispiel {**slug}"
  • Die catch-all-Route kann nicht mit Anforderungen abgeglichen werden, die abgeglichen werden sollen.
  • Durch das Entfernen anderer Routen funktioniert die catch-all-Route.

Weitere Beispiele zu diesem Fehler finden Sie in den GitHub-Issues 18677 und 16579.

Eine Opt-in-Behebung für diesen Fehler ist im .NET Core 3.1.301 SDK und höher enthalten. Der folgende Code legt einen internen Switch fest, mit dem dieser Fehler behoben wird:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Durch Catch-All-Parameter können auch leere Zeichenfolgen gefunden werden.

Der Catch-All-Parameter schützt die entsprechenden Zeichen (Escaping), wenn die Route verwendet wird, um eine URL, einschließlich Pfadtrennzeichen (/) zu generieren. Z.B. generiert die Route foo/{*path} mit den Routenwerten { path = "my/path" }foo/my%2Fpath. Beachten Sie den umgekehrten Schrägstrich mit Escapezeichen. Um Trennzeichen für Roundtrips einsetzen zu können, verwenden Sie das Routenparameterpräfix **. Die Route foo/{**path} mit { path = "my/path" } generiert foo/my/path.

Bei einem URL-Muster, durch das ein Dateiname mit einer optionalen Erweiterung erfasst werden soll, sind noch weitere Aspekte zu berücksichtigen. Dies wird z.B. anhand der Vorlage files/{filename}.{ext?} deutlich. Wenn sowohl für filename als auch für ext Werte vorhanden sind, werden beide Werte angegeben. Wenn nur für filename ein Wert in der URL vorhanden ist, wird für die Route eine Übereinstimmung ermittelt, da der nachstehende Punkt (.) optional ist. Für die folgenden URLs wird eine Übereinstimmung für die Route ermittelt:

  • /files/myFile.txt
  • /files/myFile

Routenparameter können über mehrere Standardwerte verfügen, die nach dem Parameternamen angegeben werden und durch ein Gleichheitszeichen (=) voneinander getrennt werden. Mit {controller=Home} wird beispielsweise Home als Standardwert für controller definiert. Der Standardwert wird verwendet, wenn kein Wert in der Parameter-URL vorhanden ist. Routenparameter sind optional, wenn am Ende des Parameternamens ein Fragezeichen (?) angefügt wird. Beispielsweise id?. Zwischen optionalen Werten und Standardroutenparametern besteht folgender Unterschied:

  • Ein Routenparameter mit einem Standardwert erzeugt immer einen Wert.
  • Ein optionaler Parameter hat nur dann einen Wert, wenn ein Wert von der Anforderungs-URL bereitgestellt wird.

Routenparameter können Einschränkungen aufweisen, die mit dem gebundenen Routenwert der URL übereinstimmen müssen. Eine Inline-Einschränkung für einen Routenparameter geben Sie an, indem Sie hinter dem Namen des Routenparameters einen Doppelpunkt (:) und einen Einschränkungsnamen hinzufügen. Wenn für die Einschränkung Argumente erforderlich sind, werden diese nach dem Einschränkungsnamen in Klammern ((...)) eingeschlossen. Mehrere Inline-Einschränkungen können festgelegt werden, indem ein weiterer Doppelpunkt (:) und Einschränkungsname hinzugefügt werden.

Der Einschränkungsname und die Argumente werden dem IInlineConstraintResolver-Dienst übergeben, wodurch eine Instanz von IRouteConstraint für die URL-Verarbeitung erstellt werden kann. In der Routenvorlage blog/{article:minlength(10)} wird beispielsweise die Einschränkung minlength mit dem Argument 10 festgelegt. Weitere Informationen zu Routeneinschränkungen und eine Liste der vom Framework bereitgestellten Einschränkungen finden Sie im Abschnitt Routeneinschränkungen.

Routenparameter können darüber hinaus über Parametertransformatoren verfügen. Diese wandeln den Wert eines Parameters beim Generieren von Links um und passen Aktionen und Seiten an URLs an. Wie Einschränkungen können auch Parametertransformatoren einem Routenparameter inline hinzugefügt werden, indem ein Doppelpunkt (:) und der Name des Transformators hinter dem Namen des Routenparameters hinzugefügt werden. In der Routenvorlage blog/{article:slugify} wird beispielsweise der Transformator slugify festgelegt. Weitere Informationen zu Parametertransformatoren finden Sie im Abschnitt Parametertransformatoren.

Die folgende Tabelle enthält Beispielvorlagen für Routen und deren Verhalten:

Routenvorlage Beispiel-URI für Übereinstimmung Der Anforderungs-URI
hello /hello Nur für den Pfad /hello wird eine Übereinstimmung ermittelt.
{Page=Home} / Eine Übereinstimmung wird ermittelt, und Page wird auf Home festgelegt.
{Page=Home} /Contact Eine Übereinstimmung wird ermittelt, und Page wird auf Contact festgelegt.
{controller}/{action}/{id?} /Products/List Stimmt mit dem Products-Controller und der List-Aktion überein.
{controller}/{action}/{id?} /Products/Details/123 Wird dem Controller Products und der Aktion Details zugeordnet, bei der id auf 123 festgelegt ist.
{controller=Home}/{action=Index}/{id?} / Stimmt mit dem Home-Controller und der Index-Methode überein. id wird ignoriert.
{controller=Home}/{action=Index}/{id?} /Products Stimmt mit dem Products-Controller und der Index-Methode überein. id wird ignoriert.

Mit Vorlagen lässt sich Routing besonders leicht durchführen. Einschränkungen und Standardwerte können auch außerhalb der Routenvorlage angegeben werden.

Komplexe Segmente

Komplexe Segmente werden von rechts nach links auf eine nicht gierige Weise durch entsprechende Literaltrennzeichen verarbeitet. Beispielsweise ist [Route("/a{b}c{d}")] ein komplexes Segment. Komplexe Segmente funktionieren auf eine bestimmte Weise, die für eine erfolgreiche Verwendung verstanden werden muss. Das Beispiel in diesem Abschnitt zeigt, warum komplexe Segmente nur dann wirklich gut funktionieren, wenn der Trennzeichentext nicht innerhalb der Parameterwerte erscheint. Für komplexere Fälle ist die Verwendung eines RegEx und das anschließende manuelle Extrahieren der Werte erforderlich.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Dies ist eine Zusammenfassung der Schritte, die beim Routing mit der Vorlage /a{b}c{d} und dem URL-Pfad /abcd ausgeführt werden. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen:

  • Das erste Literal von rechts nach links ist c. Daher wird /abcd von rechts durchsucht und /ab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /ab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Es ist kein verbleibender Text und keine verbleibende Routenvorlage vorhanden. Folglich ist dies eine Übereinstimmung.

Nachfolgend ist ein Beispiel für einen negativen Fall mit derselben Vorlage /a{b}c{d} und dem URL-Pfad /aabcd. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen: Bei diesem Fall handelt es sich nicht um eine Übereinstimmung, was durch denselben Algorithmus belegt wird:

  • Das erste Literal von rechts nach links ist c. Daher wird /aabcd von rechts durchsucht und /aab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /aab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /a|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Zu diesem Zeitpunkt gibt es noch verbleibenden Text a, aber es gibt keine Routenvorlage mehr, die der Algorithmus analysieren kann, weshalb dies keine Übereinstimmung ist.

Da der übereinstimmende Algorithmus nicht gierig ist:

  • Entspricht er der kleinstmöglichen Textmenge in jedem Schritt.
  • Alle Fälle, in denen der Trennzeichenwert in den Parameterwerten angezeigt wird, stimmen nicht überein.

Reguläre Ausdrücke bieten eine viel bessere Kontrolle über das Abgleichsverhalten.

Beim gierigen Abgleich, auch als Lazy Matching bezeichnet, wird die größtmögliche Zeichenfolge abgeglichen. Beim nicht gierigen Abgleich ist dies die kürzeste Zeichenfolge.

Routing mit Sonderzeichen

Ein Routing mit Sonderzeichen kann zu unerwarteten Ergebnissen führen. Stellen Sie sich z. B. einen Controller mit der folgenden Aktionsmethode vor:

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

Wenn string id die folgenden codierten Werte enthält, können unerwartete Ergebnisse auftreten:

ASCII Codiert
/ %2F
+

Routenparameter sind nicht immer URL-decodiert. Dieses Problem wird möglicherweise in Zukunft behoben. Weitere Informationen finden Sie in diesem GitHub-Issue.

Routeneinschränkungen

Routeneinschränkungen werden angewendet, wenn eine Übereinstimmung mit der eingehenden URL gefunden wurde und der URL-Pfad in Routenwerten mit Token versehen wird. In der Regel wird mit Routeneinschränkungen der Routenwert der zugehörigen Vorlage geprüft. Dabei wird anhand einer True/False-Entscheidung bestimmt, ob der Wert gültig ist. Für einige Routeneinschränkungen werden anstelle des Routenwerts andere Daten verwendet, um zu ermitteln, ob das Routing einer Anforderung möglich ist. HttpMethodRouteConstraint kann beispielsweise auf der Grundlage des HTTP-Verbs eine Anforderung entweder annehmen oder ablehnen. Einschränkungen werden in Routinganforderungen und bei der Linkgenerierung verwendet.

Warnung

Verwenden Sie keine Einschränkungen für die Eingabeüberprüfung. Wenn Einschränkungen für die Eingabevalidierung verwendet werden, führt eine ungültige Eingabe zu einem 404-Fehler (Nicht gefunden). Eine ungültige Eingabe sollte zu einer ungültigen Anforderung (400) mit einer entsprechenden Fehlermeldung führen. Verwenden Sie Routeneinschränkungen nicht, um Eingaben für eine bestimmte Route zu überprüfen, sondern um ähnliche Routen zu unterscheiden.

In der folgenden Tabelle werden Beispiele für Routeneinschränkungen und deren zu erwartendes Verhalten beschrieben:

Einschränkung Beispiel Beispiele für Übereinstimmungen Hinweise
int {id:int} 123456789, -123456789 Für jeden Integer wird eine Übereinstimmung ermittelt.
bool {active:bool} true, FALSE Entspricht true oder false. Groß-/Kleinschreibung nicht beachten
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Entspricht einem gültigen DateTime-Wert in der invarianten Kultur. Siehe vorherige Warnung.
decimal {price:decimal} 49.99, -1,000.01 Entspricht einem gültigen decimal-Wert in der invarianten Kultur. Siehe vorherige Warnung.
double {weight:double} 1.234, -1,001.01e8 Entspricht einem gültigen double-Wert in der invarianten Kultur. Siehe vorherige Warnung.
float {weight:float} 1.234, -1,001.01e8 Entspricht einem gültigen float-Wert in der invarianten Kultur. Siehe vorherige Warnung.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Für einen gültigen Guid-Wert wird eine Übereinstimmung ermittelt.
long {ticks:long} 123456789, -123456789 Für einen gültigen long-Wert wird eine Übereinstimmung ermittelt.
minlength(value) {username:minlength(4)} Rick Die Zeichenfolge muss mindestens eine Länge von 4 Zeichen aufweisen.
maxlength(value) {filename:maxlength(8)} MyFile Die Zeichenfolge darf maximal eine Länge von 8 Zeichen aufweisen.
length(length) {filename:length(12)} somefile.txt Die Zeichenfolge muss genau 12 Zeichen aufweisen.
length(min,max) {filename:length(8,16)} somefile.txt Die Zeichenfolge muss mindestens eine Länge von 8 und darf maximal eine Länge von 16 Zeichen aufweisen.
min(value) {age:min(18)} 19 Der Integerwert muss mindestens 18 sein.
max(value) {age:max(120)} 91 Der Integerwert darf nicht größer als 120 sein.
range(min,max) {age:range(18,120)} 91 Der Integerwert muss zwischen 18 und 120 liegen.
alpha {name:alpha} Rick Die Zeichenfolge muss aus mindestens einem alphabetische Zeichen bestehen, a-z und ohne Unterscheidung zwischen Groß-/Kleinbuchstaben.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 Die Zeichenfolge muss mit dem regulären Ausdruck übereinstimmen. Weitere Informationen finden Sie unter Tipps zum Definieren eines regulären Ausdrucks.
required {name:required} Rick Hierdurch wird erzwungen, dass ein Wert, der kein Parameter ist, für die URL-Generierung vorhanden sein muss.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Auf einen einzelnen Parameter können mehrere durch Doppelpunkte getrennte Einschränkungen angewendet werden. Durch die folgende Einschränkung wird ein Parameter beispielsweise auf einen Integerwert größer oder gleich 1 beschränkt:

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Warnung

Für Routeneinschränkungen, mit denen die URL überprüft wird und die in den CLR-Typ umgewandelt werden, wird immer die invariante Kultur verwendet. Dies gilt z. B. für die Konvertierung in den CLR-Typ int oder DateTime. Diese Einschränkungen setzen voraus, dass die URL nicht lokalisierbar ist. Die vom Framework bereitgestellten Routeneinschränkungen ändern nicht die Werte, die in Routenwerten gespeichert sind. Alle Routenwerte, die aus der URL analysiert werden, werden als Zeichenfolgen gespeichert. Durch die float-Einschränkung wird beispielsweise versucht, den Routenwert in einen Gleitkommawert zu konvertieren. Mit dem konvertierten Wert wird allerdings nur überprüft, ob eine Umwandlung überhaupt möglich ist.

Reguläre Ausdrücke in Einschränkungen

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Reguläre Ausdrücke können mithilfe der regex(...)-Routeneinschränkung als Inline-Einschränkungen angegeben werden. Methoden der MapControllerRoute-Familie akzeptieren auch ein Objektliteral von Einschränkungen. Wenn dieses Formular verwendet wird, werden Zeichenfolgenwerte als reguläre Ausdrücke interpretiert.

Der folgende Code verwendet eine Inline-RegEx-Einschränkung:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

Der folgende Code verwendet ein Objektliteral, um eine RegEx-Einschränkung anzugeben:

app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });

Im ASP.NET Core-Framework wird dem Konstruktor für reguläre Ausdrücke RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant hinzugefügt. Eine Beschreibung dieser Member finden Sie unter RegexOptions.

In regulären Ausdrücken werden Trennzeichen und Token verwendet, die auch beim Routing und in der Programmiersprache C# in ähnlicher Weise verwendet werden. Token, die reguläre Ausdrücke enthalten, müssen mit einem Escapezeichen versehen werden. Wenn Sie den regulären Ausdruck ^\d{3}-\d{2}-\d{4}$ in einer Inline-Einschränkung verwenden möchten, nutzen Sie eine der folgenden Optionen:

  • Ersetzen Sie \-Zeichen in der Zeichenfolge durch \\-Zeichen in der C#-Quelldatei, um das Escapezeichen für die Zeichenfolge \ zu setzen.
  • Ausführliche Zeichenfolgeliterale

Wenn Sie Trennzeichen für Routenparameter mit Escapezeichen versehen möchten ({, }, [, ]), geben Sie jedes Zeichen im Ausdruck doppelt ein (z. B. {{, }}, [[, ]]). In der folgenden Tabelle werden reguläre Ausdrücke und Ausdrücke aufgeführt, die mit Escapezeichen versehen sind:

Regulärer Ausdruck Mit Escapezeichen versehener regulärer Ausdruck
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Beim Routing verwendete reguläre Ausdrücke beginnen oft mit dem ^-Zeichen und stellen die Startposition der Zeichenfolge dar. Die Ausdrücke enden häufig mit einem Dollarzeichen ($) und stellen das Ende der Zeichenfolge dar. Mit den Zeichen ^ und $ wird sichergestellt, dass der reguläre Ausdruck mit dem vollständigen Routenparameterwert übereinstimmt. Ohne die Zeichen ^ und $ werden mit dem regulären Ausdruck alle Teilzeichenfolgen ermittelt, was häufig nicht gewünscht ist. In der folgenden Tabelle finden Sie Beispiele für reguläre Ausdrücke. Außerdem wird erklärt, warum ein Abgleich erfolgreich ist oder fehlschlägt:

expression Zeichenfolge Match Kommentar
[a-z]{2} hello Ja Teilzeichenfolge stimmt überein
[a-z]{2} 123abc456 Ja Teilzeichenfolge stimmt überein
[a-z]{2} mz Ja Ausdruck stimmt überein
[a-z]{2} MZ Ja keine Unterscheidung zwischen Groß-/Kleinbuchstaben
^[a-z]{2}$ hello Nein siehe Erläuterungen zu ^ und $ oben
^[a-z]{2}$ 123abc456 Nein siehe Erläuterungen zu ^ und $ oben

Weitere Informationen zur Syntax von regulären Ausdrücken finden Sie unter Sprachelemente für reguläre Ausdrücke – Kurzübersicht.

Einen regulären Ausdruck können Sie verwenden, um einen Parameter auf zulässige Werte einzuschränken. Mit {action:regex(^(list|get|create)$)} werden beispielsweise für den action-Routenwert nur die Werte list, get oder create abgeglichen. Wenn die Zeichenfolge ^(list|get|create)$ dem Einschränkungswörterbuch übergeben wird, führt dies zum gleichen Ergebnis. Auch Einschränkungen, die dem zugehörigen Wörterbuch hinzugefügt werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden als reguläre Ausdrücke behandelt. Einschränkungen, die innerhalb einer Vorlage übergeben werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden nicht als reguläre Ausdrücke behandelt.

Benutzerdefinierte Routeneinschränkungen

Benutzerdefinierte Routeneinschränkungen können durch Implementierung der IRouteConstraint-Schnittstelle erstellt werden. Die IRouteConstraint-Schnittstelle umfasst die Match-Methode, die true zurückgibt, wenn die Einschränkung erfüllt wird, und andernfalls false.

Benutzerdefinierte Routeneinschränkungen werden nur selten benötigt. Bevor Sie eine benutzerdefinierte Routeneinschränkung implementieren, sollten Sie Alternativen in Betracht ziehen, wie z. B. Modellbindung.

Der ASP.NET Core-Ordner Constraints bietet nützliche Beispiele für die Erstellung von Einschränkungen. Beispiel: GuidRouteConstraint.

Zum Verwenden eines benutzerdefinierten IRouteConstraint-Elements muss der Routeneinschränkungstyp bei der ConstraintMap-Eigenschaft der App im Dienstcontainer registriert werden. Eine ConstraintMap ist ein Wörterbuch, das Routeneinschränkungsschlüssel IRouteConstraint-Implementierungen zuordnet, die diese Einschränkungen überprüfen. Die ConstraintMap einer App kann in Program.cs entweder als Teil eines AddRouting-Aufrufs oder durch direktes Konfigurieren von RouteOptions mit builder.Services.Configure<RouteOptions> aktualisiert werden. Zum Beispiel:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

Die vorangehende Einschränkung wird im folgenden Code angewendet:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

Die Implementierung von NoZeroesRouteConstraint verhindert die Verwendung von 0 in einem Routenparameter:

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Der vorangehende Code:

  • Verhindert, dass 0 im {id}-Segment der Route vorhanden ist.
  • Dient als einfaches Beispiel für die Implementierung einer benutzerdefinierten Einschränkung. Es sollte nicht in einer Produktions-App eingesetzt werden.

Der folgende Code bietet einen besseren Ansatz, um zu verhindern, dass eine id mit einer 0 verarbeitet wird:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}

Der vorangehende Code bietet im Vergleich zum NoZeroesRouteConstraint-Ansatz folgende Vorteile:

  • Eine benutzerdefinierte Einschränkung ist nicht erforderlich.
  • Es wird ein beschreibender Fehler zurückgegeben, wenn der Routenparameter 0 enthält.

Parametertransformatoren

Parametertransformatoren:

Beispielsweise generiert ein benutzerdefinierter Parametertransformator slugify im Routenmuster blog\{article:slugify} mit Url.Action(new { article = "MyTestArticle" })blog\my-test-article.

Betrachten Sie die folgende Implementierung von IOutboundParameterTransformer:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}

Um einen Parametertransformator in einem Routenmuster zu verwenden, konfigurieren Sie ihn mit ConstraintMap in Program.cs:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

Das ASP.NET Core-Framework verwendet Parametertransformatoren, um den URI zu transformieren, zu dem ein Endpunkt aufgelöst wird. Beispielsweise wandeln Parametertransformatoren die Routenwerte um, die zum Zuordnen folgender Elemente verwendet werden: area, controller, action und page.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Mit der vorstehenden Routenvorlage wird die Aktion SubscriptionManagementController.GetAll dem URI /subscription-management/get-all zugeordnet. Ein Parametertransformator ändert nicht die zum Generieren eines Links verwendeten Routenwerte. Beispielsweise gibt Url.Action("GetAll", "SubscriptionManagement")/subscription-management/get-all aus.

ASP.NET Core bietet API-Konventionen für die Verwendung von Parametertransformatoren mit generierten Routen:

Referenz für URL-Generierung

Dieser Abschnitt enthält eine Referenz für den Algorithmus, der durch die URL-Generierung implementiert wird. In der Praxis werden bei den meisten komplexen Beispielen für die URL-Generierung Controller oder Razor Pages verwendet. Weitere Informationen finden Sie unter Routing in Controllern.

Die URL-Generierung beginnt mit einem Aufruf von LinkGenerator.GetPathByAddress oder einer ähnlichen Methode. Die Methode wird mit einer Adresse, mehreren Routenwerten und optional mit Informationen zur aktuellen Anforderung von HttpContext versehen.

Im ersten Schritt wird die Adresse verwendet, um bestimmte Endpunktkandidaten mithilfe einer IEndpointAddressScheme<TAddress>-Schnittstelle aufzulösen, die dem Adresstyp entspricht.

Sobald eine Kandidatengruppe anhand des Adressschemas gefunden wurde, werden die Endpunkte geordnet und iterativ verarbeitet, bis die URL-Generierung erfolgreich abgeschlossen ist. Bei der URL-Generierung wird nicht auf Mehrdeutigkeiten geprüft, daher ist das erste zurückgegebene Ergebnis das Endergebnis.

Behandeln von Problemen mit der Protokollierung bei der URL-Generierung

Der erste Schritt bei der Behebung von Problemen bei der URL-Generierung ist die Einstellung des Protokolliergrads von Microsoft.AspNetCore.Routing auf TRACE. LinkGenerator protokolliert viele Details über die Verarbeitung, die bei der Problembehebung nützlich sein können.

Ausführliche Informationen zur URL-Generierung finden Sie unter Referenz für URL-Generierung.

Adressen

Mithilfe von Adressen wird bei der URL-Generierung ein Aufruf in der API zur Linkgenerierung an mehrere Endpunktkandidaten gebunden.

Adressen sind ein erweiterbares Konzept, das standardmäßig mit zwei Implementierungen bereitgestellt wird:

  • Die Verwendung von Endpunktname (string) als Adresse:
    • Bietet ähnliche Funktionalität wie der Routenname von MVC.
    • Wird der IEndpointNameMetadata-Metadatentyp verwendet.
    • Löst die bereitgestellte Zeichenfolge anhand der Metadaten aller registrierten Endpunkte auf.
    • Löst beim Start eine Ausnahme aus, wenn mehrere Endpunkte den gleichen Namen aufweisen.
    • Wird für die allgemeine Verwendung außerhalb von Controllern und Razor Pages empfohlen.
  • Die Verwendung von Routenwerten (RouteValuesAddress) als Adresse:
    • Bietet eine ähnliche Funktionalität wie die veraltete Funktion zur URL-Generierung von Controllern und Razor Pages.
    • Lässt sich nur schwer erweitern und debuggen.
    • Bietet die Implementierung, die von IUrlHelper, Taghilfsprogrammen, HTML-Hilfsprogrammen, Aktionsergebnissen usw. verwendet wird.

Aufgabe des Adressschemas ist es, die Verbindung zwischen der Adresse und den übereinstimmenden Endpunkten anhand von beliebigen Kriterien herzustellen:

  • Das Schema für Endpunktenamen führt eine allgemeine Wörterbuchsuche durch.
  • Das Schema der Routenwerte weist eine komplexe beste Teilmenge des Mengenalgorithmus auf.

Umgebungswerte und explizite Werte

Aus der aktuellen Anforderung greift das Routing auf die Routenwerte der aktuellen Anforderung HttpContext.Request.RouteValues zu. Die mit der aktuellen Anforderung verbundenen Werte werden als Umgebungswerte bezeichnet. Aus Gründen der Übersichtlichkeit werden in der Dokumentation die an die Methoden übergebenen Routenwerte als explizite Werte bezeichnet.

Das folgende Beispiel zeigt Umgebungswerte und explizite Werte. Er liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte:

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

Der vorangehende Code:

Der folgende Code liefert nur explizite Werte und keine Umgebungswerte:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

Die vorhergehende Methode gibt /Home/Subscribe/17 zurück.

Der folgende Code in WidgetController gibt /Widget/Subscribe/17 zurück:

var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });

Der folgende Code stellt den Controller aus den Umgebungswerten in der aktuellen Anforderung und explizite Werte dar:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

Für den Code oben gilt:

  • /Gadget/Edit/17 wird zurückgegeben.
  • Url ruft die IUrlHelper-Schnittstelle ab.
  • Action generiert eine URL mit einem absoluten Pfad für eine Aktionsmethode. Die URL enthält den angegebenen action-Namen und route-Werte.

Sie liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}

Im vorangehenden Code wird url auf /Edit/17 festgelegt, wenn die Option zum Bearbeiten der Razor Page die folgende Seitenanweisung enthält:

@page "{id:int}"

Wenn die Routenvorlage "{id:int}" nicht in der Seite „Bearbeiten“ enthalten ist, ist url gleich /Edit?id=17.

Das Verhalten der IUrlHelper-Schnittstelle von MVC fügt zusätzlich zu den hier beschriebenen Regeln eine weitere Komplexitätsebene hinzu:

  • IUrlHelper liefert immer die Routenwerte aus der aktuellen Anforderung als Umgebungswerte.
  • IUrlHelper.action kopiert immer die aktuellen Routenwerte action und controller als explizite Werte, sofern sie nicht vom Entwickler außer Kraft gesetzt werden.
  • IUrlHelper.page kopiert immer den aktuellen Routenwert page als expliziten Wert, sofern er nicht außer Kraft gesetzt wird.
  • IUrlHelper.Page setzt immer den aktuellen Routenwert handler mit null als expliziten Wert außer Kraft, sofern er nicht außer Kraft gesetzt wird.

Benutzer sind oft von den Verhaltensdetails der Umgebungswerte überrascht, da MVC anscheinend nicht den eigenen Regeln folgt. Aus Verlaufs- und Kompatibilitätsgründen weisen bestimmte Routenwerte wie action, controller, page und handler ein spezielles Verhalten auf.

Die äquivalente Funktionalität, die durch LinkGenerator.GetPathByAction und LinkGenerator.GetPathByPage bereitgestellt wird, verdoppelt diese Anomalien von IUrlHelper aus Kompatibilitätsgründen.

URL-Generierungsprozess

Sobald die Gruppe der Endpunktkandidaten ermittelt ist, wird der URL-Generierungsalgorithmus angewendet:

  • Die Endpunkte werden iterativ verarbeitet.
  • Das erste erfolgreiche Ergebnis wird zurückgegeben.

Der erste Schritt in diesem Prozess wird als Routenwertinvalidierung bezeichnet. Die Routenwertinvalidierung ist der Prozess, bei dem das Routing entscheidet, welche Routenwerte aus den Umgebungswerten verwendet und welche ignoriert werden sollen. Jeder Umgebungswert wird berücksichtigt und entweder mit den expliziten Werten kombiniert oder aber ignoriert.

Denken Sie daran, dass Umgebungswerte Anwendungsentwicklern in allgemeinen Fällen das Schreiben von Code sparen können. In der Regel sind die Szenarios, in denen Umgebungswerte hilfreich sind, mit MVC verknüpft:

  • Bei der Verknüpfung mit einer anderen Aktion im gleichen Controller muss der Controllername nicht angegeben werden.
  • Bei der Verknüpfung mit einem anderen Controller im gleichen Bereich muss der Bereich nicht angegeben werden.
  • Bei der Verknüpfung mit der gleichen Aktionsmethode müssen keine Routenwerte angegeben werden.
  • Bei der Verknüpfung mit einem anderen Teil der App sollen keine Routenwerte übertragen werden, die für diesen Teil der App irrelevant sind.

Aufrufe an LinkGenerator oder IUrlHelper, die null zurückgeben, sind meist dadurch bedingt, dass die Routenwertinvalidierung nicht verstanden wurde. Beheben Sie die Routenwertinvalidierung, indem Sie explizit mehr Routenwerte angeben, um zu prüfen, ob das Problem dadurch gelöst wird.

Bei der Routenwertinvalidierung wird davon ausgegangen, dass das URL-Schema der Anwendung hierarchisch ist, mit einer von links nach rechts gebildeten Hierarchie. Sehen Sie sich die einfache Controllerroutenvorlage {controller}/{action}/{id?} an, um ein Gespür dafür zu bekommen, wie dies in der Praxis funktioniert. Durch eine Änderung auf einen Wert werden alle rechts angezeigten Routenwerte ungültig. Dies spricht für die These von der Hierarchie. Wenn die App einen Umgebungswert für id hat und der Vorgang einen anderen Wert für controller angibt:

  • id wird nicht wiederverwendet, weil {controller} links von {id?} steht.

Einige Beispiele veranschaulichen dieses Prinzip:

  • Wenn die expliziten Werte einen Wert für id enthalten, wird der Umgebungswert für id ignoriert. Die Umgebungswerte für controller und action können verwendet werden.
  • Wenn die expliziten Werte einen Wert für action enthalten, wird jeder Umgebungswert für action ignoriert. Die Umgebungswerte für controller können verwendet werden. Wenn sich der explizite Wert für action von dem Umgebungswert für action unterscheidet, wird der Wert id nicht verwendet. Wenn der explizite Wert für action mit dem Umgebungswert für action übereinstimmt, kann der Wert id verwendet werden.
  • Wenn die expliziten Werte einen Wert für controller enthalten, wird jeder Umgebungswert für controller ignoriert. Wenn sich der explizite Wert für controller von dem Umgebungswert für controller unterscheidet, werden die Werte action und id nicht verwendet. Wenn der explizite Wert für controller mit dem Umgebungswert für controller übereinstimmt, können die Werte action und id verwendet werden.

Dieser Prozess wird zusätzlich durch die vorhandenen Attributrouten und dedizierten konventionellen Routen erschwert. Konventionelle Routen des Controllers wie {controller}/{action}/{id?} legen eine Hierarchie mithilfe von Routenparametern fest. Bei bestimmten konventionellen Routen und Attributrouten zu Controllern und Razor Pages:

  • Gibt es eine Hierarchie für Routenwerte.
  • Werden diese nicht in der Vorlage angezeigt.

Für diese Fälle definiert die URL-Generierung das Konzept der erforderlichen Werte. Bei Endpunkten, die von Controllern und Razor Pages erstellt wurden, sind erforderliche Werte angegeben, die eine Routenwertinvalidierung ermöglichen.

Der Algorithmus der Routenwertinvalidierung im Detail:

  • Die erforderlichen Wertnamen werden mit den Routenparametern kombiniert und dann von links nach rechts verarbeitet.
  • Für jeden Parameter werden der Umgebungswert und der explizite Wert verglichen:
    • Wenn der Umgebungswert und der explizite Wert gleich sind, wird der Prozess fortgesetzt.
    • Wenn der Umgebungswert vorhanden ist und der explizite Wert nicht, wird der Umgebungswert bei der URL-Generierung verwendet.
    • Wenn der Umgebungswert nicht vorhanden ist und der explizite Wert vorhanden ist, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.
    • Wenn der Umgebungswert und der explizite Wert vorhanden und die beiden Werte unterschiedlich sind, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.

An diesem Punkt ist der Vorgang zur URL-Generierung bereit, Routeneinschränkungen auszuwerten. Die akzeptierten Werte werden mit den Standardwerten der Parameter kombiniert, die für Einschränkungen bereitgestellt werden. Wenn alle Einschränkungen erfüllt sind, wird der Vorgang fortgesetzt.

Als Nächstes können die akzeptierten Werte verwendet werden, um die Routenvorlage zu erweitern. Die Routenvorlage wird verarbeitet:

  • Von links nach rechts.
  • Für jeden Parameter wird der akzeptierte Wert ersetzt.
  • In den folgenden Sonderfällen:
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter einen Standardwert hat, wird der Standardwert verwendet.
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter optional ist, wird die Verarbeitung fortgesetzt.
    • Wenn irgendein Routenparameter rechts neben einem fehlenden optionalen Parameter einen Wert hat, schlägt der Vorgang fehl.
    • Zusammenhängende Parameter mit Standardwerten und optionale Parameter werden, wenn möglich, reduziert dargestellt.

Explizit bereitgestellte Werte, für die keine Übereinstimmungen mit einem Routensegment ermittelt werden, werden der Abfragezeichenfolge hinzugefügt. In der folgenden Tabelle werden die Ergebnisse dargestellt, die aus der Verwendung der Routenvorlage {controller}/{action}/{id?} hervorgehen:

Umgebungswerte Explizite Werte Ergebnis
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

Probleme mit der Routenwertinvalidierung

Der folgende Code zeigt ein Beispiel für ein Schema zur URL-Generierung, das vom Routing nicht unterstützt wird:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

Im vorangehenden Code wird der culture-Routenparameter für die Lokalisierung verwendet. Ziel ist es, dass der culture-Parameter immer als Umgebungswert akzeptiert wird. Der culture-Parameter wird jedoch aufgrund der Art und Weise, wie die erforderlichen Werte funktionieren, nicht als Umgebungswert akzeptiert:

  • In der "default"-Routenvorlage befindet sich der culture-Routenparameter links von controller, sodass culture durch Änderungen an controller nicht ungültig wird.
  • In der "blog"-Routenvorlage wird der culture-Routenparameter rechts von controller betrachtet, der in den erforderlichen Werten aufgeführt ist.

Zerlegen von URL-Pfaden mit LinkParser

Die LinkParser Klasse fügt die Möglichkeit hinzu, einen URL-Pfads in einen Satz von Routenwerten zu zerlegen. Die ParsePathByEndpointName Methode verwendet einen Endpunktnamen und einen URL-Pfad und gibt einen Satz von aus dem URL-Pfad extrahierten Routenwerten zurück.

Im folgenden Beispielcontroller verwendet die GetProduct Aktion eine Routenvorlage von api/Products/{id} und hat eine Name von GetProduct:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

In derselben Controllerklasse erwartet die AddRelatedProduct Aktion einen URL-Pfad, pathToRelatedProduct, der als Abfragezeichenfolgen-Parameter bereitgestellt werden kann:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

Im vorherigen Beispiel extrahiert die AddRelatedProduct Aktion den id Routenwert aus dem URL-Pfad. Beispielsweise wird der relatedProductId Wert bei einem /api/Products/1 URL-Pfad auf 1 festgelegt. Dieser Ansatz ermöglicht es den Clients der API, URL-Pfade beim Verweisen auf Ressourcen zu verwenden, ohne wissen zu müssen, wie eine solche URL strukturiert ist.

Konfigurieren von Endpunktmetadaten

Die folgenden Links enthalten Informationen zum Konfigurieren von Endpunktmetadaten:

Hostabgleich in Routen mit RequireHost

RequireHost wendet eine Einschränkung auf die Route an, für die der angegebene Host erforderlich ist. Der Parameter RequireHost oder [Host] kann wie folgt lauten:

  • Host: www.domain.com, entspricht www.domain.com mit einem beliebigen Port.
  • Host mit Platzhalter: *.domain.com, entspricht www.domain.com, subdomain.domain.com oder www.subdomain.domain.com an einem beliebigen Port.
  • Port: *:5000, entspricht Port 5000 mit einem beliebigen Host.
  • Host und Port: www.domain.com:5000 oder *.domain.com:5000, entspricht dem Host und Port.

Es können mehrere Parameter mit RequireHost oder [Host] angegeben werden. Die Einschränkung gleicht die Hosts ab, die für einen der Parameter gültig sind. Beispielsweise entspricht [Host("domain.com", "*.domain.com")]domain.com, www.domain.com und subdomain.domain.com.

Im folgenden Code wird RequireHost verwendet, um den angegebenen Host auf der Route anzufordern:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

Im folgenden Code wird das [Host]-Attribut für den Controller verwendet, um die einzelnen angegebenen Hosts anzufordern:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

Wenn das [Host]-Attribut sowohl auf die Controller- als auch auf die Aktionsmethode angewendet wird, trifft Folgendes zu:

  • Das Attribut auf der Aktion wird verwendet.
  • Das Controllerattribut wird ignoriert.

Leistungsleitfaden für das Routing

Wenn eine App Leistungsprobleme hat, wird die Ursache häufig beim Routing vermutet. Das Routing wird deshalb in Betracht gezogen, weil Frameworks wie Controller und Razor Pages in ihren Protokollierungsmeldungen die innerhalb des Frameworks verbrachte Zeit angeben. Wenn es einen signifikanten Unterschied zwischen der von den Controllern gemeldeten Zeit und der Gesamtzeit der Anforderung gibt:

  • Schließen Entwickler ihren App-Code als Ursache des Problems aus.
  • Wird in der Regel angenommen, dass das Routing die Ursache für das Problem ist.

Die Leistung des Routings wird anhand von Tausenden von Endpunkten getestet. Es ist unwahrscheinlich, dass eine typische App auf ein Leistungsproblem stößt, nur weil diese zu umfangreich ist. Die häufigste Ursache für eine langsames Routing ist üblicherweise eine schlecht funktionierende benutzerdefinierte Middleware.

Das folgende Codebeispiel veranschaulicht eine grundlegende Technik zur Eingrenzung der Verzögerungsquelle:

var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");

Auf das Zeitrouting:

  • Verschachteln Sie jede Middleware mit einer Kopie der im vorherigen Code gezeigten Zeitmiddleware.
  • Fügen Sie einen eindeutigen Bezeichner hinzu, um die Zeitdaten mit dem Code zu korrelieren.

Dies ist ein einfacher Weg, um die Verzögerung zu verringern, wenn sie signifikant ist, zum Beispiel größer als 10ms. Wenn Time 2 von Time 1 subtrahiert wird, ergibt sich die in der UseRouting-Middleware benötigte Zeit.

Der folgende Code verwendet einen kompakteren Ansatz als der vorangegangene Zeitcode:

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");

Potenziell teure Routingfeatures

Die folgende Liste gibt einen Einblick in Routingfeatures, die im Vergleich zu einfachen Routenvorlagen relativ teuer sind:

  • Reguläre Ausdrücke: Mit einer kleinen Menge an Eingaben ist möglich, reguläre Ausdrücke zu schreiben, die komplex sind oder eine lange Ausführungszeit haben.
  • Komplexe Segmente ({x}-{y}-{z}):
    • Sind wesentlich teurer als das Analysieren eines regulären URL-Pfadsegments.
    • Führen dazu, dass viel mehr Teilzeichenfolgen zugeordnet werden.
  • Synchroner Datenzugriff: Viele komplexe Apps verfügen im Rahmen Ihrer Routingfunktionen über Datenbankzugriff. Verwenden Sie Erweiterbarkeitspunkte wie MatcherPolicy und EndpointSelectorContext, die asynchron sind.

Leitfaden für große Routingtabellen

ASP.NET Core verwendet standardmäßig einen Routingalgorithmus, bei dem Arbeitsspeicher zugunsten von CPU-Zeit geopfert wird. Dies hat den angenehmen Effekt, dass die Zeit für den Routenabgleich nur von der Länge des abzugleichenden Pfads und nicht von der Routenanzahl abhängt. Dieser Ansatz kann jedoch in einigen Fällen problematisch sein, etwa dann, wenn die App eine große Anzahl von Routen umfasst (mehrere Tausend) und die Routen eine große Anzahl variabler Präfixe enthalten. Beispiel: Die Routen weisen Parameter in frühen Segmenten der Route auf, etwa {parameter}/some/literal.

Es ist unwahrscheinlich, dass eine App in eine Situation gerät, in der dies ein Problem darstellt, es sei denn:

  • Es ist eine große Anzahl von Routen in der App vorhanden, die dieses Muster verwenden.
  • Es ist eine große Anzahl von Routen in der App vorhanden.

Wie lässt sich feststellen, ob eine Anwendung mit dem Problem einer großer Routentabelle zu kämpfen hat?

  • Es gibt zwei Symptome, auf die Sie achten sollten:
    • Die App wird bei der ersten Anforderung langsam gestartet.
      • Dieses Symptom ist zwingend, reicht aber allein nicht aus. Es gibt viele andere, nicht routenbezogene Probleme, die zu einem langsamen App-Start führen können. Überprüfen Sie auch auf die folgende Bedingung, um genau zu bestimmen, ob diese Situation für die App vorliegt.
    • Die Anwendung verbraucht beim Start viel Speicher, und ein Speicherabbild zeigt eine große Anzahl von Microsoft.AspNetCore.Routing.Matching.DfaNode-Instanzen.

Beheben dieses Problems

Es gibt verschiedene Techniken und Optimierungen, die auf Routen angewendet werden können, um dieses Szenario weitgehend zu vermeiden:

  • Wenden Sie nach Möglichkeit Routeneinschränkungen auf Ihre Parameter an, z. B. {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)}.
    • Dadurch kann der Routingalgorithmus die für den Abgleich verwendeten Strukturen intern optimieren und den verwendeten Arbeitsspeicher drastisch reduzieren.
    • In den meisten Fällen reicht dies aus, um zu einem akzeptablen Verhalten zurückzukehren.
  • Ändern Sie die Routen, um Parameter in spätere Segmente in der Vorlage zu verlagern.
    • Dadurch wird die Anzahl von möglichen „Pfaden“ reduziert, die bei Angabe eines Pfads mit einem Endpunkt übereinstimmen.
  • Verwenden Sie eine dynamische Route, und führen Sie die Zuordnung zu einem Controller/einer Seite dynamisch durch.
    • Dies kann mithilfe von MapDynamicControllerRoute und MapDynamicPageRoute erreicht werden.

Leitfaden für Bibliotheksautoren

Dieser Abschnitt enthält Hinweise für Bibliotheksautoren, die auf dem Routing aufbauen. Diese Details sollen sicherstellen, dass App-Entwickler gute Erfahrungen mit Bibliotheken und Frameworks machen, die das Routing erweitern.

Definieren von Endpunkten

Wenn Sie ein Framework erstellen möchten, das das Routing für den URL-Abgleich verwendet, sollten Sie zunächst eine Benutzeroberfläche definieren, die auf UseEndpoints aufbaut.

Es wird empfohlen, dass Sie auf IEndpointRouteBuilder aufbauen. Auf diese Weise können Benutzer Ihr Framework mit anderen ASP.NET Core-Features problemlos zusammenstellen. Jede ASP.NET Core-Vorlage umfasst die Routingfunktionalität. Gehen Sie davon aus, dass das Routing vorhanden und den Benutzern vertraut ist.

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

Es wird empfohlen, dass Sie einen versiegelten konkreten Typ aus einem Aufruf an MapMyFramework(...) zurückgeben, der IEndpointConventionBuilder implementiert. Die meisten Map...-Methoden in Frameworks folgen diesem Muster. Die IEndpointConventionBuilder-Schnittstelle:

  • Ermöglicht das Zusammensetzen von Metadaten.
  • Wird von verschiedenen Erweiterungsmethoden angesteuert.

Wenn Sie Ihren eigenen Typ deklarieren, können Sie dem Generator Ihre eigene frameworkspezifische Funktionalität hinzufügen. Es ist in Ordnung, einen vom Framework deklarierten Generator zu umschließen und Aufrufe an ihn weiterzuleiten.

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

Ziehen Sie in Betracht, Ihre eigene EndpointDataSource-Klasse zu schreiben. Die EndpointDataSource-Klasse vom primitiven Typ auf niedriger Ebene eignet sich zum Deklarieren und Aktualisieren einer Sammlung von Endpunkten. EndpointDataSource ist eine leistungsstarke API, die von Controllern und Razor Pages verwendet wird.

Die Routingtests haben ein grundlegendes Beispiel für eine Datenquelle, die nicht aktualisiert wird.

Versuchen Sie nicht, eine EndpointDataSource-Klasse standardmäßig zu registrieren. Fordern Sie die Benutzer auf, Ihr Framework in UseEndpoints zu registrieren. Der Grundgedanke beim Routing ist, dass standardmäßig nichts enthalten ist, und dass die Endpunkte bei UseEndpoints registriert werden müssen.

Erstellen von in das Routing integrierter Middleware

Ziehen Sie in Betracht, Metadatentypen als Schnittstelle zu definieren.

Ermöglichen Sie, Metadatentypen als Attribut für Klassen und Methoden zu verwenden.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Frameworks wie Controller und Razor Pages unterstützen die Anwendung von Metadatenattributen auf Typen und Methoden. Für das Deklarieren von Metadatentypen gilt:

  • Machen Sie diese als Attribute zugänglich.
  • Die meisten Benutzer sind mit der Anwendung von Attributen vertraut.

Durch die Deklaration eines Metadatentyps als Schnittstelle wird die Flexibilität zusätzlich erhöht:

  • Schnittstelle können zusammengesetzt werden.
  • Entwickler können ihre eigenen Typen deklarieren, die mehrere Richtlinien kombinieren.

Ermöglichen Sie, Metadaten außer Kraft zu setzen, wie in folgendem Beispiel gezeigt:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

Die beste Möglichkeit, diese Richtlinien zu befolgen, besteht darin, die Definition von Markermetadaten zu vermeiden:

  • Suchen Sie nicht nur nach dem Vorhandensein eines Metadatentyps.
  • Definieren Sie eine Eigenschaft für die Metadaten, und überprüfen Sie die Eigenschaft.

Die Metadatensammlung ist geordnet und unterstützt das Außerkraftsetzen nach Priorität. Im Fall von Controllern sind Metadaten für die Aktionsmethode am spezifischsten.

Nutzen Sie Middleware mit und ohne Routing.

app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();

Ein gutes Beispiel für diese Vorgabe ist die UseAuthorization-Middleware. Mithilfe der Autorisierungsmiddleware können Sie eine Fallbackrichtlinie einbauen. Die Fallbackrichtlinie gilt, falls angegeben, für beide:

  • Endpunkte ohne angegebene Richtlinie.
  • Anforderungen, die nicht mit einem Endpunkt übereinstimmen.

Dies macht die Autorisierungsmiddleware außerhalb des Routingkontexts sehr nützlich. Die Autorisierungsmiddleware kann für die traditionelle Middlewareprogrammierung verwendet werden.

Debugdiagnose

Legen Sie für eine ausführliche Routingdiagnoseausgabe Logging:LogLevel:Microsoft auf Debug fest. Legen Sie in der Entwicklungsumgebung die Protokollebene in appsettings.Development.json fest:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Zusätzliche Ressourcen

Das Routing wird für das Abgleichen von HTTP-Anforderungen und das Verteilen an ausführbare Endpunkte der App eingesetzt. Endpunkte sind die Einheiten des ausführbaren Codes für die Anforderungsverarbeitung in der App. Endpunkte werden in der App definiert und beim Start der App konfiguriert. Beim Endpunktabgleich können Werte aus der Anforderungs-URL extrahiert und für die Verarbeitung der Anforderung bereitgestellt werden. Mithilfe von Endpunktinformationen aus der App lassen sich durch das Routing URLs generieren, die Endpunkten zugeordnet werden.

Apps können das Routing mit folgenden Funktionen konfigurieren:

In diesem Artikel werden die grundlegenden Details zum ASP.NET Core-Routing beschrieben. Informationen zur Routingkonfiguration finden Sie wie folgt:

Das in diesem Dokument beschriebene Endpunktroutingsystem gilt für ASP.NET Core 3.0 und höher. Weitere Informationen zum vorherigen Routingsystem, das auf IRouter basiert, erhalten Sie, wenn Sie die ASP.NET Core 2.1-Version wie folgt auswählen:

  • Mit der Versionsauswahl für eine vorherige Version.
  • Mit Auswahl von ASP.NET Core 2.1.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Die Downloadbeispiele für dieses Dokument werden durch eine bestimmte Startup-Klasse aktiviert. Um ein bestimmtes Beispiel auszuführen, ändern Sie Program.cs so, dass die gewünschte Startup-Klasse aufgerufen wird.

Routinggrundlagen

Alle ASP.NET Core-Vorlagen enthalten die Routingfunktionen im generierten Code. Das Routing wird in Startup.Configure in der Middlewarepipeline registriert.

Der folgende Code veranschaulicht ein einfaches Beispiel für das Routing:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

Beim Routing wird ein Middlewarepaar verwendet, das durch UseRouting und UseEndpoints registriert wird:

  • UseRouting fügt der Middlewarepipeline einen Routenabgleich hinzu. Diese Middleware prüft die in der App definierten Endpunkte und wählt anhand der Anforderung die beste Übereinstimmung aus.
  • UseEndpoints fügt der Middlewarepipeline die Endpunktausführung hinzu. Dabei wird der mit dem ausgewählten Endpunkt verknüpfte Delegat ausgeführt.

Das vorherige Beispiel enthält eine einzelnen Route-zu- Code-Endpunkt unter Verwendung der MapGet-Methode:

  • Wenn eine GET-HTTP-Anforderung an die Stamm-URL /gesendet wird:
    • Der angezeigte Anforderungsdelegat wird ausgeführt.
    • Hello World! wird in die HTTP-Antwort geschrieben. Die Stamm-URL / lautet standardmäßig https://localhost:5001/.
  • Wenn die Anforderungsmethode nicht GET bzw. die Stamm-URL nicht / ist, gibt es keinen Routenabgleich und es wird ein HTTP-404-Fehler zurückgegeben.

Endpunkt

Die MapGet-Methode wird verwendet, um einen Endpunkt zu definieren. Ein Endpunkt kann Folgendes sein:

  • Ausgewählt, indem die URL und die HTTP-Methode abgeglichen werden.
  • Ausgeführt, indem ein Delegat ausgeführt wird.

Endpunkte, die von der App zugeordnet und ausgeführt werden können, sind in UseEndpoints konfiguriert. Mit MapGet, MapPost und ähnlichen Methoden werden beispielsweise Anforderungsdelegate mit dem Routingsystem verbunden. Zudem können weitere Methoden zur Verbindung von ASP.NET Core-Frameworkfunktionen mit dem Routingsystem verwendet werden:

Das folgende Beispiel zeigt das Routing mit einer anspruchsvolleren Routenvorlage:

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/hello/{name:alpha}", async context =>
    {
        var name = context.Request.RouteValues["name"];
        await context.Response.WriteAsync($"Hello {name}!");
    });
});

Die Zeichenfolge /hello/{name:alpha} ist eine Routenvorlage. Sie wird verwendet, um zu konfigurieren, wie der Endpunkt abgeglichen wird. In diesem Fall gleicht die Vorlage Folgendes ab:

  • Eine URL wie /hello/Ryan
  • Alle URL-Pfade, die mit /hello/ beginnen, gefolgt von einer Sequenz alphabetischer Zeichen. :alpha wendet eine Routeneinschränkung an, die nur alphabetische Zeichen abgleicht. Routeneinschränkungen werden weiter unten in diesem Dokument erläutert.

Das zweite Segment des URL-Pfads, {name:alpha}:

Das in diesem Dokument beschriebene Endpunktroutingsystem gilt ab ASP.NET Core 3.0. Alle Versionen von ASP.NET Core unterstützen jedoch dieselben Routenvorlagenfunktionen und Routeneinschränkungen.

Das folgende Beispiel zeigt das Routing mit Integritätsprüfungen und Autorisierung:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

Wenn Sie möchten, dass Codekommentare in anderen Sprachen als Englisch angezeigt werden, informieren Sie uns in diesem GitHub-Issue.

Im vorherigen Beispiel wird veranschaulicht, wie Sie:

  • Die Autorisierungsmiddleware für das Routing verwenden.
  • Endpunkte zum Konfigurieren des Autorisierungsverhaltens verwendet werden können.

Der MapHealthChecks-Aufruf fügt einen Endpunkt für eine Integritätsprüfung hinzu. Durch die Verkettung von RequireAuthorization mit diesem Aufruf wird eine Autorisierungsrichtlinie an den Endpunkt angefügt.

Der Aufruf von UseAuthentication und UseAuthorization wird die Authentifizierungs- und Autorisierungsmiddleware hinzugefügt. Diese Middleware wird zum Ausführen folgender Aktionen zwischen UseRouting und UseEndpoints platziert:

  • Anzeigen des von UseRouting ausgewählten Endpunkts.
  • Anwenden einer Autorisierungsrichtlinie vor dem Senden von UseEndpoints an den Endpunkt.

Endpunktmetadaten

Im vorangehenden Beispiel gibt es zwei Endpunkte, aber nur dem für die Integritätsprüfung ist eine Autorisierungsrichtlinie angefügt. Wenn die Anforderung mit dem Endpunkt der Integritätsprüfung, /healthz, übereinstimmt, wird eine Autorisierungsprüfung durchgeführt. Dadurch wird veranschaulicht, dass Endpunkten zusätzliche Daten zugeordnet werden können. Diese zusätzlichen Daten werden als Metadaten des Endpunkts bezeichnet:

  • Die Metadaten lassen sich von routingfähiger Middleware verarbeiten.
  • Die Metadaten können einen beliebigen .NET-Typ aufweisen.

Routingkonzepte

Durch Hinzufügen des effizienten Endpunkt-Konzepts stellt das Routingsystem eine Ergänzung der Middlewarepipeline dar. Endpunkte stehen für Funktionseinheiten der App, die sich in Bezug auf Routing, Autorisierung und die Anzahl der ASP.NET Core-Systeme voneinander unterscheiden.

ASP.NET Core-Endpunktdefinition

Ein ASP.NET Core-Endpunkt ist:

Der folgende Code zeigt, wie der Endpunkt, der mit der aktuellen Anforderung übereinstimmt, abgerufen und geprüft werden kann:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.Use(next => context =>
    {
        var endpoint = context.GetEndpoint();
        if (endpoint is null)
        {
            return Task.CompletedTask;
        }
        
        Console.WriteLine($"Endpoint: {endpoint.DisplayName}");

        if (endpoint is RouteEndpoint routeEndpoint)
        {
            Console.WriteLine("Endpoint has route pattern: " +
                routeEndpoint.RoutePattern.RawText);
        }

        foreach (var metadata in endpoint.Metadata)
        {
            Console.WriteLine($"Endpoint has metadata: {metadata}");
        }

        return Task.CompletedTask;
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

Der Endpunkt, falls ausgewählt, kann aus dem HttpContext-Element abgerufen werden. Seine Eigenschaften können geprüft werden. Endpunktobjekte sind unveränderlich und können nach der Erstellung nicht mehr geändert werden. Der häufigste Typ des Endpunkts ist eine RouteEndpoint-Klasse. RouteEndpoint enthält Informationen, die eine Auswahl durch das Routingsystem ermöglichen.

Im vorangehenden Code wird mit app.Use eine Middleware inline konfiguriert.

Der folgende Code zeigt, dass es, je nachdem, wo app.Use in der Pipeline aufgerufen wird, möglicherweise keinen Endpunkt gibt:

// Location 1: before routing runs, endpoint is always null here
app.Use(next => context =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match
app.Use(next => context =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseEndpoints(endpoints =>
{
    // Location 3: runs when this endpoint matches
    endpoints.MapGet("/", context =>
    {
        Console.WriteLine(
            $"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
        return Task.CompletedTask;
    }).WithDisplayName("Hello");
});

// Location 4: runs after UseEndpoints - will only run if there was no match
app.Use(next => context =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

In diesem vorangehenden Beispiel werden Console.WriteLine-Anweisungen hinzugefügt, die anzeigen, ob ein Endpunkt ausgewählt wurde oder nicht. Aus Gründen der Übersichtlichkeit wird in dem Beispiel dem bereitgestellten /-Endpunkt ein Anzeigename zugewiesen.

Wenn Sie diesen Code mit einer URL / ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

Wenn Sie diesen Code mit einer anderen URL ausführen, wird Folgendes angezeigt:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

Diese Ausgabe zeigt Folgendes:

  • Der Endpunkt ist immer NULL, bevor UseRouting aufgerufen wird.
  • Wenn eine Übereinstimmung gefunden wird, ist der Endpunkt zwischen UseRouting und UseEndpoints ungleich NULL.
  • Die UseEndpoints-Middleware ist eine Terminalmiddleware, wenn eine Übereinstimmung gefunden wird. Terminalmiddleware wird weiter unten in diesem Dokument definiert.
  • Die Middleware nach UseEndpoints wird nur ausgeführt, wenn keine Übereinstimmung gefunden wird.

Die UseRouting-Middleware verwendet die SetEndpoint-Methode, um den Endpunkt an den aktuellen Kontext anzufügen. Es ist möglich, die UseRouting-Middleware durch benutzerdefinierte Logik zu ersetzen und dennoch die Vorteile durch die Verwendung von Endpunkten zu nutzen. Endpunkte befinden sich auf niedriger Ebene, wie Middleware, und sind nicht an die Routingimplementierung gekoppelt. Die meisten Apps müssen UseRouting nicht durch eigene Logik ersetzen.

Die UseEndpoints-Middleware ist so konzipiert, dass Sie zusammen mit der UseRouting-Middleware verwendet werden kann. Die Hauptlogik zum Ausführen eines Endpunkts ist nicht kompliziert. Mit GetEndpoint können Sie einen Endpunkt abrufen und dann dessen RequestDelegate-Eigenschaft aufrufen.

Der folgende Code veranschaulicht, wie Middleware das Routing beeinflussen oder darauf reagieren kann:

public class IntegratedMiddlewareStartup
{ 
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Location 1: Before routing runs. Can influence request before routing runs.
        app.UseHttpMethodOverride();

        app.UseRouting();

        // Location 2: After routing runs. Middleware can match based on metadata.
        app.Use(next => context =>
        {
            var endpoint = context.GetEndpoint();
            if (endpoint?.Metadata.GetMetadata<AuditPolicyAttribute>()?.NeedsAudit
                                                                            == true)
            {
                Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
            }

            return next(context);
        });

        app.UseEndpoints(endpoints =>
        {         
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello world!");
            });

            // Using metadata to configure the audit policy.
            endpoints.MapGet("/sensitive", async context =>
            {
                await context.Response.WriteAsync("sensitive data");
            })
            .WithMetadata(new AuditPolicyAttribute(needsAudit: true));
        });

    } 
}

public class AuditPolicyAttribute : Attribute
{
    public AuditPolicyAttribute(bool needsAudit)
    {
        NeedsAudit = needsAudit;
    }

    public bool NeedsAudit { get; }
}

Im vorherigen Beispiel werden zwei wichtige Konzepte dargestellt:

  • Die Middleware kann vor UseRouting ausgeführt werden, um die Daten zu ändern, auf denen das Routing basiert.
  • Die Middleware kann zwischen UseRouting und UseEndpoints ausgeführt werden, um die Ergebnisse des Routings vor der Ausführung des Endpunkts zu verarbeiten.
    • Middleware, die zwischen UseRouting und UseEndpoints ausgeführt wird:
      • Überprüft in der Regel die Metadaten, um die Endpunkte zu ermitteln.
      • Trifft häufig Sicherheitsentscheidungen, wie UseAuthorization und UseCors.
    • Durch die Kombination aus Middleware und Metadaten ist es möglich, für jeden einzelnen Endpunkt Richtlinien zu konfigurieren.

Der vorangehende Code zeigt ein Beispiel für eine benutzerdefinierte Middleware, die entpunktbezogene Richtlinien unterstützt. Die Middleware schreibt ein Überwachungsprotokoll für den Zugriff auf vertrauliche Daten in der Konsole. Die Middleware kann so konfiguriert werden, dass ein Endpunkt mit den AuditPolicyAttribute-Metadaten überwacht wird. In diesem Beispiel wird ein Opt-In-Muster veranschaulicht, bei dem nur Endpunkte überwacht werden, die als vertraulich markiert sind. Es ist möglich, diese Logik umgekehrt zu definieren, indem beispielsweise alles geprüft wird, was nicht als sicher markiert ist. Das Endpunktmetadaten-System ist flexibel. Diese Logik lässt sich für jeden Anwendungsfall passend schreiben.

Der vorherige Beispielcode soll die grundlegenden Konzepte von Endpunkten veranschaulichen. Das Beispiel ist nicht für Produktionsumgebungen vorgesehen. Eine vollständigere Version einer Middleware für Überwachungsprotokolle würde Folgendes bieten:

  • Protokollieren in einer Datei oder einer Datenbank.
  • Einschließen von Details wie Benutzer, IP-Adresse, Name des vertraulichen Endpunkts usw.

Die Metadaten der Überwachungsrichtlinie AuditPolicyAttribute sind als Attribute definiert, um die Verwendung mit klassenbasierten Frameworks wie Controllern und SignalR zu erleichtern. Bei Verwendung von Route-zu-Code:

  • Metadaten werden an eine Generator-API angefügt.
  • Klassenbasierte Frameworks enthalten beim Erstellen von Endpunkten alle Attribute der entsprechenden Methode und Klasse.

Die bewährten Methoden für Metadatentypen sind, sie entweder als Schnittstellen oder als Attribute zu definieren. Schnittstellen und Attribute ermöglichen die Wiederverwendung von Code. Das Metadatensystem ist flexibel und weist keine Einschränkungen auf.

Vergleichen von Terminalmiddleware und Routing

Das folgende Codebeispiel zeigt den Unterschied zwischen Middleware und Routing:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Approach 1: Writing a terminal middleware.
    app.Use(next => async context =>
    {
        if (context.Request.Path == "/")
        {
            await context.Response.WriteAsync("Hello terminal middleware!");
            return;
        }

        await next(context);
    });

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // Approach 2: Using routing.
        endpoints.MapGet("/Movie", async context =>
        {
            await context.Response.WriteAsync("Hello routing!");
        });
    });
}

Beim in Approach 1: gezeigten Stil von Middleware handelt es sich um Terminalmiddleware. Sie wird als Terminalmiddleware bezeichnet, da sie einen Abgleich durchgeführt.

  • Der Abgleich im vorangehenden Beispiel ist Path == "/" für die Middleware und Path == "/Movie" für das Routing.
  • Bei einem erfolgreichen Abgleich führt sie bestimmte Funktionen aus und kehrt zurück, anstatt die next-Middleware aufzurufen.

Sie wird „Terminal Middleware“ genannt, da sie die Suche beendet, einige Funktionen ausführt und dann zurückkehrt.

Vergleichen von Terminalmiddleware und Routing:

  • Beide Ansätze ermöglichen das Beenden der Verarbeitungspipeline:
    • Die Middleware beendet die Pipeline, indem Sie zurückkehrt, anstatt next aufzurufen.
    • Endpunkte beenden immer die Verarbeitung.
  • Terminalmiddleware ermöglicht die Positionierung der Middleware an einer beliebigen Stelle in der Pipeline:
    • Endpunkte werden an der Position von UseEndpoints ausgeführt.
  • Mit Terminalmiddleware kann beliebiger Code beendet werden, wenn die Middleware übereinstimmt:
    • Ein benutzerdefinierter Routenabgleichscode kann sehr umständlich und schwierig zu schreiben sein.
    • Das Routing bietet unkomplizierte Lösungen für typische Apps. Die meisten Apps erfordern keinen benutzerdefinierten Routenabgleichscode.
  • Endpunkte haben eine Schnittstelle mit Middleware wie UseAuthorization und UseCors.
    • Die Verwendung einer Terminalmiddleware mit UseAuthorization oder UseCors erfordert eine manuelle Verknüpfung mit dem Autorisierungssystem.

Ein Endpunkt definiert beides:

  • Einen Delegaten zum Verarbeiten von Anforderungen.
  • Eine Sammlung beliebiger Metadaten. Die Metadaten werden zur Implementierung von übergreifenden Belangen verwendet, die auf Richtlinien und der Konfiguration basieren, die den einzelnen Endpunkten angefügt sind.

Terminalmiddleware kann sehr nützlich sein, erfordert aber möglicherweise auch:

  • Einen großen Codierungs- und Testaufwand.
  • Eine manuelle Integration in andere Systeme für die gewünschte Flexibilität.

Ziehen Sie daher zunächst die Integration von Routingfunktionen in Betracht, bevor Sie damit beginnen, Terminalmiddleware zu schreiben.

Vorhandene Terminalmiddleware, die in Map oder MapWhen integriert ist, kann in der Regel in einen routingfähigen Endpunkt umgewandelt werden. MapHealthChecks zeigt das Muster für eine Routinglösung:

  • Schreiben Sie eine Erweiterungsmethode in der IEndpointRouteBuilder-Schnittstelle.
  • Erstellen Sie eine geschachtelte Middlewarepipeline mit CreateApplicationBuilder.
  • Fügen Sie die Middleware an die neue Pipeline an. In diesem Fall UseHealthChecks.
  • Verwenden Sie Build, um die Middlewarepipeline in einem RequestDelegate-Delegaten zu erstellen.
  • Rufen Sie Map auf, und stellen Sie die neue Middlewarepipeline bereit.
  • Geben Sie das Generatorobjekt zurück, das von Map aus der Erweiterungsmethode bereitgestellt wurde.

Im folgenden Code ist die Verwendung von MapHealthChecks gezeigt:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

Das vorangehende Beispiel zeigt, warum das Zurückgeben des Generatorobjekts wichtig ist. Wenn das Generatorobjekt zurückgegeben wird, kann der App-Entwickler Richtlinien konfigurieren, z. B. die Autorisierung für den Endpunkt. In diesem Beispiel ist die Middleware für Integritätsprüfungen nicht direkt in das Autorisierungssystem integriert.

Das Metadatensystem wurde als Antwort auf die Probleme erstellt, die von Erweiterbarkeitsautoren mithilfe von Terminalmiddleware aufgetreten sind. Für jede Middleware ist es problematisch, deren eigene Integration in das Autorisierungssystem umzusetzen.

URL-Zuordnung

  • Bei diesem Prozess werden eingehende Anforderungen durch Routing mit einem Endpunkt abgeglichen.
  • Basiert auf Daten im URL-Pfad und den URL-Headern.
  • Kann erweitert werden, um beliebige Daten in der Anforderung zu überprüfen.

Wenn eine Routingmiddleware ausgeführt wird, legt sie ein Endpoint-Element fest und leitet Werte an eine Anforderungsfunktion in der HttpContext-Klasse der aktuellen Anforderung weiter:

  • Durch Aufrufen von HttpContext.GetEndPoint wird der Endpunkt abgerufen.
  • HttpRequest.RouteValues ruft die Sammlung der Routenwerte ab.

Middleware wird nach der Routingmiddleware ausgeführt und kann den Endpunkt erkennen und Maßnahmen ergreifen. So kann beispielsweise eine Autorisierungsmiddleware die Erfassung der Metadaten des Endpunkts für eine Autorisierungsrichtlinie abfragen. Nachdem die gesamte Middleware in der Anforderungsverarbeitungspipeline ausgeführt wurde, wird der Delegat des ausgewählten Endpunkts aufgerufen.

Das Routingsystem ist beim Endpunktrouting für alle Weiterleitungsentscheidungen zuständig. Da die Middleware Richtlinien auf der Grundlage des ausgewählten Endpunkts anwendet, ist Folgendes wichtig:

  • Alle Entscheidungen, die sich auf die Verteilung oder die Anwendung von Sicherheitsrichtlinien auswirken können, werden im Routingsystem getroffen.

Warnung

Wenn für Abwärtskompatibilität ein Controller- oder Razor Pages-Endpunktdelegat ausgeführt wird, werden die Eigenschaften von RouteContext.RouteData auf Grundlage der bisher verarbeiteten Anforderungen auf entsprechende Werte festgelegt.

Der Typ RouteContext wird in einer zukünftigen Version als veraltet markiert:

  • Migrieren Sie RouteData.Values zu HttpRequest.RouteValues.
  • Migrieren Sie RouteData.DataTokens, um IDataTokensMetadata aus den Endpunktmetadaten abzurufen.

Der URL-Abgleich erfolgt in mehreren Phasen und kann konfiguriert werden. In jeder Phase werden mehrere Übereinstimmungen ausgegeben. Diese Übereinstimmungen lassen sich in der nächsten Phase weiter eingrenzen. Die Routingimplementierung garantiert keine Verarbeitungsreihenfolge für übereinstimmende Endpunkte. Alle möglichen Übereinstimmungen werden gleichzeitig verarbeitet. Für die URL-Abgleichsphasen gilt folgende Reihenfolge. ASP.NET Core:

  1. Verarbeitet den URL-Pfad mit mehreren Endpunkten und ihren Routenvorlagen und sammelt alle Übereinstimmungen.
  2. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den angewendeten Routeneinschränkungen passen.
  3. Nimmt die vorangehende Liste und entfernt die Übereinstimmungen, die nicht zu den MatcherPolicy-Instanzen passen.
  4. Verwendet EndpointSelector, um eine abschließende Entscheidung anhand der vorangehenden Liste zu treffen.

Die Liste der Endpunkte wird entsprechend den folgenden Punkten priorisiert:

Alle übereinstimmenden Endpunkte werden in jeder Phase verarbeitet, bis EndpointSelector erreicht ist. EndpointSelector stellt die abschließende Phase dar. Darin wird der Endpunkt mit der höchsten Priorität aus den Übereinstimmungen als beste Übereinstimmung ausgewählt. Wenn es andere Übereinstimmungen mit derselben Priorität wie die beste Übereinstimmung gibt, wird ein Ausnahmefehler wegen einer nicht eindeutigen Übereinstimmung ausgelöst.

Die Routenpriorität wird anhand einer spezifischeren Routenvorlage berechnet, der eine höhere Priorität eingeräumt wird. Dies wird z. B. anhand der Vorlagen /hello und /{message} deutlich:

  • Beide stimmen mit dem URL-Pfad /hello überein.
  • /hello ist spezifischer und hat daher höhere Priorität.

Im Allgemeinen eignet sich die Routenpriorität gut, um die beste Übereinstimmung für die in der Praxis verwendeten URL-Schemata zu finden. Verwenden Sie Order nur bei Bedarf, um eine Mehrdeutigkeit zu vermeiden.

Aufgrund der Erweiterungsmöglichkeiten, die das Routing bietet, kann das Routingsystem die mehrdeutigen Routen nicht im Voraus berechnen. Betrachten Sie ein Beispiel wie die Routenvorlagen /{message:alpha} und /{message:int}:

  • Die alpha-Einschränkung gleicht nur alphabetische Zeichen ab.
  • Die int-Einschränkung gleicht nur Zahlen ab.
  • Diese Vorlagen haben die gleiche Routenpriorität, aber es gibt keine einzige URL, auf die sie beide zutreffen.
  • Wenn das Routingsystem beim Starten einen Mehrdeutigkeitsfehler gemeldet hat, würde dieser den gültigen Anwendungsfall blockieren.

Warnung

Die Reihenfolge der Vorgänge in UseEndpoints wirkt sich nicht auf das Routingverhalten aus, mit einer Ausnahme. MapControllerRoute und MapAreaRoute weisen ihren Endpunkten automatisch einen Reihenfolgenwert zu, basierend auf der Reihenfolge, in der sie aufgerufen werden. Dadurch wird das Langzeitverhalten von Controllern simuliert, ohne dass das Routingsystem die gleichen Garantien bietet wie ältere Routingimplementierungen.

Bei der bisherigen Routingimplementierung war es möglich, eine Routingerweiterung vorzunehmen, die von der Verarbeitungsreihenfolge der Routen abhängt. Endpunktrouting in ASP.NET Core 3.0 und höher:

  • Weist kein Konzept für Routen auf.
  • Bietet keine garantierte Reihenfolge. Alle Endpunkte werden gleichzeitig verarbeitet.

Routenvorlagenpriorität und Reihenfolge der Endpunktauswahl

Die Routenvorlagenpriorität ist ein System, bei dem jeder Routenvorlage ein Wert zugewiesen wird, je nachdem, wie spezifisch diese ist. Routenvorlagenpriorität:

  • Verhindert, dass die Reihenfolge der Endpunkte häufig angepasst werden muss.
  • Versucht, die allgemeinen Erwartungen an das Routingverhalten abzugleichen.

Dies wird z. B. anhand der Vorlagen /Products/List und /Products/{id} deutlich. Es wäre begründet, anzunehmen, dass /Products/List eine bessere Übereinstimmung als /Products/{id} für den URL-Pfad /Products/List ist. Dies funktioniert, weil das Literalsegment /List eine höhere Priorität als das Parametersegment /{id} hat.

Wie die Priorisierung im Einzelnen funktioniert, ist an die Definition der Routenvorlagen gekoppelt:

  • Vorlagen mit mehr Segmenten sind in der Regel spezifischer.
  • Ein Segment mit Literaltext gilt als spezifischer als ein Parametersegment.
  • Ein Parametersegment mit einer Einschränkung gilt als spezifischer als ein Parametersegment ohne Einschränkung.
  • Ein komplexes Segment wird als genauso spezifisch betrachtet wie ein Parametersegment mit einer Einschränkung.
  • Catch-All-Parameter, die am wenigsten spezifisch sind. Unter Catch-All in der Referenz zu Routenvorlagen finden Sie wichtige Informationen zu Catch-All-Routen.

Einen Verweis auf genaue Werte finden Sie im Quellcode auf GitHub.

Konzepte zur URL-Generierung

URL-Generierung:

  • Der Prozess, bei dem durch Routing ein URL-Pfad basierend auf mehreren Routenwerten erstellt wird.
  • Sie ermöglicht eine logische Trennung zwischen den Endpunkten und den URLs, die auf diese zugreifen.

Das Endpunktrouting umfasst die API zur Linkgenerierung (LinkGenerator). LinkGenerator ist ein Singleton-Dienst, der in DI verfügbar ist. Die LinkGenerator-API kann außerhalb des Kontexts einer ausgeführten Anforderung verwendet werden. Mvc.IUrlHelper und Szenarios, die von IUrlHelper abhängig sind (z. B. Taghilfsprogramme, HTML-Hilfsprogramme und Aktionsergebnisse), verwenden die LinkGenerator-API, um entsprechende Funktionen bereitzustellen.

Die API zur Linkgenerierung wird von Konzepten wie Adressen und Adressschemas unterstützt. Sie können mithilfe eines Adressschemas die Endpunkte bestimmen, die bei der Linkgenerierung berücksichtigt werden sollen. Beispielsweise werden Routennamen und Routenwerte als Adressschemas implementiert. Diese Szenarios kennen viele Benutzer von Controllern und Razor Pages.

Die API zur Linkgenerierung kann Controller und Razor Pages über die folgenden Erweiterungsmethoden miteinander verknüpfen:

Beim Überladen dieser Methoden werden Argumente akzeptiert, die den HttpContext umfassen. Diese Methoden sind zwar in funktionaler Hinsicht äquivalent zu Url.Action und Url.Page, bieten aber zusätzliche Flexibilität und Optionen.

Die GetPath*-Methoden sind Url.Action und Url.Page in der Hinsicht ähnlich, dass sie einen URI mit einem absoluten Pfad generieren. Die GetUri*-Methoden generieren immer einen absoluten URI mit einem Schema und einem Host. Die Methoden, die einen HttpContext akzeptieren, generieren im Kontext der ausgeführten Anforderung einen URI. Die Umgebungsroutenwerte, der URL-basierte Pfad, das Schema und der Host von der ausführenden Anforderung werden so lange verwendet, bis sie außer Kraft gesetzt werden.

LinkGenerator wird mit einer Adresse aufgerufen. Ein URI wird in zwei Schritten generiert:

  1. Eine Adresse wird an eine Liste von Endpunkten gebunden, die der Adresse zugeordnet werden können.
  2. Jedes RoutePattern eines Endpunkts wird bewertet, bis ein Routenmuster gefunden wird, das den angegebenen Werten zugeordnet werden kann. Die daraus resultierende Ausgabe wird mit URI-Teilen kombiniert, die für die API zur Linkgenerierung bereitgestellt wird, und zurückgegeben.

Die von LinkGenerator bereitgestellten Methoden unterstützen die Standardfunktionen zur Generierung von Links für jeden beliebigen Adresstypen. Am praktischsten ist es, die API zur Linkgenerierung mit Erweiterungsmethoden zu verwenden, die Vorgänge für einen bestimmten Adresstypen ausführen:

Erweiterungsmethode Beschreibung
GetPathByAddress Generiert einen URI mit einem absoluten Pfad, der auf den angegebenen Werten basiert.
GetUriByAddress Generiert einen absoluten URI, der auf den angegebenen Werten basiert.

Warnung

Beachten Sie die folgenden Implikationen zum Aufrufen von LinkGenerator-Methoden:

  • Verwenden Sie GetUri*-Erweiterungsmethoden in App-Konfigurationen, die den Host-Header von eingehenden Anforderungen nicht überprüfen, mit Bedacht. Wenn der Host-Header von eingehenden Anforderungen nicht überprüft wird, können nicht vertrauenswürdige Anforderungseingaben zurück an den Client in URIs einer Ansicht bzw. Seite zurückgesendet werden. Es wird empfohlen, dass alle Produktions-Apps ihren Server so konfigurieren, dass der Host-Header auf bekannte gültige Werte überprüft wird.

  • Verwenden Sie LinkGenerator in Kombination mit Map oder MapWhen in Middleware mit Bedacht. Map* ändert den Basispfad der ausgeführten Anforderung. Dies beeinflusst die Ausgabe der Linkgenerierung. Für alle LinkGenerator-APIs ist die Angabe eines Basispfads zulässig. Geben Sie einen leeren Basispfad an, um die Auswirkungen von Map* auf die Linkgenerierung rückgängig zu machen.

Middlewarebeispiel

Im folgenden Beispiel verwendet eine Middleware die LinkGenerator-API, um eine Verknüpfung zu einer Aktionsmethode herzustellen, die Speicherprodukte aufführt. Sie können für jede beliebige Klasse in einer App die API zur Linkgenerierung verwenden, indem Sie diese in eine Klasse einfügen und GenerateLink aufrufen:

public class ProductsLinkMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        var url = _linkGenerator.GetPathByAction("ListProducts", "Store");

        httpContext.Response.ContentType = "text/plain";

        await httpContext.Response.WriteAsync($"Go to {url} to see our products.");
    }
}

Referenz für Routenvorlagen

Token in {} definieren Routenparameter, die beim Abgleich der Route gebunden werden. In einem Routensegment können mehrere Routenparameter definiert werden, müssen aber durch einen Literalwert getrennt werden. {controller=Home}{action=Index} ist z.B. keine gültige Route, da sich zwischen {controller} und {action} kein Literalwert befindet. Routenparameter müssen einen Namen besitzen und können zusätzliche Attribute aufweisen.

Eine Literalzeichenfolge, die nicht den Routenparametern entspricht (z.B. {id}), muss zusammen mit dem Pfadtrennzeichen / mit dem URL-Text übereinstimmen. Beim Abgleich von Text wird nicht zwischen Groß-/Kleinbuchstaben unterschieden, und die Übereinstimmung basiert auf der decodierten Repräsentation des URL-Pfads. Damit das Trennzeichen ({ oder }) der Routenparameter-Literalzeichenfolge bei einem Abgleich gefunden wird, muss es doppelt vorhanden sein, was einem Escapezeichen entspricht. Beispielsweise {{ oder }}.

Sternchen * oder Doppelsternchen **:

  • Kann als Präfix für einen Routenparameter verwendet werden, um an den rest des URI zu binden.
  • Werden Catch-All-Parameter genannt. Zum Beispiel blog/{**slug}:
    • Entspricht einem beliebigen URI, der mit /blog beginnt und einem beliebigen Wert folgt.
    • Der Wert nach /blog wird dem Slug-Routenwert zugewiesen.

Warnung

Ein catch-all-Parameter kann aufgrund eines Fehlers beim Routing nicht ordnungsgemäß mit Routen übereinstimmen. Apps, die von diesem Fehler betroffen sind, weisen die folgenden Merkmale auf:

  • Eine catch-all-Route, zum Beispiel {**slug}"
  • Die catch-all-Route kann nicht mit Anforderungen abgeglichen werden, die abgeglichen werden sollen.
  • Durch das Entfernen anderer Routen funktioniert die catch-all-Route.

Weitere Beispiele zu diesem Fehler finden Sie in den GitHub-Issues 18677 und 16579.

Eine Opt-in-Behebung für diesen Fehler ist im .NET Core 3.1.301 SDK und höher enthalten. Der folgende Code legt einen internen Switch fest, mit dem dieser Fehler behoben wird:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

Durch Catch-All-Parameter können auch leere Zeichenfolgen gefunden werden.

Der Catch-All-Parameter schützt die entsprechenden Zeichen (Escaping), wenn die Route verwendet wird, um eine URL, einschließlich Pfadtrennzeichen (/) zu generieren. Z.B. generiert die Route foo/{*path} mit den Routenwerten { path = "my/path" }foo/my%2Fpath. Beachten Sie den umgekehrten Schrägstrich mit Escapezeichen. Um Trennzeichen für Roundtrips einsetzen zu können, verwenden Sie das Routenparameterpräfix **. Die Route foo/{**path} mit { path = "my/path" } generiert foo/my/path.

Bei einem URL-Muster, durch das ein Dateiname mit einer optionalen Erweiterung erfasst werden soll, sind noch weitere Aspekte zu berücksichtigen. Dies wird z.B. anhand der Vorlage files/{filename}.{ext?} deutlich. Wenn sowohl für filename als auch für ext Werte vorhanden sind, werden beide Werte angegeben. Wenn nur für filename ein Wert in der URL vorhanden ist, wird für die Route eine Übereinstimmung ermittelt, da der nachstehende Punkt (.) optional ist. Für die folgenden URLs wird eine Übereinstimmung für die Route ermittelt:

  • /files/myFile.txt
  • /files/myFile

Routenparameter können über mehrere Standardwerte verfügen, die nach dem Parameternamen angegeben werden und durch ein Gleichheitszeichen (=) voneinander getrennt werden. Mit {controller=Home} wird beispielsweise Home als Standardwert für controller definiert. Der Standardwert wird verwendet, wenn kein Wert in der Parameter-URL vorhanden ist. Routenparameter sind optional, wenn am Ende des Parameternamens ein Fragezeichen (?) angefügt wird. Beispielsweise id?. Zwischen optionalen Werten und Standardroutenparametern besteht folgender Unterschied:

  • Ein Routenparameter mit einem Standardwert erzeugt immer einen Wert.
  • Ein optionaler Parameter hat nur dann einen Wert, wenn ein Wert von der Anforderungs-URL bereitgestellt wird.

Routenparameter können Einschränkungen aufweisen, die mit dem gebundenen Routenwert der URL übereinstimmen müssen. Eine Inline-Einschränkung für einen Routenparameter geben Sie an, indem Sie hinter dem Namen des Routenparameters einen Doppelpunkt (:) und einen Einschränkungsnamen hinzufügen. Wenn für die Einschränkung Argumente erforderlich sind, werden diese nach dem Einschränkungsnamen in Klammern ((...)) eingeschlossen. Mehrere Inline-Einschränkungen können festgelegt werden, indem ein weiterer Doppelpunkt (:) und Einschränkungsname hinzugefügt werden.

Der Einschränkungsname und die Argumente werden dem IInlineConstraintResolver-Dienst übergeben, wodurch eine Instanz von IRouteConstraint für die URL-Verarbeitung erstellt werden kann. In der Routenvorlage blog/{article:minlength(10)} wird beispielsweise die Einschränkung minlength mit dem Argument 10 festgelegt. Weitere Informationen zu Routeneinschränkungen und eine Liste der vom Framework bereitgestellten Einschränkungen finden Sie im Abschnitt Referenz zu Routeneinschränkungen.

Routenparameter können darüber hinaus über Parametertransformatoren verfügen. Diese wandeln den Wert eines Parameters beim Generieren von Links um und passen Aktionen und Seiten an URLs an. Wie Einschränkungen können auch Parametertransformatoren einem Routenparameter inline hinzugefügt werden, indem ein Doppelpunkt (:) und der Name des Transformators hinter dem Namen des Routenparameters hinzugefügt werden. In der Routenvorlage blog/{article:slugify} wird beispielsweise der Transformator slugify festgelegt. Weitere Informationen zu Parametertransformatoren finden Sie im Abschnitt Parametertransformatorreferenz.

Die folgende Tabelle enthält Beispielvorlagen für Routen und deren Verhalten:

Routenvorlage Beispiel-URI für Übereinstimmung Der Anforderungs-URI
hello /hello Nur für den Pfad /hello wird eine Übereinstimmung ermittelt.
{Page=Home} / Eine Übereinstimmung wird ermittelt, und Page wird auf Home festgelegt.
{Page=Home} /Contact Eine Übereinstimmung wird ermittelt, und Page wird auf Contact festgelegt.
{controller}/{action}/{id?} /Products/List Stimmt mit dem Products-Controller und der List-Aktion überein.
{controller}/{action}/{id?} /Products/Details/123 Wird dem Controller Products und der Aktion Details zugeordnet, bei der id auf 123 festgelegt ist.
{controller=Home}/{action=Index}/{id?} / Stimmt mit dem Home-Controller und der Index-Methode überein. id wird ignoriert.
{controller=Home}/{action=Index}/{id?} /Products Stimmt mit dem Products-Controller und der Index-Methode überein. id wird ignoriert.

Mit Vorlagen lässt sich Routing besonders leicht durchführen. Einschränkungen und Standardwerte können auch außerhalb der Routenvorlage angegeben werden.

Komplexe Segmente

Komplexe Segmente werden von rechts nach links auf eine nicht gierige Weise durch entsprechende Literaltrennzeichen verarbeitet. Beispielsweise ist [Route("/a{b}c{d}")] ein komplexes Segment. Komplexe Segmente funktionieren auf eine bestimmte Weise, die für eine erfolgreiche Verwendung verstanden werden muss. Das Beispiel in diesem Abschnitt zeigt, warum komplexe Segmente nur dann wirklich gut funktionieren, wenn der Trennzeichentext nicht innerhalb der Parameterwerte erscheint. Für komplexere Fälle ist die Verwendung eines RegEx und das anschließende manuelle Extrahieren der Werte erforderlich.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Dies ist eine Zusammenfassung der Schritte, die beim Routing mit der Vorlage /a{b}c{d} und dem URL-Pfad /abcd ausgeführt werden. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen:

  • Das erste Literal von rechts nach links ist c. Daher wird /abcd von rechts durchsucht und /ab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /ab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Es ist kein verbleibender Text und keine verbleibende Routenvorlage vorhanden. Folglich ist dies eine Übereinstimmung.

Nachfolgend ist ein Beispiel für einen negativen Fall mit derselben Vorlage /a{b}c{d} und dem URL-Pfad /aabcd. | wird verwendet, um die Funktionsweise des Algorithmus besser zu veranschaulichen: Bei diesem Fall handelt es sich nicht um eine Übereinstimmung, was durch denselben Algorithmus belegt wird:

  • Das erste Literal von rechts nach links ist c. Daher wird /aabcd von rechts durchsucht und /aab|c|d gefunden.
  • Alles auf der rechten Seite (d) ist jetzt mit dem Routenparameter {d} abgeglichen.
  • Das nächste Literal von rechts nach links ist a. Also wird /aab|c|d dort gesucht, wo die Suche unterbrochen wurde, und dann wird a in /a|a|b|c|d gefunden.
  • Der Wert auf der rechten Seite (b) ist jetzt mit dem Routenparameter {b} abgeglichen.
  • Zu diesem Zeitpunkt gibt es noch verbleibenden Text a, aber es gibt keine Routenvorlage mehr, die der Algorithmus analysieren kann, weshalb dies keine Übereinstimmung ist.

Da der übereinstimmende Algorithmus nicht gierig ist:

  • Entspricht er der kleinstmöglichen Textmenge in jedem Schritt.
  • Alle Fälle, in denen der Trennzeichenwert in den Parameterwerten angezeigt wird, stimmen nicht überein.

Reguläre Ausdrücke bieten eine viel bessere Kontrolle über das Abgleichsverhalten.

Beim gierigen Abgleich, auch als Lazy Matching bezeichnet, wird die größtmögliche Zeichenfolge abgeglichen. Beim nicht gierigen Abgleich ist dies die kürzeste Zeichenfolge.

Referenz für Routeneinschränkungen

Routeneinschränkungen werden angewendet, wenn eine Übereinstimmung mit der eingehenden URL gefunden wurde und der URL-Pfad in Routenwerten mit Token versehen wird. In der Regel wird mit Routeneinschränkungen der Routenwert der zugehörigen Vorlage geprüft. Dabei wird anhand einer True/False-Entscheidung bestimmt, ob der Wert gültig ist. Für einige Routeneinschränkungen werden anstelle des Routenwerts andere Daten verwendet, um zu ermitteln, ob das Routing einer Anforderung möglich ist. HttpMethodRouteConstraint kann beispielsweise auf der Grundlage des HTTP-Verbs eine Anforderung entweder annehmen oder ablehnen. Einschränkungen werden in Routinganforderungen und bei der Linkgenerierung verwendet.

Warnung

Verwenden Sie keine Einschränkungen für die Eingabeüberprüfung. Wenn Einschränkungen für die Eingabevalidierung verwendet werden, führt eine ungültige Eingabe zu einem 404-Fehler (Nicht gefunden). Eine ungültige Eingabe sollte zu einer ungültigen Anforderung (400) mit einer entsprechenden Fehlermeldung führen. Verwenden Sie Routeneinschränkungen nicht, um Eingaben für eine bestimmte Route zu überprüfen, sondern um ähnliche Routen zu unterscheiden.

In der folgenden Tabelle werden Beispiele für Routeneinschränkungen und deren zu erwartendes Verhalten beschrieben:

Einschränkung Beispiel Beispiele für Übereinstimmungen Hinweise
int {id:int} 123456789, -123456789 Für jeden Integer wird eine Übereinstimmung ermittelt.
bool {active:bool} true, FALSE Entspricht true oder false. Groß-/Kleinschreibung nicht beachten
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm Entspricht einem gültigen DateTime-Wert in der invarianten Kultur. Siehe vorherige Warnung.
decimal {price:decimal} 49.99, -1,000.01 Entspricht einem gültigen decimal-Wert in der invarianten Kultur. Siehe vorherige Warnung.
double {weight:double} 1.234, -1,001.01e8 Entspricht einem gültigen double-Wert in der invarianten Kultur. Siehe vorherige Warnung.
float {weight:float} 1.234, -1,001.01e8 Entspricht einem gültigen float-Wert in der invarianten Kultur. Siehe vorherige Warnung.
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 Für einen gültigen Guid-Wert wird eine Übereinstimmung ermittelt.
long {ticks:long} 123456789, -123456789 Für einen gültigen long-Wert wird eine Übereinstimmung ermittelt.
minlength(value) {username:minlength(4)} Rick Die Zeichenfolge muss mindestens eine Länge von 4 Zeichen aufweisen.
maxlength(value) {filename:maxlength(8)} MyFile Die Zeichenfolge darf maximal eine Länge von 8 Zeichen aufweisen.
length(length) {filename:length(12)} somefile.txt Die Zeichenfolge muss genau 12 Zeichen aufweisen.
length(min,max) {filename:length(8,16)} somefile.txt Die Zeichenfolge muss mindestens eine Länge von 8 und darf maximal eine Länge von 16 Zeichen aufweisen.
min(value) {age:min(18)} 19 Der Integerwert muss mindestens 18 sein.
max(value) {age:max(120)} 91 Der Integerwert darf nicht größer als 120 sein.
range(min,max) {age:range(18,120)} 91 Der Integerwert muss zwischen 18 und 120 liegen.
alpha {name:alpha} Rick Die Zeichenfolge muss aus mindestens einem alphabetische Zeichen bestehen, a-z und ohne Unterscheidung zwischen Groß-/Kleinbuchstaben.
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 Die Zeichenfolge muss mit dem regulären Ausdruck übereinstimmen. Weitere Informationen finden Sie unter Tipps zum Definieren eines regulären Ausdrucks.
required {name:required} Rick Hierdurch wird erzwungen, dass ein Wert, der kein Parameter ist, für die URL-Generierung vorhanden sein muss.

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Auf einen einzelnen Parameter können mehrere durch Doppelpunkte getrennte Einschränkungen angewendet werden. Durch die folgende Einschränkung wird ein Parameter beispielsweise auf einen Integerwert größer oder gleich 1 beschränkt:

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

Warnung

Für Routeneinschränkungen, mit denen die URL überprüft wird und die in den CLR-Typ umgewandelt werden, wird immer die invariante Kultur verwendet. Dies gilt z. B. für die Konvertierung in den CLR-Typ int oder DateTime. Diese Einschränkungen setzen voraus, dass die URL nicht lokalisierbar ist. Die vom Framework bereitgestellten Routeneinschränkungen ändern nicht die Werte, die in Routenwerten gespeichert sind. Alle Routenwerte, die aus der URL analysiert werden, werden als Zeichenfolgen gespeichert. Durch die float-Einschränkung wird beispielsweise versucht, den Routenwert in einen Gleitkommawert zu konvertieren. Mit dem konvertierten Wert wird allerdings nur überprüft, ob eine Umwandlung überhaupt möglich ist.

Reguläre Ausdrücke in Einschränkungen

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Reguläre Ausdrücke können mithilfe der regex(...)-Routeneinschränkung als Inline-Einschränkungen angegeben werden. Methoden der MapControllerRoute-Familie akzeptieren auch ein Objektliteral von Einschränkungen. Wenn dieses Formular verwendet wird, werden Zeichenfolgenwerte als reguläre Ausdrücke interpretiert.

Der folgende Code verwendet eine Inline-RegEx-Einschränkung:

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
        context => 
        {
            return context.Response.WriteAsync("inline-constraint match");
        });
 });

Der folgende Code verwendet ein Objektliteral, um eine RegEx-Einschränkung anzugeben:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "people",
        pattern: "People/{ssn}",
        constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
        defaults: new { controller = "People", action = "List", });
});

Im ASP.NET Core-Framework wird dem Konstruktor für reguläre Ausdrücke RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant hinzugefügt. Eine Beschreibung dieser Member finden Sie unter RegexOptions.

In regulären Ausdrücken werden Trennzeichen und Token verwendet, die auch beim Routing und in der Programmiersprache C# in ähnlicher Weise verwendet werden. Token, die reguläre Ausdrücke enthalten, müssen mit einem Escapezeichen versehen werden. Wenn Sie den regulären Ausdruck ^\d{3}-\d{2}-\d{4}$ in einer Inline-Einschränkung verwenden möchten, nutzen Sie eine der folgenden Optionen:

  • Ersetzen Sie \-Zeichen in der Zeichenfolge durch \\-Zeichen in der C#-Quelldatei, um das Escapezeichen für die Zeichenfolge \ zu setzen.
  • Ausführliche Zeichenfolgeliterale

Wenn Sie Trennzeichen für Routenparameter mit Escapezeichen versehen möchten ({, }, [, ]), geben Sie jedes Zeichen im Ausdruck doppelt ein (z. B. {{, }}, [[, ]]). In der folgenden Tabelle werden reguläre Ausdrücke und Ausdrücke aufgeführt, die mit Escapezeichen versehen sind:

Regulärer Ausdruck Mit Escapezeichen versehener regulärer Ausdruck
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

Beim Routing verwendete reguläre Ausdrücke beginnen oft mit dem ^-Zeichen und stellen die Startposition der Zeichenfolge dar. Die Ausdrücke enden häufig mit einem Dollarzeichen ($) und stellen das Ende der Zeichenfolge dar. Mit den Zeichen ^ und $ wird sichergestellt, dass der reguläre Ausdruck mit dem vollständigen Routenparameterwert übereinstimmt. Ohne die Zeichen ^ und $ werden mit dem regulären Ausdruck alle Teilzeichenfolgen ermittelt, was häufig nicht gewünscht ist. In der folgenden Tabelle finden Sie Beispiele für reguläre Ausdrücke. Außerdem wird erklärt, warum ein Abgleich erfolgreich ist oder fehlschlägt:

expression Zeichenfolge Match Kommentar
[a-z]{2} hello Ja Teilzeichenfolge stimmt überein
[a-z]{2} 123abc456 Ja Teilzeichenfolge stimmt überein
[a-z]{2} mz Ja Ausdruck stimmt überein
[a-z]{2} MZ Ja keine Unterscheidung zwischen Groß-/Kleinbuchstaben
^[a-z]{2}$ hello Nein siehe Erläuterungen zu ^ und $ oben
^[a-z]{2}$ 123abc456 Nein siehe Erläuterungen zu ^ und $ oben

Weitere Informationen zur Syntax von regulären Ausdrücken finden Sie unter Sprachelemente für reguläre Ausdrücke – Kurzübersicht.

Einen regulären Ausdruck können Sie verwenden, um einen Parameter auf zulässige Werte einzuschränken. Mit {action:regex(^(list|get|create)$)} werden beispielsweise für den action-Routenwert nur die Werte list, get oder create abgeglichen. Wenn die Zeichenfolge ^(list|get|create)$ dem Einschränkungswörterbuch übergeben wird, führt dies zum gleichen Ergebnis. Auch Einschränkungen, die dem zugehörigen Wörterbuch hinzugefügt werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden als reguläre Ausdrücke behandelt. Einschränkungen, die innerhalb einer Vorlage übergeben werden und mit keiner vorgegebenen Einschränkung übereinstimmen, werden nicht als reguläre Ausdrücke behandelt.

Benutzerdefinierte Routeneinschränkungen

Benutzerdefinierte Routeneinschränkungen können durch Implementierung der IRouteConstraint-Schnittstelle erstellt werden. Die IRouteConstraint-Schnittstelle umfasst die Match-Methode, die true zurückgibt, wenn die Einschränkung erfüllt wird, und andernfalls false.

Benutzerdefinierte Routeneinschränkungen werden nur selten benötigt. Bevor Sie eine benutzerdefinierte Routeneinschränkung implementieren, sollten Sie Alternativen in Betracht ziehen, wie z. B. Modellbindung.

Der ASP.NET Core-Ordner Constraints bietet nützliche Beispiele für die Erstellung von Einschränkungen. Beispiel: GuidRouteConstraint.

Zum Verwenden eines benutzerdefinierten IRouteConstraint-Elements muss der Routeneinschränkungstyp bei der ConstraintMap-Eigenschaft der App im Dienstcontainer registriert werden. Eine ConstraintMap ist ein Wörterbuch, das Routeneinschränkungsschlüssel IRouteConstraint-Implementierungen zuordnet, die diese Einschränkungen überprüfen. Die ConstraintMap einer App kann in Startup.ConfigureServices entweder als Teil eines services.AddRouting-Aufrufs oder durch direktes Konfigurieren von RouteOptions mit services.Configure<RouteOptions> aktualisiert werden. Zum Beispiel:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
    });
}

Die vorangehende Einschränkung wird im folgenden Code angewendet:

[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    // GET /api/test/3
    [HttpGet("{id:customName}")]
    public IActionResult Get(string id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // GET /api/test/my/3
    [HttpGet("my/{id:customName}")]
    public IActionResult Get(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

MyDisplayRouteInfo wird von dem NuGet-Paket Rick.Docs.Samples.RouteInfo bereitgestellt und zeigt Routeninformationen an.

Die Implementierung von MyCustomConstraint verhindert die Anwendung von 0 auf einen Routenparameter:

class MyCustomConstraint : IRouteConstraint
{
    private Regex _regex;

    public MyCustomConstraint()
    {
        _regex = new Regex(@"^[1-9]*$",
                            RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
                            TimeSpan.FromMilliseconds(100));
    }
    public bool Match(HttpContext httpContext, IRouter route, string routeKey,
                      RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out object value))
        {
            var parameterValueString = Convert.ToString(value,
                                                        CultureInfo.InvariantCulture);
            if (parameterValueString == null)
            {
                return false;
            }

            return _regex.IsMatch(parameterValueString);
        }

        return false;
    }
}

Warnung

Übergeben Sie ein Timeout, wenn Sie System.Text.RegularExpressions zum Verarbeiten nicht vertrauenswürdiger Eingaben verwenden. Ein böswilliger Benutzer kann Eingaben für RegularExpressions angeben, um einen Denial-of-Service-Angriff durchzuführen. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Der vorangehende Code:

  • Verhindert, dass 0 im {id}-Segment der Route vorhanden ist.
  • Dient als einfaches Beispiel für die Implementierung einer benutzerdefinierten Einschränkung. Es sollte nicht in einer Produktions-App eingesetzt werden.

Der folgende Code bietet einen besseren Ansatz, um zu verhindern, dass eine id mit einer 0 verarbeitet wird:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return ControllerContext.MyDisplayRouteInfo(id);
}

Der vorangehende Code bietet im Vergleich zum MyCustomConstraint-Ansatz folgende Vorteile:

  • Eine benutzerdefinierte Einschränkung ist nicht erforderlich.
  • Es wird ein beschreibender Fehler zurückgegeben, wenn der Routenparameter 0 enthält.

Parametertransformatorreferenz

Parametertransformatoren:

Beispielsweise generiert ein benutzerdefinierter Parametertransformator slugify im Routenmuster blog\{article:slugify} mit Url.Action(new { article = "MyTestArticle" })blog\my-test-article.

Betrachten Sie die folgende Implementierung von IOutboundParameterTransformer:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString(), 
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}

Um einen Parametertransformator in einem Routenmuster zu verwenden, konfigurieren Sie ihn mit ConstraintMap in Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}

Das ASP.NET Core-Framework verwendet Parametertransformatoren, um den URI zu transformieren, zu dem ein Endpunkt aufgelöst wird. Beispielsweise wandeln Parametertransformatoren die Routenwerte um, die zum Zuordnen folgender Elemente verwendet werden: area, controller, action und page.

routes.MapControllerRoute(
    name: "default",
    template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");

Mit der vorstehenden Routenvorlage wird die Aktion SubscriptionManagementController.GetAll dem URI /subscription-management/get-all zugeordnet. Ein Parametertransformator ändert nicht die zum Generieren eines Links verwendeten Routenwerte. Beispielsweise gibt Url.Action("GetAll", "SubscriptionManagement")/subscription-management/get-all aus.

ASP.NET Core bietet API-Konventionen für die Verwendung von Parametertransformatoren mit generierten Routen:

Referenz für URL-Generierung

Dieser Abschnitt enthält eine Referenz für den Algorithmus, der durch die URL-Generierung implementiert wird. In der Praxis werden bei den meisten komplexen Beispielen für die URL-Generierung Controller oder Razor Pages verwendet. Weitere Informationen finden Sie unter Routing in Controllern.

Die URL-Generierung beginnt mit einem Aufruf von LinkGenerator.GetPathByAddress oder einer ähnlichen Methode. Die Methode wird mit einer Adresse, mehreren Routenwerten und optional mit Informationen zur aktuellen Anforderung von HttpContext versehen.

Im ersten Schritt wird die Adresse verwendet, um bestimmte Endpunktkandidaten mithilfe einer IEndpointAddressScheme<TAddress>-Schnittstelle aufzulösen, die dem Adresstyp entspricht.

Sobald eine Kandidatengruppe anhand des Adressschemas gefunden wurde, werden die Endpunkte geordnet und iterativ verarbeitet, bis die URL-Generierung erfolgreich abgeschlossen ist. Bei der URL-Generierung wird nicht auf Mehrdeutigkeiten geprüft, daher ist das erste zurückgegebene Ergebnis das Endergebnis.

Behandeln von Problemen mit der Protokollierung bei der URL-Generierung

Der erste Schritt bei der Behebung von Problemen bei der URL-Generierung ist die Einstellung des Protokolliergrads von Microsoft.AspNetCore.Routing auf TRACE. LinkGenerator protokolliert viele Details über die Verarbeitung, die bei der Problembehebung nützlich sein können.

Ausführliche Informationen zur URL-Generierung finden Sie unter Referenz für URL-Generierung.

Adressen

Mithilfe von Adressen wird bei der URL-Generierung ein Aufruf in der API zur Linkgenerierung an mehrere Endpunktkandidaten gebunden.

Adressen sind ein erweiterbares Konzept, das standardmäßig mit zwei Implementierungen bereitgestellt wird:

  • Die Verwendung von Endpunktname (string) als Adresse:
    • Bietet ähnliche Funktionalität wie der Routenname von MVC.
    • Wird der IEndpointNameMetadata-Metadatentyp verwendet.
    • Löst die bereitgestellte Zeichenfolge anhand der Metadaten aller registrierten Endpunkte auf.
    • Löst beim Start eine Ausnahme aus, wenn mehrere Endpunkte den gleichen Namen aufweisen.
    • Wird für die allgemeine Verwendung außerhalb von Controllern und Razor Pages empfohlen.
  • Die Verwendung von Routenwerten (RouteValuesAddress) als Adresse:
    • Bietet eine ähnliche Funktionalität wie die veraltete Funktion zur URL-Generierung von Controllern und Razor Pages.
    • Lässt sich nur schwer erweitern und debuggen.
    • Bietet die Implementierung, die von IUrlHelper, Taghilfsprogrammen, HTML-Hilfsprogrammen, Aktionsergebnissen usw. verwendet wird.

Aufgabe des Adressschemas ist es, die Verbindung zwischen der Adresse und den übereinstimmenden Endpunkten anhand von beliebigen Kriterien herzustellen:

  • Das Schema für Endpunktenamen führt eine allgemeine Wörterbuchsuche durch.
  • Das Schema der Routenwerte weist eine komplexe beste Teilmenge des Mengenalgorithmus auf.

Umgebungswerte und explizite Werte

Aus der aktuellen Anforderung greift das Routing auf die Routenwerte der aktuellen Anforderung HttpContext.Request.RouteValues zu. Die mit der aktuellen Anforderung verbundenen Werte werden als Umgebungswerte bezeichnet. Aus Gründen der Übersichtlichkeit werden in der Dokumentation die an die Methoden übergebenen Routenwerte als explizite Werte bezeichnet.

Das folgende Beispiel zeigt Umgebungswerte und explizite Werte. Er liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte: { id = 17, }:

public class WidgetController : Controller
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public IActionResult Index()
    {
        var url = _linkGenerator.GetPathByAction(HttpContext,
                                                 null, null,
                                                 new { id = 17, });
        return Content(url);
    }

Der vorangehende Code:

Der folgende Code liefert keine Umgebungswerte und keine expliziten Werte: { controller = "Home", action = "Subscribe", id = 17, }:

public IActionResult Index2()
{
    var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
                                             new { id = 17, });
    return Content(url);
}

Die vorhergehende Methode gibt /Home/Subscribe/17 zurück.

Der folgende Code in WidgetController gibt /Widget/Subscribe/17 zurück:

var url = _linkGenerator.GetPathByAction("Subscribe", null,
                                         new { id = 17, });

Der folgende Code stellt den Controller aus den Umgebungswerten in der aktuellen Anforderung und explizite Werte dar: { action = "Edit", id = 17, }:

public class GadgetController : Controller
{
    public IActionResult Index()
    {
        var url = Url.Action("Edit", new { id = 17, });
        return Content(url);
    }

Für den Code oben gilt:

  • /Gadget/Edit/17 wird zurückgegeben.
  • Url ruft die IUrlHelper-Schnittstelle ab.
  • Action generiert eine URL mit einem absoluten Pfad für eine Aktionsmethode. Die URL enthält den angegebenen action-Namen und route-Werte.

Sie liefert Umgebungswerte aus der aktuellen Anforderung und explizite Werte: { page = "./Edit, id = 17, }:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var url = Url.Page("./Edit", new { id = 17, });
        ViewData["URL"] = url;
    }
}

Im vorangehenden Code wird url auf /Edit/17 festgelegt, wenn die Option zum Bearbeiten der Razor Page die folgende Seitenanweisung enthält:

@page "{id:int}"

Wenn die Routenvorlage "{id:int}" nicht in der Seite „Bearbeiten“ enthalten ist, ist url gleich /Edit?id=17.

Das Verhalten der IUrlHelper-Schnittstelle von MVC fügt zusätzlich zu den hier beschriebenen Regeln eine weitere Komplexitätsebene hinzu:

  • IUrlHelper liefert immer die Routenwerte aus der aktuellen Anforderung als Umgebungswerte.
  • IUrlHelper.action kopiert immer die aktuellen Routenwerte action und controller als explizite Werte, sofern sie nicht vom Entwickler außer Kraft gesetzt werden.
  • IUrlHelper.page kopiert immer den aktuellen Routenwert page als expliziten Wert, sofern er nicht außer Kraft gesetzt wird.
  • IUrlHelper.Page setzt immer den aktuellen Routenwert handler mit null als expliziten Wert außer Kraft, sofern er nicht außer Kraft gesetzt wird.

Benutzer sind oft von den Verhaltensdetails der Umgebungswerte überrascht, da MVC anscheinend nicht den eigenen Regeln folgt. Aus Verlaufs- und Kompatibilitätsgründen weisen bestimmte Routenwerte wie action, controller, page und handler ein spezielles Verhalten auf.

Die äquivalente Funktionalität, die durch LinkGenerator.GetPathByAction und LinkGenerator.GetPathByPage bereitgestellt wird, verdoppelt diese Anomalien von IUrlHelper aus Kompatibilitätsgründen.

URL-Generierungsprozess

Sobald die Gruppe der Endpunktkandidaten ermittelt ist, wird der URL-Generierungsalgorithmus angewendet:

  • Die Endpunkte werden iterativ verarbeitet.
  • Das erste erfolgreiche Ergebnis wird zurückgegeben.

Der erste Schritt in diesem Prozess wird als Routenwertinvalidierung bezeichnet. Die Routenwertinvalidierung ist der Prozess, bei dem das Routing entscheidet, welche Routenwerte aus den Umgebungswerten verwendet und welche ignoriert werden sollen. Jeder Umgebungswert wird berücksichtigt und entweder mit den expliziten Werten kombiniert oder aber ignoriert.

Denken Sie daran, dass Umgebungswerte Anwendungsentwicklern in allgemeinen Fällen das Schreiben von Code sparen können. In der Regel sind die Szenarios, in denen Umgebungswerte hilfreich sind, mit MVC verknüpft:

  • Bei der Verknüpfung mit einer anderen Aktion im gleichen Controller muss der Controllername nicht angegeben werden.
  • Bei der Verknüpfung mit einem anderen Controller im gleichen Bereich muss der Bereich nicht angegeben werden.
  • Bei der Verknüpfung mit der gleichen Aktionsmethode müssen keine Routenwerte angegeben werden.
  • Bei der Verknüpfung mit einem anderen Teil der App sollen keine Routenwerte übertragen werden, die für diesen Teil der App irrelevant sind.

Aufrufe an LinkGenerator oder IUrlHelper, die null zurückgeben, sind meist dadurch bedingt, dass die Routenwertinvalidierung nicht verstanden wurde. Beheben Sie die Routenwertinvalidierung, indem Sie explizit mehr Routenwerte angeben, um zu prüfen, ob das Problem dadurch gelöst wird.

Bei der Routenwertinvalidierung wird davon ausgegangen, dass das URL-Schema der Anwendung hierarchisch ist, mit einer von links nach rechts gebildeten Hierarchie. Sehen Sie sich die einfache Controllerroutenvorlage {controller}/{action}/{id?} an, um ein Gespür dafür zu bekommen, wie dies in der Praxis funktioniert. Durch eine Änderung auf einen Wert werden alle rechts angezeigten Routenwerte ungültig. Dies spricht für die These von der Hierarchie. Wenn die App einen Umgebungswert für id hat und der Vorgang einen anderen Wert für controller angibt:

  • id wird nicht wiederverwendet, weil {controller} links von {id?} steht.

Einige Beispiele veranschaulichen dieses Prinzip:

  • Wenn die expliziten Werte einen Wert für id enthalten, wird der Umgebungswert für id ignoriert. Die Umgebungswerte für controller und action können verwendet werden.
  • Wenn die expliziten Werte einen Wert für action enthalten, wird jeder Umgebungswert für action ignoriert. Die Umgebungswerte für controller können verwendet werden. Wenn sich der explizite Wert für action von dem Umgebungswert für action unterscheidet, wird der Wert id nicht verwendet. Wenn der explizite Wert für action mit dem Umgebungswert für action übereinstimmt, kann der Wert id verwendet werden.
  • Wenn die expliziten Werte einen Wert für controller enthalten, wird jeder Umgebungswert für controller ignoriert. Wenn sich der explizite Wert für controller von dem Umgebungswert für controller unterscheidet, werden die Werte action und id nicht verwendet. Wenn der explizite Wert für controller mit dem Umgebungswert für controller übereinstimmt, können die Werte action und id verwendet werden.

Dieser Prozess wird zusätzlich durch die vorhandenen Attributrouten und dedizierten konventionellen Routen erschwert. Konventionelle Routen des Controllers wie {controller}/{action}/{id?} legen eine Hierarchie mithilfe von Routenparametern fest. Bei bestimmten konventionellen Routen und Attributrouten zu Controllern und Razor Pages:

  • Gibt es eine Hierarchie für Routenwerte.
  • Werden diese nicht in der Vorlage angezeigt.

Für diese Fälle definiert die URL-Generierung das Konzept der erforderlichen Werte. Bei Endpunkten, die von Controllern und Razor Pages erstellt wurden, sind erforderliche Werte angegeben, die eine Routenwertinvalidierung ermöglichen.

Der Algorithmus der Routenwertinvalidierung im Detail:

  • Die erforderlichen Wertnamen werden mit den Routenparametern kombiniert und dann von links nach rechts verarbeitet.
  • Für jeden Parameter werden der Umgebungswert und der explizite Wert verglichen:
    • Wenn der Umgebungswert und der explizite Wert gleich sind, wird der Prozess fortgesetzt.
    • Wenn der Umgebungswert vorhanden ist und der explizite Wert nicht, wird der Umgebungswert bei der URL-Generierung verwendet.
    • Wenn der Umgebungswert nicht vorhanden ist und der explizite Wert vorhanden ist, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.
    • Wenn der Umgebungswert und der explizite Wert vorhanden und die beiden Werte unterschiedlich sind, verwerfen Sie den Umgebungswert und alle nachfolgenden Umgebungswerte.

An diesem Punkt ist der Vorgang zur URL-Generierung bereit, Routeneinschränkungen auszuwerten. Die akzeptierten Werte werden mit den Standardwerten der Parameter kombiniert, die für Einschränkungen bereitgestellt werden. Wenn alle Einschränkungen erfüllt sind, wird der Vorgang fortgesetzt.

Als Nächstes können die akzeptierten Werte verwendet werden, um die Routenvorlage zu erweitern. Die Routenvorlage wird verarbeitet:

  • Von links nach rechts.
  • Für jeden Parameter wird der akzeptierte Wert ersetzt.
  • In den folgenden Sonderfällen:
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter einen Standardwert hat, wird der Standardwert verwendet.
    • Wenn bei den akzeptierten Werten ein Wert fehlt und der Parameter optional ist, wird die Verarbeitung fortgesetzt.
    • Wenn irgendein Routenparameter rechts neben einem fehlenden optionalen Parameter einen Wert hat, schlägt der Vorgang fehl.
    • Zusammenhängende Parameter mit Standardwerten und optionale Parameter werden, wenn möglich, reduziert dargestellt.

Explizit bereitgestellte Werte, für die keine Übereinstimmungen mit einem Routensegment ermittelt werden, werden der Abfragezeichenfolge hinzugefügt. In der folgenden Tabelle werden die Ergebnisse dargestellt, die aus der Verwendung der Routenvorlage {controller}/{action}/{id?} hervorgehen:

Umgebungswerte Explizite Werte Ergebnis
controller = "Home" action = "About" /Home/About
controller = "Home" controller = "Order", action = "About" /Order/About
controller = "Home", color = "Red" action = "About" /Home/About
controller = "Home" action = "About", color = "Red" /Home/About?color=Red

Probleme mit der Routenwertinvalidierung

Ab ASP.NET Core 3.0 funktionieren einige Schemas zur URL-Generierung, die in früheren ASP.NET Core-Versionen verwendet wurden, nicht mehr sehr gut. Das ASP.NET Core-Team plant, in einer zukünftigen Version Features hinzuzufügen, die diese Anforderungen erfüllen. Im Moment ist die beste Lösung die Verwendung von Legacy-Routingfunktionen.

Der folgende Code zeigt ein Beispiel für ein Schema zur URL-Generierung, das vom Routing nicht unterstützt wird.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("default", 
                                     "{culture}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute("blog", "{culture}/{**slug}", 
                                      new { controller = "Blog", action = "ReadPost", });
});

Im vorangehenden Code wird der culture-Routenparameter für die Lokalisierung verwendet. Ziel ist es, dass der culture-Parameter immer als Umgebungswert akzeptiert wird. Der culture-Parameter wird jedoch aufgrund der Art und Weise, wie die erforderlichen Werte funktionieren, nicht als Umgebungswert akzeptiert:

  • In der "default"-Routenvorlage befindet sich der culture-Routenparameter links von controller, sodass culture durch Änderungen an controller nicht ungültig wird.
  • In der "blog"-Routenvorlage wird der culture-Routenparameter rechts von controller betrachtet, der in den erforderlichen Werten aufgeführt ist.

Konfigurieren von Endpunktmetadaten

Die folgenden Links enthalten Informationen zum Konfigurieren von Endpunktmetadaten:

Hostabgleich in Routen mit RequireHost

RequireHost wendet eine Einschränkung auf die Route an, für die der angegebene Host erforderlich ist. Der RequireHost- oder [Host]-Parameter kann wie folgt lauten:

  • Host: www.domain.com, entspricht www.domain.com mit einem beliebigen Port.
  • Host mit Platzhalter: *.domain.com, entspricht www.domain.com, subdomain.domain.com oder www.subdomain.domain.com an einem beliebigen Port.
  • Port: *:5000, entspricht Port 5000 mit einem beliebigen Host.
  • Host und Port: www.domain.com:5000 oder *.domain.com:5000, entspricht dem Host und Port.

Es können mehrere Parameter mit RequireHost oder [Host] angegeben werden. Die Einschränkung gleicht die Hosts ab, die für einen der Parameter gültig sind. Beispielsweise entspricht [Host("domain.com", "*.domain.com")]domain.com, www.domain.com und subdomain.domain.com.

Im folgenden Code wird RequireHost verwendet, um den angegebenen Host auf der Route anzufordern:

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
            .RequireHost("contoso.com");
        endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
            .RequireHost("adventure-works.com");
        endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
    });
}

Im folgenden Code wird das [Host]-Attribut für den Controller verwendet, um die einzelnen angegebenen Hosts anzufordern:

[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Host("example.com:8080")]
    public IActionResult Privacy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

Wenn das [Host]-Attribut sowohl auf die Controller- als auch auf die Aktionsmethode angewendet wird, trifft Folgendes zu:

  • Das Attribut auf der Aktion wird verwendet.
  • Das Controllerattribut wird ignoriert.

Leistungsleitfaden für das Routing

Der Großteil der Routingfunktionalität wurde in ASP.NET Core 3.0 aktualisiert, um die Leistung zu erhöhen.

Wenn eine App Leistungsprobleme hat, wird die Ursache häufig beim Routing vermutet. Das Routing wird deshalb in Betracht gezogen, weil Frameworks wie Controller und Razor Pages in ihren Protokollierungsmeldungen die innerhalb des Frameworks verbrachte Zeit angeben. Wenn es einen signifikanten Unterschied zwischen der von den Controllern gemeldeten Zeit und der Gesamtzeit der Anforderung gibt:

  • Schließen Entwickler ihren App-Code als Ursache des Problems aus.
  • Wird in der Regel angenommen, dass das Routing die Ursache für das Problem ist.

Die Leistung des Routings wird anhand von Tausenden von Endpunkten getestet. Es ist unwahrscheinlich, dass eine typische App auf ein Leistungsproblem stößt, nur weil diese zu umfangreich ist. Die häufigste Ursache für eine langsames Routing ist üblicherweise eine schlecht funktionierende benutzerdefinierte Middleware.

Das folgende Codebeispiel veranschaulicht eine grundlegende Technik zur Eingrenzung der Verzögerungsquelle:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}

Auf das Zeitrouting:

  • Verschachteln Sie jede Middleware mit einer Kopie der im vorherigen Code gezeigten Zeitmiddleware.
  • Fügen Sie einen eindeutigen Bezeichner hinzu, um die Zeitdaten mit dem Code zu korrelieren.

Dies ist ein einfacher Weg, um die Verzögerung zu verringern, wenn sie signifikant ist, zum Beispiel größer als 10ms. Wenn Time 2 von Time 1 subtrahiert wird, ergibt sich die in der UseRouting-Middleware benötigte Zeit.

Der folgende Code verwendet einen kompakteren Ansatz als der vorangegangene Zeitcode:

public sealed class MyStopwatch : IDisposable
{
    ILogger<Startup> _logger;
    string _message;
    Stopwatch _sw;

    public MyStopwatch(ILogger<Startup> logger, string message)
    {
        _logger = logger;
        _message = message;
        _sw = Stopwatch.StartNew();
    }

    private bool disposed = false;


    public void Dispose()
    {
        if (!disposed)
        {
            _logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",
                                    _message, _sw.ElapsedMilliseconds);

            disposed = true;
        }
    }
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    int count = 0;
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }

    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}

Potenziell teure Routingfeatures

Die folgende Liste gibt einen Einblick in Routingfeatures, die im Vergleich zu einfachen Routenvorlagen relativ teuer sind:

  • Reguläre Ausdrücke: Mit einer kleinen Menge an Eingaben ist möglich, reguläre Ausdrücke zu schreiben, die komplex sind oder eine lange Ausführungszeit haben.
  • Komplexe Segmente ({x}-{y}-{z}):
    • Sind wesentlich teurer als das Analysieren eines regulären URL-Pfadsegments.
    • Führen dazu, dass viel mehr Teilzeichenfolgen zugeordnet werden.
    • Die komplexe Segmentlogik wurde beim Aktualisieren der ASP.NET Core 3.0-Routingleistung nicht aktualisiert.
  • Synchroner Datenzugriff: Viele komplexe Apps verfügen im Rahmen Ihrer Routingfunktionen über Datenbankzugriff. Das Routing in ASP.NET Core 2.2 und früheren Versionen bietet möglicherweise nicht die richtigen Erweiterungspunkte zur Unterstützung des Routings für den Datenbankzugriff. Zum Beispiel sind IRouteConstraint und IActionConstraint synchron. Erweiterbarkeitspunkte wie MatcherPolicy und EndpointSelectorContext sind asynchron.

Leitfaden für Bibliotheksautoren

Dieser Abschnitt enthält Hinweise für Bibliotheksautoren, die auf dem Routing aufbauen. Diese Details sollen sicherstellen, dass App-Entwickler gute Erfahrungen mit Bibliotheken und Frameworks machen, die das Routing erweitern.

Definieren von Endpunkten

Wenn Sie ein Framework erstellen möchten, das das Routing für den URL-Abgleich verwendet, sollten Sie zunächst eine Benutzeroberfläche definieren, die auf UseEndpoints aufbaut.

Es wird empfohlen, dass Sie auf IEndpointRouteBuilder aufbauen. Auf diese Weise können Benutzer Ihr Framework mit anderen ASP.NET Core-Features problemlos zusammenstellen. Jede ASP.NET Core-Vorlage umfasst die Routingfunktionalität. Gehen Sie davon aus, dass das Routing vorhanden und den Benutzern vertraut ist.

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...);

    endpoints.MapHealthChecks("/healthz");
});

Es wird empfohlen, dass Sie einen versiegelten konkreten Typ aus einem Aufruf an MapMyFramework(...) zurückgeben, der IEndpointConventionBuilder implementiert. Die meisten Map...-Methoden in Frameworks folgen diesem Muster. Die IEndpointConventionBuilder-Schnittstelle:

  • Ermöglicht die Kombinierbarkeit von Metadaten.
  • Wird von verschiedenen Erweiterungsmethoden angesteuert.

Wenn Sie Ihren eigenen Typ deklarieren, können Sie dem Generator Ihre eigene frameworkspezifische Funktionalität hinzufügen. Es ist in Ordnung, einen vom Framework deklarierten Generator zu umschließen und Aufrufe an ihn weiterzuleiten.

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization()
                                 .WithMyFrameworkFeature(awesome: true);

    endpoints.MapHealthChecks("/healthz");
});

Ziehen Sie in Betracht, Ihre eigene EndpointDataSource-Klasse zu schreiben. Die EndpointDataSource-Klasse vom primitiven Typ auf niedriger Ebene eignet sich zum Deklarieren und Aktualisieren einer Sammlung von Endpunkten. EndpointDataSource ist eine leistungsstarke API, die von Controllern und Razor Pages verwendet wird.

Die Routingtests haben ein grundlegendes Beispiel für eine Datenquelle, die nicht aktualisiert wird.

Versuchen Sie nicht, eine EndpointDataSource-Klasse standardmäßig zu registrieren. Fordern Sie die Benutzer auf, Ihr Framework in UseEndpoints zu registrieren. Der Grundgedanke beim Routing ist, dass standardmäßig nichts enthalten ist, und dass die Endpunkte bei UseEndpoints registriert werden müssen.

Erstellen von in das Routing integrierter Middleware

Ziehen Sie in Betracht, Metadatentypen als Schnittstelle zu definieren.

Ermöglichen Sie, Metadatentypen als Attribut für Klassen und Methoden zu verwenden.

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

Frameworks wie Controller und Razor Pages unterstützen die Anwendung von Metadatenattributen auf Typen und Methoden. Für das Deklarieren von Metadatentypen gilt:

  • Machen Sie diese als Attribute zugänglich.
  • Die meisten Benutzer sind mit der Anwendung von Attributen vertraut.

Durch die Deklaration eines Metadatentyps als Schnittstelle wird die Flexibilität zusätzlich erhöht:

  • Schnittstelle können zusammengesetzt werden.
  • Entwickler können ihre eigenen Typen deklarieren, die mehrere Richtlinien kombinieren.

Ermöglichen Sie, Metadaten außer Kraft zu setzen, wie in folgendem Beispiel gezeigt:

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

Die beste Möglichkeit, diese Richtlinien zu befolgen, besteht darin, die Definition von Markermetadaten zu vermeiden:

  • Suchen Sie nicht nur nach dem Vorhandensein eines Metadatentyps.
  • Definieren Sie eine Eigenschaft für die Metadaten, und überprüfen Sie die Eigenschaft.

Die Metadatensammlung ist geordnet und unterstützt das Außerkraftsetzen nach Priorität. Im Fall von Controllern sind Metadaten für die Aktionsmethode am spezifischsten.

Nutzen Sie Middleware mit und ohne Routing.

app.UseRouting();

app.UseAuthorization(new AuthorizationPolicy() { ... });

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization();
});

Ein gutes Beispiel für diese Vorgabe ist die UseAuthorization-Middleware. Mithilfe der Autorisierungsmiddleware können Sie eine Fallbackrichtlinie einbauen. Die Fallbackrichtlinie gilt, falls angegeben, für beide:

  • Endpunkte ohne angegebene Richtlinie.
  • Anforderungen, die nicht mit einem Endpunkt übereinstimmen.

Dies macht die Autorisierungsmiddleware außerhalb des Routingkontexts sehr nützlich. Die Autorisierungsmiddleware kann für die traditionelle Middlewareprogrammierung verwendet werden.

Debugdiagnose

Legen Sie für eine ausführliche Routingdiagnoseausgabe Logging:LogLevel:Microsoft auf Debug fest. Legen Sie in der Entwicklungsumgebung die Protokollebene in appsettings.Development.json fest:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}