Attributrouting in ASP.NET-Web-API 2

Routing ist, wie die Web-API einen URI mit einer Aktion abgleicht. Web-API 2 unterstützt eine neue Art von Routing, die als Attributrouting bezeichnet wird. Wie der Name schon sagt, verwendet das Attributrouting Attribute, um Routen zu definieren. Attributrouting gibt Ihnen mehr Kontrolle über die URIs in Ihrer Web-API. Beispielsweise können Sie ganz einfach URIs erstellen, die Hierarchien von Ressourcen beschreiben.

Der frühere Routingstil, der als konventionsbasiertes Routing bezeichnet wird, wird weiterhin vollständig unterstützt. Tatsächlich können Sie beide Techniken im selben Projekt kombinieren.

In diesem Thema wird gezeigt, wie Sie das Attributrouting aktivieren, und es werden die verschiedenen Optionen für das Attributrouting beschrieben. Ein End-to-End-Tutorial, das Attributrouting verwendet, finden Sie unter Erstellen einer REST-API mit Attributrouting in Web-API 2.

Voraussetzungen

Visual Studio 2017 Community-, Professional- oder Enterprise-Edition

Alternativ können Sie den NuGet-Paket-Manager verwenden, um die erforderlichen Pakete zu installieren. Wählen Sie im Menü Extras in Visual Studio die Option NuGet-Paket-Manager und dann Paket-Manager-Konsole aus. Geben Sie den folgenden Befehl im Fenster Paket-Manager-Konsole ein:

Install-Package Microsoft.AspNet.WebApi.WebHost

Warum Attributrouting?

In der ersten Version der Web-API wurde konventionsbasiertes Routing verwendet. Bei diesem Routingtyp definieren Sie eine oder mehrere Routenvorlagen, bei denen es sich im Wesentlichen um parametrisierte Zeichenfolgen handelt. Wenn das Framework eine Anforderung empfängt, wird der URI mit der Routenvorlage abgegleichen. Weitere Informationen zum konventionsbasierten Routing finden Sie unter Routing in ASP.NET-Web-API.

Ein Vorteil des konventionsbasierten Routings ist, dass Vorlagen an einem zentralen Ort definiert werden und die Routingregeln konsistent auf alle Controller angewendet werden. Leider macht das konventionsbasierte Routing es schwierig, bestimmte URI-Muster zu unterstützen, die in RESTful-APIs üblich sind. Beispielsweise enthalten Ressourcen häufig untergeordnete Ressourcen: Kunden haben Aufträge, Filme haben Schauspieler, Bücher haben Autoren usw. Es ist natürlich, URIs zu erstellen, die diese Beziehungen widerspiegeln:

/customers/1/orders

Diese Art von URI ist mit konventionsbasiertem Routing schwierig zu erstellen. Obwohl dies möglich ist, lassen sich die Ergebnisse nicht gut skalieren, wenn Sie über viele Controller oder Ressourcentypen verfügen.

Beim Attributrouting ist es trivial, eine Route für diesen URI zu definieren. Sie fügen einfach der Controlleraktion ein Attribut hinzu:

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

Hier sind einige andere Muster, die das Attributrouting einfach macht.

API-Versionsverwaltung

In diesem Beispiel wird "/api/v1/products" an einen anderen Controller als "/api/v2/products" weitergeleitet.

/api/v1/products /api/v2/products

Überladene URI-Segmente

In diesem Beispiel ist "1" eine Bestellnummer, aber "ausstehend" wird einer Auflistung zugeordnet.

/orders/1 /orders/pending

Mehrere Parametertypen

In diesem Beispiel ist "1" eine Bestellnummer, aber "2013/06/16" gibt ein Datum an.

/orders/1 /orders/2013/06/16

Aktivieren des Attributroutings

Um das Attributrouting zu aktivieren, rufen Sie mapHttpAttributeRoutes während der Konfiguration auf. Diese Erweiterungsmethode wird in der System.Web.Http.HttpConfigurationExtensions-Klasse definiert.

using System.Web.Http;

namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            // Other Web API configuration not shown.
        }
    }
}

