ASP.NET MVC WebAPI – Routing

written by devangelist on Januar 25, 2013 in Allgemein and API and ASP.NET and ASP.NET MVC and ASP.NET MVC Web API with no comments

Sie sollen sprechend sein, die URLs. Dieses unter URL-Rewriting bekannte Verfahren hat sich in den letzten Jahren im Web stark etabliert. Bereits seit der Einführung der ersten Version des ASP.NET MVC-Frameworks wird dies unter dem Begriff Routing unterstützt. Mit MVC 4 und der damit eingeführten Web API wurde noch einiges an Feinschliff geleistet, die Logik blieb aber gleich.

Der große Unterschied bzw. Neuerung in der Web API ist jener, dass für das Selektieren bzw. Erkennen der Action im Controller die HTTP-Methode (und nicht den Pfad) verwendet wird.

Dies ist keine Limitierung sondern ein Feature, welches natürlich nicht zwingend verwendet werden muss. Das Old-Style-Routing kann weiterhin verwendet werden, dieser Artikel zeigt wie es geht.

Dass die Action nun quasi “automatisch” erkannt wird, klingt erst mal spannend, denn endlich sind Namen wie “/api/products/getFoo/1″ erlaubt, selbst wenn die Methode im Controller als GetById deklariert ist.

Jedoch bevor darauf eingegangen wird warum diese verwirrende Möglichkeit nun funktioniert, ein paar Basics zur Routen-Erstellung und damit ein paar Schritte zurück:

Ziel soll jenes sein, anhand einer URI und basierend auf einem Template, einen Controller und eine Action zu selektieren. Dafür bedarf es eines Templates welches die Art und Weise vorgibt, wie diese URI zu lesen und interpretieren ist. Ein solches route-Template ist im Prinzip ähnlich einer gewöhnlichen URI, welche Platzhalter enthalten kann:

"/{controller}/{action}/id"
"/app/{controller}/root/{category}/id"

In einer Route können für die Platzhalter Default-Werte angeben werden:

defaults: new { category = "all" }

Zusätzlich kann eine Einschränkungen auf die Erkennung eines Segmentes gesetzt werden:

constraint: new { id = @"\d+" }

In diesem Beispiel wird nur eine Id erkannt, welche ein oder mehrere Zeichen lang ist.

Platzhalter können beliebig definiert werden, einzig und allein “{controller}” und “{action}” haben einen vordefinierten Zweck. Während bei klassischen MVC-Anwendungen der Action-Platzhalter verwendet wurde, wird er wie bereits angedeutet in der Web-API eher vermieden.

Intern erstellt das Framework ein Dictionary (Route-Dictionary) welches einen Wert für jeden Platzhalter enthält. Diese Schlüssel-Wert-Paare werden aus der URL gebildet und im IHttpRouteDate-Objekt gespeichert. Einzige Ausnahme für das Dictionary bilden die Optional-Parameters, welche nur bei gegebenen Wert hinzugefügt werden.

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

Ein Aufruf der Route /api/products/ würde folgende Schlüssel-Wert Paare erzeugen:

controller: "products"
category: "all" /api/products/cat/1
controller: "products
category: cat
id: 1

Finde den Conroller:

Theoretiker:

Die Selektierung eines Controllers wird von der IHttpControllerSelector.SelectController Methode gehandhabt. Diese nimmt ein HttpRequestMessage-Objekt entgegen und gibt einen HttpControllerDescriptor zurück. Standardmäßig wird DefaultHttpControllerSelector verwendet.

Dieser DefaultHttpControllerSelector verwendet das IHttpControllerTypeResolver-Interface um eine Liste der gesamten Controller in der Web API, welche IHttpController implementieren, nicht abstrakt sind und einen Namen welcher in “Controller” endet haben, zu bekommen.

Praktiker:

  • Im Dictionary wird ein Key “controller” gesucht.
  • Der dazugehörige Wert wird mit <Wert>Controller zum Typ-Namen.
  • In der Web-API wird ein Controller mit diesem Typ-Namen gesucht.

Zum Beispiel wird der Eintrag im Dictionary “controller” = “product” zu ProductController

Nur selten wird der Controller-Seketor überschrieben. Sei dies nun gewünscht, ist dies problemlos machbar:

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

Finde die Action:

Theoretiker:

Nach dem Controller wird das Framework die Action mithilfe der IHttpActionSelector.SelectAction Methode selektieren. Diese Methode nimmt ein HttpControllerContext entgegen und gibt ein HttpActionDescriptor zurück.

Auch hier gibt es einen Standardimplementierung, nämlich jene des ApiControllerActionSelector.

Der Praktiker:

Bei der Selektierung der Action werden folgende Kriterien berücksichtigt:

  • Die HTTP-Method des Requests
  • Der “{action}“-Parameter, wenn vorhanden
  • Die Parameter der Action des Controllers

