Routing- und Aktionsauswahl in ASP.NET-Web-API

In diesem Artikel wird beschrieben, wie ASP.NET-Web-API eine HTTP-Anforderung an eine bestimmte Aktion auf einem Controller weiter leitet.

Hinweis

Eine allgemeine Übersicht über das Routing finden Sie unter Routing in ASP.NET-Web-API.

In diesem Artikel werden die Details des Routingprozesses behandelt. Wenn Sie ein Web-API-Projekt erstellen und feststellen, dass einige Anforderungen nicht wie erwartet weitergeleitet werden, wird dieser Artikel hoffentlich hilfreich sein.

Das Routing weist drei Standard Phasen auf:

  1. Abgleich des URI mit einer Routenvorlage.
  2. Auswählen eines Controllers.
  3. Auswählen einer Aktion.

Sie können einige Teile des Prozesses durch Ihre eigenen benutzerdefinierten Verhaltensweisen ersetzen. In diesem Artikel beschreibe ich das Standardverhalten. Am Ende notiere ich die Stellen, an denen Sie das Verhalten anpassen können.

Routenvorlagen

Eine Routenvorlage ähnelt einem URI-Pfad, kann aber Platzhalterwerte aufweisen, die mit geschweiften Klammern angegeben sind:

"api/{controller}/public/{category}/{id}"

Wenn Sie eine Route erstellen, können Sie Standardwerte für einige oder alle Platzhalter angeben:

defaults: new { category = "all" }

Sie können auch Einschränkungen bereitstellen, die einschränken, wie ein URI-Segment mit einem Platzhalter übereinstimmen kann:

constraints: new { id = @"\d+" }   // Only matches if "id" is one or more digits.

Das Framework versucht, die Segmente im URI-Pfad mit der Vorlage abzugleichen. Literale in der Vorlage müssen genau übereinstimmen. Ein Platzhalter stimmt mit jedem Wert überein, es sei denn, Sie geben Einschränkungen an. Das Framework stimmt nicht mit anderen Teilen des URI überein, z. B. dem Hostnamen oder den Abfrageparametern. Das Framework wählt die erste Route in der Routingtabelle aus, die dem URI entspricht.

Es gibt zwei spezielle Platzhalter: "{controller}" und "{action}".

  • "{controller}" gibt den Namen des Controllers an.
  • "{action}" gibt den Namen der Aktion an. In der Web-API besteht die übliche Konvention darin, "{action}" wegzulassen.

Standardeinstellungen

Wenn Sie Standardwerte angeben, entspricht die Route einem URI, dem diese Segmente fehlen. Zum Beispiel:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}",
    defaults: new { category = "all" }
);

Die URIs http://localhost/api/products/all und http://localhost/api/products entsprechen der vorherigen Route. Im letzteren URI wird dem fehlenden {category} Segment der Standardwert allzugewiesen.

Routenwörterbuch

Wenn das Framework eine Übereinstimmung für einen URI findet, erstellt es ein Wörterbuch, das den Wert für jeden Platzhalter enthält. Die Schlüssel sind die Platzhalternamen, ohne die geschweiften Klammern. Die Werte werden aus dem URI-Pfad oder den Standardwerten übernommen. Das Wörterbuch wird im IHttpRouteData-Objekt gespeichert.

Während dieser Phase des Routenabgleichs werden die speziellen Platzhalter "{controller}" und "{action}" genau wie die anderen Platzhalter behandelt. Sie werden einfach im Wörterbuch mit den anderen Werten gespeichert.

Ein Standardwert kann den speziellen Wert RouteParameter.Optional aufweisen. Wenn einem Platzhalter dieser Wert zugewiesen wird, wird der Wert dem Routenwörterbuch nicht hinzugefügt. Zum Beispiel:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}/{id}",
    defaults: new { category = "all", id = RouteParameter.Optional }
);

Für den URI-Pfad "api/products" enthält das Routenwörterbuch Folgendes:

  • Controller: "Products"
  • Kategorie: "alle"