Attributrouting kann mit konventionsbasiertem Routing kombiniert werden. Um konventionsbasierte Routen zu definieren, rufen Sie die MapHttpRoute-Methode auf.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Attribute routing.
        config.MapHttpAttributeRoutes();

        // Convention-based routing.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Weitere Informationen zum Konfigurieren der Web-API finden Sie unter Konfigurieren ASP.NET-Web-API 2.

Hinweis: Migrieren von Web-API 1

Vor Web-API 2 haben die Web-API-Projektvorlagen Code wie folgt generiert:

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

Wenn das Attributrouting aktiviert ist, löst dieser Code eine Ausnahme aus. Wenn Sie ein Upgrade eines vorhandenen Web-API-Projekts auf die Verwendung des Attributroutings durchführen, müssen Sie diesen Konfigurationscode wie folgt aktualisieren:

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

Hinweis

Weitere Informationen finden Sie unter Konfigurieren der Web-API mit ASP.NET Hosting.

Hinzufügen von Routenattributen

Hier sehen Sie ein Beispiel für eine Route, die mithilfe eines Attributs definiert wird:

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

Die Zeichenfolge "customers/{customerId}/orders" ist die URI-Vorlage für die Route. Die Web-API versucht, den Anforderungs-URI der Vorlage zuzuordnen. In diesem Beispiel sind "customers" und "orders" Literalsegmente, und "{customerId}" ist ein Variablenparameter. Die folgenden URIs würden dieser Vorlage entsprechen:

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

Sie können den Abgleich einschränken, indem Sie Einschränkungen verwenden, die weiter unten in diesem Thema beschrieben werden.

Beachten Sie, dass der Parameter "{customerId}" in der Routenvorlage mit dem Namen des parameters customerId in der -Methode übereinstimmt. Wenn die Web-API die Controlleraktion aufruft, versucht sie, die Routenparameter zu binden. Wenn der URI beispielsweise lautet http://example.com/customers/1/orders, versucht die Web-API, den Wert "1" an den parameter customerId in der Aktion zu binden.

Eine URI-Vorlage kann mehrere Parameter aufweisen:

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

Alle Controllermethoden ohne Routenattribute verwenden konventionsbasiertes Routing. Auf diese Weise können Sie beide Routingtypen im selben Projekt kombinieren.

HTTP-Methoden

Die Web-API wählt auch Aktionen basierend auf der HTTP-Methode der Anforderung (GET, POST usw.) aus. Standardmäßig sucht die Web-API nach einer Übereinstimmung zwischen Groß-/Kleinschreibung und dem Start des Controllermethodennamens. Beispielsweise entspricht eine Controllermethode mit dem Namen PutCustomers einer HTTP PUT-Anforderung.

Sie können diese Konvention überschreiben, indem Sie die Methode mit einem der folgenden Attribute versehen:

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

Im folgenden Beispiel ordnet die Web-API die CreateBook-Methode HTTP POST-Anforderungen zu.

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

Verwenden Sie für alle anderen HTTP-Methoden, einschließlich nicht standardmäßiger Methoden, das AcceptVerbs-Attribut , das eine Liste von HTTP-Methoden akzeptiert.

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

Routenpräfixe

Häufig beginnen die Routen in einem Controller alle mit dem gleichen Präfix. Zum Beispiel:

public class BooksController : ApiController
{
    [Route("api/books")]
    public IEnumerable<Book> GetBooks() { ... }

    [Route("api/books/{id:int}")]
    public Book GetBook(int id) { ... }

    [Route("api/books")]
    [HttpPost]
    public HttpResponseMessage CreateBook(Book book) { ... }
}

Sie können ein allgemeines Präfix für einen gesamten Controller festlegen, indem Sie das [RoutePrefix] -Attribut verwenden:

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET api/books
    [Route("")]
    public IEnumerable<Book> Get() { ... }

    // GET api/books/5
    [Route("{id:int}")]
    public Book Get(int id) { ... }

    // POST api/books
    [Route("")]
    public HttpResponseMessage Post(Book book) { ... }
}

Verwenden Sie eine Tilde (~) für das Methodenattribute, um das Routenpräfix zu überschreiben:

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}

Das Routenpräfix kann Parameter enthalten:

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