Inbegriffen und somit qualifiziert sind alle öffentlichen (public) Instanz-Methoden des Controllers.
Ausgeschlossen sind spezielle Methoden wie Konstruktoren, Events, vererbte Methoden aus der ApiController-Basisklasse, usw.

HTTP Methods

Es werden nur jene Methoden ausgewählt, welche mit der gegebenen HTTP-Methode übereinstimmen. Wichtig für die Erkennung ist die Angabe über ein Attribut: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, oder HttpPut oder über einen Methoden-Name, welcher mit “Get”, “Post”, “Put”, “Delete”, “Head”, “Options”, oder “Patch” anfängt.

Gibt es keine Übereinstimmung wird von einem POST-Request ausgegangen.

Parameter Bindings

Ein Parameter Binding bestimmt die Art und Weise, mit welcher die Web-API den Wert für einen Parameter erstellt.

Standardmäßig werden:

  • Einfache Typen (wie DateTime, Decimal, Guid, String und TimeSpan) aus der URL genommen.
  • Komplexe Typen vom Request-Body genommen.

Dieser Artikel befasst sich mit der am häufigsten verwendeten Möglichkeit des URL-Bindings. Mehr Infos über eigene Bindings und über die Erkennung von komplexen Typen gibt es in diesem Post.

Vom Ei zur Henne

Der Ablauf um eine Action zu Selektieren unterliegt den folgenden Punkten:

  • Eine Liste aller Actions des Controllers erstellen welche der HTTP-Request-Methode entsprechen.
  • Für jede Action eine Liste der Parameter (mit simplen Daten-Typ) erstellen und optionale Parameter ausschließen.
  • Aus dieser Liste versuchen für jeden Parameter einen Treffer zu finden. Sowohl im Route-Dictionary als auch im URI-Querystring.
  • Gibt es eine Action wo alle Parameter einen Treffer in der URI haben, verwenden.
  • Die Action mit den meisten Parameter-Treffern gewinnt.
  • Actions mit dem [NonAction]-Attribut ignorieren.

Prinzipiell sollte ein Parameter den Wert aus der URI, dem Request-Body oder aus einem Custom-Binding bekommen. Parameter welche aus der URI kommen, müssen einen entsprechenden Wert im Pfad oder im Querystring erzielen.

Als Beispiel:

public void Get(int id)

Der id-Parameter ist der URL gebunden, sprich die Action kann nur über eine URI aufgerufen werden, welche einen Wert für “id” beinhaltet (egal ob im Route-Dictionary oder im Querystring). Die einzige Ausnahme bilden optionale Parameter, für welche nicht zwingend ein Wert benötigt wird. (Komplexe Typen können der URI nur über ein Custom-Binding gebunden werden)

Zusammenfassend:

  • Die Action muss mit der HTTP-Methode des Requests übereinstimmen.
  • Der Action-Name muss dem “{action}”-Platzhalter übereinstimmen, wenn vorhanden.
  • Für jeden Parameter in der Action, welcher aus der URI genommen wird, muss der dazugehörige Parameter-Name im Route-Dictionary oder im URI-Querystring gefunden werden (außer bei optionalen Parametern).
  • Die Action mit den meisten übereinstimmenden Parametern gewinnt. (Im minimalen Fall eine Methode ohne Parameter)

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, int version = 1) {}
    [HttpGet]
    public void GetByName(string name) {}
    public void Post(Product product) {}
    public void Put(int id, Product product) {}
}

HTTP request:

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

Route Dictionary:

controller: "products"
id: "1"
Controller -> ProductsController

Action Selektierung:

Es handelst sich um ein GET-Request, es kommen also nur die Methoden GetAll, GetById und GetByName in Frage.

Da das Route Dictionary keinen “action“-Platzhalter beinhaltet, kann dieser ignoriert werden und anhand der folgenden Tabelle weiter Selektiert bzw. gefunden werden:

Parameter in den Actions:

GetAll             - Keine  (ok)
GetById            - "id"   (ok)
FindProductsByName - "name" (nicht ok)

Sowohl die GetAll als auch die GetById kommen in Frage, jedoch gewinnt die GetById, da diese mehr Treffer (“id“) als die GetAll (keine Parameter) erzielt.

ASP.NET MVC WebAPI – Routing: 1 Stern2 Sterne3 Sterne4 Sterne5 Sterne 5,00 von 5 Punkte, 5 abgegebene Stimmen.
Loading ... Loading ...

About the Author

Roberto Bez ist passionierter Webentwickler und TechLead bei der HolidayCheck AG. Für Roberto bedeutet das Entwickeln nicht nur Arbeit, sondern auch Freude, Motivation und täglich neue, aufregende Herausforderungen. Besonders gerne setzt er sich mit neuen Webtechnologien sowie Datenbanken aller Art auseinander und versucht diese in die tägliche Anwendungsentwicklung miteinzubringen. Neben dem Entwickeln trifft man ihn gerne Abends beim Laufen oder im Sommer bei Mountainbike-Touren durch die schönen Berge Südtirols.