Für "api/products/toys/123" enthält das Routenwörterbuch jedoch Folgendes:

  • Controller: "Products"
  • Kategorie: "Spielzeug"
  • id: "123"

Die Standardwerte können auch einen Wert enthalten, der an keiner Stelle in der Routenvorlage angezeigt wird. Wenn die Route übereinstimmt, wird dieser Wert im Wörterbuch gespeichert. Zum Beispiel:

routes.MapHttpRoute(
    name: "Root",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "customers", id = RouteParameter.Optional }
);

Wenn der URI-Pfad "api/root/8" lautet, enthält das Wörterbuch zwei Werte:

  • Controller: "Kunden"
  • id: "8"

Auswählen eines Controllers

Die Controllerauswahl wird von der IHttpControllerSelector.SelectController-Methode verarbeitet. Diese Methode übernimmt eine HttpRequestMessage-instance und gibt einen HttpControllerDescriptor zurück. Die Standardimplementierung wird von der DefaultHttpControllerSelector-Klasse bereitgestellt. Diese Klasse verwendet einen einfachen Algorithmus:

  1. Suchen Sie im Routenwörterbuch nach dem Schlüssel "Controller".
  2. Nehmen Sie den Wert für diesen Schlüssel, und fügen Sie die Zeichenfolge "Controller" an, um den Namen des Controllertyps abzurufen.
  3. Suchen Sie nach einem Web-API-Controller mit diesem Typnamen.

Wenn das Routenwörterbuch beispielsweise das Schlüssel-Wert-Paar "controller" = "products" enthält, lautet der Controllertyp "ProductsController". Wenn es keinen übereinstimmenden Typ oder mehrere Übereinstimmungen gibt, gibt das Framework einen Fehler an den Client zurück.

Für Schritt 3 verwendet DefaultHttpControllerSelector die IHttpControllerTypeResolver-Schnittstelle , um die Liste der Web-API-Controllertypen abzurufen. Die Standardimplementierung von IHttpControllerTypeResolver gibt alle öffentlichen Klassen zurück, die (a) IHttpController implementieren, (b) nicht abstrakt sind und (c) einen Namen haben, der mit "Controller" endet.

Aktionsauswahl

Nachdem Sie den Controller ausgewählt haben, wählt das Framework die Aktion aus, indem es die IHttpActionSelector.SelectAction-Methode aufruft. Diese Methode verwendet einen HttpControllerContext und gibt einen HttpActionDescriptor zurück.

Die Standardimplementierung wird von der ApiControllerActionSelector-Klasse bereitgestellt. Um eine Aktion auszuwählen, wird Folgendes angezeigt:

  • Die HTTP-Methode der Anforderung.
  • Der Platzhalter "{action}" in der Routenvorlage, sofern vorhanden.
  • Die Parameter der Aktionen auf dem Controller.

Bevor wir uns mit dem Auswahlalgorithmus ansehen, müssen wir einige Dinge über Controlleraktionen verstehen.

Welche Methoden auf dem Controller gelten als "Aktionen"? Beim Auswählen einer Aktion untersucht das Framework nur öffentliche instance Methoden auf dem Controller. Außerdem werden "Special Name" -Methoden (Konstruktoren, Ereignisse, Operatorüberladungen usw.) und methoden ausgeschlossen, die von der ApiController-Klasse geerbt werden.

HTTP-Methoden. Das Framework wählt nur Aktionen aus, die der HTTP-Methode der Anforderung entsprechen und wie folgt bestimmt werden:

  1. Sie können die HTTP-Methode mit einem Attribut angeben: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost oder HttpPut.
  2. Andernfalls unterstützt die Aktion diese HTTP-Methode, wenn der Name der Controllermethode mit "Get", "Post", "Put", "Delete", "Head", "Options" oder "Patch" beginnt.
  3. Wenn keine der oben genannten Punkte vorhanden ist, unterstützt die -Methode POST.