Routeneinschränkungen

Mit Routeneinschränkungen können Sie einschränken, wie die Parameter in der Routenvorlage übereinstimmen. Die allgemeine Syntax lautet "{parameter:constraint}". Zum Beispiel:

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

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

Hier wird die erste Route nur ausgewählt, wenn das "id"-Segment des URI eine ganze Zahl ist. Andernfalls wird die zweite Route ausgewählt.

In der folgenden Tabelle sind die einschränkungen aufgeführt, die unterstützt werden.

Constraint Beschreibung Beispiel
alpha Entspricht lateinischen Groß- oder Kleinbuchstaben (a-z, A-Z) {x:alpha}
bool Entspricht einem booleschen Wert. {x:bool}
datetime Entspricht einem DateTime-Wert . {x:datetime}
Decimal Entspricht einem Dezimalwert. {x:decimal}
double Entspricht einem 64-Bit-Gleitkommawert. {x:double}
float Entspricht einem 32-Bit-Gleitkommawert. {x:float}
guid Entspricht einem GUID-Wert. {x:guid}
INT Entspricht einem 32-Bit-Ganzzahlwert. {x:int}
length Entspricht einer Zeichenfolge mit der angegebenen Länge oder innerhalb eines angegebenen Längenbereichs. {x:length(6)} {x:length(1,20)}
long Entspricht einem 64-Bit-Ganzzahlwert. {x:long}
max Entspricht einer ganzen Zahl mit einem maximalen Wert. {x:max(10)}
Maxlength Entspricht einer Zeichenfolge mit einer maximalen Länge. {x:maxlength(10)}
Min. Entspricht einer ganzen Zahl mit einem Minimalwert. {x:min(10)}
Minlength Entspricht einer Zeichenfolge mit einer Mindestlänge. {x:minlength(10)}
range Entspricht einer ganzen Zahl innerhalb eines Wertebereichs. {x:range(10,50)}
regex Entspricht einem regulären Ausdruck. {x:regex(^\d{3}-\d{3}-\d{4}$)}

Beachten Sie, dass einige der Einschränkungen, z. B. "min", Argumente in Klammern enthalten. Sie können mehrere Einschränkungen auf einen Parameter anwenden, getrennt durch einen Doppelpunkt.

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

Benutzerdefinierte Routeneinschränkungen

Sie können benutzerdefinierte Routeneinschränkungen erstellen, indem Sie die IHttpRouteConstraint-Schnittstelle implementieren. Die folgende Einschränkung beschränkt beispielsweise einen Parameter auf einen ganzzahligen Wert ungleich Null.

public class NonZeroConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, 
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            long longValue;
            if (value is long)
            {
                longValue = (long)value;
                return longValue != 0;
            }

            string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (Int64.TryParse(valueString, NumberStyles.Integer, 
                CultureInfo.InvariantCulture, out longValue))
            {
                return longValue != 0;
            }
        }
        return false;
    }
}

Der folgende Code zeigt, wie Die Einschränkung registriert wird:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

Jetzt können Sie die Einschränkung auf Ihre Routen anwenden:

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

Sie können auch die gesamte DefaultInlineConstraintResolver-Klasse ersetzen, indem Sie die IInlineConstraintResolver-Schnittstelle implementieren. Dadurch werden alle integrierten Einschränkungen ersetzt, es sei denn, Ihre Implementierung von IInlineConstraintResolver fügt sie speziell hinzu.

Optionale URI-Parameter und Standardwerte

Sie können einen URI-Parameter optional festlegen, indem Sie dem Routenparameter ein Fragezeichen hinzufügen. Wenn ein Routenparameter optional ist, müssen Sie einen Standardwert für den Methodenparameter definieren.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

In diesem Beispiel /api/books/locale/1033 und /api/books/locale geben die gleiche Ressource zurück.

Alternativ können Sie einen Standardwert in der Routenvorlage wie folgt angeben:

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