Parameterbindungen. Mit einer Parameterbindung erstellt die Web-API einen Wert für einen Parameter. Dies ist die Standardregel für die Parameterbindung:

  • Einfache Typen werden dem URI entnommen.
  • Komplexe Typen werden dem Anforderungstext entnommen.

Einfache Typen umfassen alle .NET Framework primitiven Typen sowie DateTime, Decimal, Guid, String und TimeSpan. Für jede Aktion kann höchstens ein Parameter den Anforderungstext lesen.

Hinweis

Es ist möglich, die Standardbindungsregeln außer Kraft zu setzen. Siehe WebAPI-Parameterbindung unter der Haube.

Vor diesem Hintergrund sehen Sie hier den Aktionsauswahlalgorithmus.

  1. Erstellen Sie eine Liste aller Aktionen auf dem Controller, die der HTTP-Anforderungsmethode entsprechen.

  2. Wenn das Routenwörterbuch über einen Eintrag "action" verfügt, entfernen Sie Aktionen, deren Name nicht mit diesem Wert übereinstimmt.

  3. Versuchen Sie, Aktionsparameter wie folgt dem URI zuzuordnen:

    1. Rufen Sie für jede Aktion eine Liste der Parameter ab, die ein einfacher Typ sind, wobei die Bindung den Parameter aus dem URI abruft. Schließen Sie optionale Parameter aus.
    2. Versuchen Sie in dieser Liste, eine Übereinstimmung für jeden Parameternamen zu finden, entweder im Routenwörterbuch oder in der URI-Abfragezeichenfolge. Bei Übereinstimmungen wird die Groß-/Kleinschreibung nicht beachtet, und es hängt nicht von der Parameterreihenfolge ab.
    3. Wählen Sie eine Aktion aus, bei der jeder Parameter in der Liste eine Übereinstimmung im URI aufweist.
    4. Wenn mehr als eine Aktion diese Kriterien erfüllt, wählen Sie die Aktion mit den meisten Parametern aus.
  4. Ignorieren von Aktionen mit dem [NonAction]- Attribut.

Schritt 3 ist wahrscheinlich der verwirrendste. Die Grundidee ist, dass ein Parameter seinen Wert entweder aus dem URI, aus dem Anforderungstext oder aus einer benutzerdefinierten Bindung abrufen kann. Für Parameter, die aus dem URI stammen, möchten wir sicherstellen, dass der URI tatsächlich einen Wert für diesen Parameter enthält, entweder im Pfad (über das Routenwörterbuch) oder in der Abfragezeichenfolge.

Betrachten Sie beispielsweise die folgende Aktion:

public void Get(int id)

Der Id-Parameter wird an den URI gebunden. Daher kann diese Aktion nur mit einem URI übereinstimmen, der einen Wert für "id" enthält, entweder im Routenwörterbuch oder in der Abfragezeichenfolge.

Optionale Parameter sind eine Ausnahme, da sie optional sind. Für einen optionalen Parameter ist es in Ordnung, wenn die Bindung den Wert aus dem URI nicht abrufen kann.

Komplexe Typen sind aus einem anderen Grund eine Ausnahme. Ein komplexer Typ kann nur über eine benutzerdefinierte Bindung an den URI gebunden werden. In diesem Fall kann das Framework jedoch nicht im Voraus wissen, ob der Parameter an einen bestimmten URI gebunden wird. Um dies herauszufinden, muss die Bindung aufgerufen werden. Das Ziel des Auswahlalgorithmus besteht darin, eine Aktion aus der statischen Beschreibung auszuwählen, bevor Bindungen aufgerufen werden. Daher werden komplexe Typen vom abgleichenden Algorithmus ausgeschlossen.

Nachdem die Aktion ausgewählt wurde, werden alle Parameterbindungen aufgerufen.