Dies ist fast identisch mit dem vorherigen Beispiel, aber es gibt einen geringfügigen Unterschied im Verhalten, wenn der Standardwert angewendet wird.

  • Im ersten Beispiel ("{lcid:int?}") wird der Standardwert 1033 direkt dem Methodenparameter zugewiesen, sodass der Parameter genau diesen Wert aufweist.
  • Im zweiten Beispiel ("{lcid:int=1033}") durchläuft der Standardwert von "1033" den Modellbindungsprozess. Der Standardmodellbinder konvertiert "1033" in den numerischen Wert 1033. Sie können jedoch einen benutzerdefinierten Modellbinder anschließen, der möglicherweise etwas anderes ausführen kann.

(In den meisten Fällen sind die beiden Formulare gleichwertig, es sei denn, Sie verfügen über benutzerdefinierte Modellbinder in Ihrer Pipeline.)

Routennamen

In der Web-API hat jede Route einen Namen. Routennamen sind nützlich, um Links zu generieren, sodass Sie einen Link in eine HTTP-Antwort einschließen können.

Um den Routennamen anzugeben, legen Sie die Name-Eigenschaft für das Attribut fest. Im folgenden Beispiel wird gezeigt, wie der Routenname festgelegt wird und wie der Routenname beim Generieren eines Links verwendet wird.

public class BooksController : ApiController
{
    [Route("api/books/{id}", Name="GetBookById")]
    public BookDto GetBook(int id) 
    {
        // Implementation not shown...
    }

    [Route("api/books")]
    public HttpResponseMessage Post(Book book)
    {
        // Validate and add book to database (not shown)

        var response = Request.CreateResponse(HttpStatusCode.Created);

        // Generate a link to the new book and set the Location header in the response.
        string uri = Url.Link("GetBookById", new { id = book.BookId });
        response.Headers.Location = new Uri(uri);
        return response;
    }
}

Routenreihenfolge

Wenn das Framework versucht, einen URI mit einer Route abzugleichen, wertet es die Routen in einer bestimmten Reihenfolge aus. Um die Reihenfolge anzugeben, legen Sie die Order-Eigenschaft für das Route-Attribut fest. Niedrigere Werte werden zuerst ausgewertet. Der Standardwert für die Reihenfolge ist 0 (null).

So wird die Gesamtreihenfolge bestimmt:

  1. Vergleichen Sie die Order-Eigenschaft des Routenattributes.

  2. Sehen Sie sich jedes URI-Segment in der Routenvorlage an. Ordnen Sie für jedes Segment wie folgt an:

    1. Literalsegmente.
    2. Routenparameter mit Einschränkungen.
    3. Routenparameter ohne Einschränkungen.
    4. Wildcard-Parametersegmente mit Einschränkungen.
    5. Wildcard-Parametersegmente ohne Einschränkungen.
  3. Im Falle eines Gleichstands werden Routen nach einem Ordinalzeichenfolgenvergleich (OrdinalIgnoreCase) der Routenvorlage sortiert, bei dem die Groß-/Kleinschreibung nicht beachtet wird.

Beispiel: Angenommen, Sie definieren den folgenden Controller:

[RoutePrefix("orders")]
public class OrdersController : ApiController
{
    [Route("{id:int}")] // constrained parameter
    public HttpResponseMessage Get(int id) { ... }

    [Route("details")]  // literal
    public HttpResponseMessage GetDetails() { ... }

    [Route("pending", RouteOrder = 1)]
    public HttpResponseMessage GetPending() { ... }

    [Route("{customerName}")]  // unconstrained parameter
    public HttpResponseMessage GetByCustomer(string customerName) { ... }

    [Route("{*date:datetime}")]  // wildcard
    public HttpResponseMessage Get(DateTime date) { ... }
}

Diese Routen sind wie folgt sortiert.

  1. Bestellungen/Details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. Aufträge/ausstehend

Beachten Sie, dass "details" ein Literalsegment ist und vor "{id}" angezeigt wird, aber "pending" als letztes angezeigt wird, da die Order-Eigenschaft 1 ist. (In diesem Beispiel wird davon ausgegangen, dass keine Kunden mit dem Namen "Details" oder "ausstehend" vorhanden sind. Versuchen Sie im Allgemeinen, mehrdeutige Routen zu vermeiden. In diesem Beispiel ist eine bessere Routenvorlage für GetByCustomer "customers/{customerName}" )