Zusammenfassung:

  • Die Aktion muss mit der HTTP-Methode der Anforderung übereinstimmen.
  • Der Aktionsname muss mit dem Eintrag "action" im Routenverzeichnis übereinstimmen, sofern vorhanden.
  • Wenn der Parameter für jeden Parameter der Aktion aus dem URI stammt, muss der Parametername entweder im Routenverzeichnis oder in der URI-Abfragezeichenfolge gefunden werden. (Optionale Parameter und Parameter mit komplexen Typen sind ausgeschlossen.)
  • Versuchen Sie, die größte Anzahl von Parametern abzugleichen. Die beste Übereinstimmung kann eine Methode ohne Parameter sein.

Erweitertes Beispiel

Routen:

routes.MapHttpRoute(
    name: "ApiRoot",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "products", id = RouteParameter.Optional }
);
routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Controller:

public class ProductsController : ApiController
{
    public IEnumerable<Product> GetAll() {}
    public Product GetById(int id, double version = 1.0) {}
    [HttpGet]
    public void FindProductsByName(string name) {}
    public void Post(Product value) {}
    public void Put(int id, Product value) {}
}

HTTP-Anforderung:

GET http://localhost:34701/api/products/1?version=1.5&details=1

Routenabgleich

Der URI entspricht der Route mit dem Namen "DefaultApi". Das Routenwörterbuch enthält die folgenden Einträge:

  • Controller: "Products"
  • id: "1"

Das Routenwörterbuch enthält nicht die Abfragezeichenfolgenparameter "version" und "details", aber diese werden bei der Aktionsauswahl weiterhin berücksichtigt.

Controllerauswahl

Aus dem Eintrag "controller" im Routenwörterbuch lautet ProductsControllerder Controllertyp .

Aktionsauswahl

Die HTTP-Anforderung ist eine GET-Anforderung. Die Controlleraktionen, die GET unterstützen, sind GetAll, GetByIdund FindProductsByName. Das Routenwörterbuch enthält keinen Eintrag für "action", sodass wir nicht mit dem Aktionsnamen übereinstimmen müssen.

Als Nächstes versuchen wir, Parameternamen für die Aktionen abzugleichen, wobei wir nur die GET-Aktionen betrachten.

Action Übereinstimmende Parameter
GetAll Keine
GetById "id"
FindProductsByName "Name"

Beachten Sie, dass der Versionsparameter von GetById nicht berücksichtigt wird, da es sich um einen optionalen Parameter handelt.

Die GetAll -Methode stimmt trivial überein. Die GetById -Methode stimmt ebenfalls überein, da das Routenwörterbuch "id" enthält. Die FindProductsByName -Methode stimmt nicht überein.

Die GetById -Methode gewinnt, da sie mit einem Parameter übereinstimmt, und keine Parameter für GetAll. Die -Methode wird mit den folgenden Parameterwerten aufgerufen:

  • id = 1
  • Version = 1.5

Beachten Sie, dass der Wert des Parameters aus der URI-Abfragezeichenfolge stammt, obwohl die Version nicht im Auswahlalgorithmus verwendet wurde.

Erweiterungspunkte

Die Web-API stellt Erweiterungspunkte für einige Teile des Routingprozesses bereit.

Schnittstelle BESCHREIBUNG
IHttpControllerSelector Wählt den Controller aus.
IHttpControllerTypeResolver Ruft die Liste der Controllertypen ab. DefaultHttpControllerSelector wählt den Controllertyp aus dieser Liste aus.
IAssembliesResolver Ruft die Liste der Projektassemblys ab. Die IHttpControllerTypeResolver-Schnittstelle verwendet diese Liste, um die Controllertypen zu suchen.
IHttpControllerActivator Erstellt neue Controllerinstanzen.
IHttpActionSelector Wählt die Aktion aus.
IHttpActionInvoker Ruft die Aktion auf.

Um eine eigene Implementierung für eine dieser Schnittstellen bereitzustellen, verwenden Sie die Services-Auflistung für das HttpConfiguration-Objekt :

var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));