Udostępnij za pośrednictwem


Routing atrybutów w interfejsie API sieci Web ASP.NET 2

Routing polega na tym, jak internetowy interfejs API odpowiada identyfikatorowi URI akcji. Internetowy interfejs API 2 obsługuje nowy typ routingu nazywany routingiem atrybutów. Jak sugeruje nazwa, routing atrybutów używa atrybutów do definiowania tras. Routing atrybutów zapewnia większą kontrolę nad identyfikatorami URI w internetowym interfejsie API. Można na przykład łatwo tworzyć identyfikatory URI opisujące hierarchie zasobów.

Wcześniejszy styl routingu nazywany routingiem opartym na konwencji jest nadal w pełni obsługiwany. W rzeczywistości można połączyć obie techniki w tym samym projekcie.

W tym temacie pokazano, jak włączyć routing atrybutów i opisano różne opcje routingu atrybutów. Aby zapoznać się z kompleksowego samouczka korzystającego z routingu atrybutów, zobacz Tworzenie interfejsu API REST z routingiem atrybutów w internetowym interfejsie API 2.

Wymagania wstępne

Visual Studio 2017 Community, Professional lub Enterprise edition

Alternatywnie użyj Menedżera pakietów NuGet, aby zainstalować niezbędne pakiety. W menu Narzędzia w programie Visual Studio wybierz pozycję Menedżer pakietów NuGet, a następnie wybierz pozycję Konsola menedżera pakietów. Wprowadź następujące polecenie w oknie Konsola menedżera pakietów:

Install-Package Microsoft.AspNet.WebApi.WebHost

Dlaczego routing atrybutów?

Pierwsza wersja internetowego interfejsu API używa routingu opartego na konwencji . W tym typie routingu zdefiniujesz jeden lub więcej szablonów tras, które są w zasadzie sparametryzowanymi ciągami. Gdy platforma odbiera żądanie, pasuje do identyfikatora URI względem szablonu trasy. Aby uzyskać więcej informacji na temat routingu opartego na konwencji, zobacz Routing w interfejsie API sieci Web ASP.NET.

Jedną z zalet routingu opartego na konwencji jest to, że szablony są definiowane w jednym miejscu, a reguły routingu są stosowane spójnie we wszystkich kontrolerach. Niestety routing oparty na konwencji utrudnia obsługę niektórych wzorców identyfikatorów URI, które są wspólne w interfejsach API RESTful. Na przykład zasoby często zawierają zasoby podrzędne: Klienci mają zamówienia, filmy mają aktorów, książki mają autorów itd. Naturalne jest utworzenie identyfikatorów URI, które odzwierciedlają te relacje:

/customers/1/orders

Ten typ identyfikatora URI jest trudny do utworzenia przy użyciu routingu opartego na konwencji. Chociaż można to zrobić, wyniki nie są prawidłowo skalowane, jeśli masz wiele kontrolerów lub typów zasobów.

W przypadku routingu atrybutów jest to proste zdefiniowanie trasy dla tego identyfikatora URI. Wystarczy dodać atrybut do akcji kontrolera:

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

Oto kilka innych wzorców, które routing atrybutów ułatwia.

Przechowywanie wersji interfejsu API

W tym przykładzie polecenie "/api/v1/products" będzie kierowane do innego kontrolera niż "/api/v2/products".

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

Przeciążone segmenty identyfikatorów URI

W tym przykładzie "1" jest numerem zamówienia, ale "oczekiwanie" mapuje na kolekcję.

/orders/1 /orders/pending

Wiele typów parametrów

W tym przykładzie "1" jest numerem zamówienia, ale "2013/06/16" określa datę.

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

Włączanie routingu atrybutów

Aby włączyć routing atrybutów, wywołaj metodę MapHttpAttributeRoutes podczas konfiguracji. Ta metoda rozszerzenia jest zdefiniowana w klasie System.Web.Http.HttpConfigurationExtensions .

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.
        }
    }
}

Routing atrybutów można łączyć z routingiem opartym na konwencji . Aby zdefiniować trasy oparte na konwencji, wywołaj metodę MapHttpRoute .

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 }
        );
    }
}

Aby uzyskać więcej informacji na temat konfigurowania internetowego interfejsu API, zobacz Konfigurowanie internetowego interfejsu API 2 ASP.NET.

Uwaga: migrowanie z internetowego interfejsu API 1

Przed internetowym interfejsem API 2 szablony projektu internetowego interfejsu API wygenerowały kod podobny do następującego:

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

Jeśli routing atrybutów jest włączony, ten kod zgłosi wyjątek. Jeśli uaktualnisz istniejący projekt internetowego interfejsu API do używania routingu atrybutów, zaktualizuj ten kod konfiguracji do następujących elementów:

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

Uwaga

Aby uzyskać więcej informacji, zobacz Konfigurowanie internetowego interfejsu API przy użyciu ASP.NET hostingu.

Dodawanie atrybutów trasy

Oto przykład trasy zdefiniowanej przy użyciu atrybutu:

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

Ciąg "customers/{customerId}/orders" to szablon identyfikatora URI trasy. Internetowy interfejs API próbuje dopasować identyfikator URI żądania do szablonu. W tym przykładzie "klienci" i "zamówienia" to segmenty literału, a "{customerId}" jest parametrem zmiennej. Następujące identyfikatory URI pasują do tego szablonu:

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

Dopasowanie można ograniczyć przy użyciu ograniczeń opisanych w dalszej części tego tematu.

Zwróć uwagę, że parametr "{customerId}" w szablonie trasy jest zgodny z nazwą parametru customerId w metodzie . Gdy internetowy interfejs API wywołuje akcję kontrolera, próbuje powiązać parametry trasy. Jeśli na przykład identyfikator URI to http://example.com/customers/1/orders, internetowy interfejs API próbuje powiązać wartość "1" z parametrem customerId w akcji.

Szablon identyfikatora URI może mieć kilka parametrów:

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

Wszystkie metody kontrolera, które nie mają atrybutu trasy, używają routingu opartego na konwencji. W ten sposób można połączyć oba typy routingu w tym samym projekcie.

Metody HTTP

Internetowy interfejs API wybiera również akcje na podstawie metody HTTP żądania (GET, POST itp.). Domyślnie internetowy interfejs API szuka dopasowania wielkości liter do początku nazwy metody kontrolera. Na przykład metoda kontrolera o nazwie PutCustomers odpowiada żądaniu HTTP PUT.

Tę konwencję można zastąpić, dekorując metodę dowolną z następujących atrybutów:

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

W poniższym przykładzie internetowy interfejs API mapuje metodę CreateBook na żądania HTTP POST.

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

W przypadku wszystkich innych metod HTTP, w tym metod innych niż standardowe, należy użyć atrybutu AcceptVerbs , który przyjmuje listę metod HTTP.

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

Prefiksy tras

Często trasy w kontrolerze zaczynają się od tego samego prefiksu. Na przykład:

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) { ... }
}

Wspólny prefiks dla całego kontrolera można ustawić przy użyciu atrybutu [RoutePrefix] :

[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) { ... }
}

Użyj tyldy (~) w atrybucie metody, aby zastąpić prefiks trasy:

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

    // ...
}

Prefiks trasy może zawierać parametry:

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

Ograniczenia trasy

Ograniczenia trasy umożliwiają ograniczenie dopasowania parametrów w szablonie trasy. Ogólna składnia to "{parameter:constraint}". Na przykład:

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

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

W tym miejscu pierwsza trasa zostanie wybrana tylko wtedy, gdy segment "id" identyfikatora URI jest liczbą całkowitą. W przeciwnym razie zostanie wybrana druga trasa.

Poniższa tabela zawiera listę obsługiwanych ograniczeń.

Ograniczenie Opis Przykład
alpha Dopasuje wielkie lub małe litery alfabetu łacińskiego (a-z, A-Z) {x:alpha}
bool Pasuje do wartości logicznej. {x:bool}
datetime Dopasuje wartość typu Data/godzina . {x:datetime}
decimal Dopasuje wartość dziesiętną. {x:decimal}
double Pasuje do 64-bitowej wartości zmiennoprzecinkowej. {x:double}
float Pasuje do 32-bitowej wartości zmiennoprzecinkowej. {x:float}
guid Dopasuje wartość identyfikatora GUID. {x:guid}
int Dopasuje 32-bitową wartość całkowitą. {x:int}
length Dopasuje ciąg o określonej długości lub w określonym zakresie długości. {x:length(6)} {x:length(1,20)}
długi Dopasuje 64-bitową wartość całkowitą. {x:long}
max Dopasuje liczbę całkowitą z maksymalną wartością. {x:max(10)}
Maxlength Dopasuje ciąg o maksymalnej długości. {x:maxlength(10)}
min Dopasuje liczbę całkowitą z minimalną wartością. {x:min(10)}
Minlength Dopasuje ciąg o minimalnej długości. {x:minlength(10)}
range Dopasuje liczbę całkowitą w zakresie wartości. {x:range(10,50)}
Regex Pasuje do wyrażenia regularnego. {x:regex(^\d{3}-\d{3}-\d{4}$)}

Zwróć uwagę, że niektóre ograniczenia, takie jak "min", przyjmują argumenty w nawiasach. Można zastosować wiele ograniczeń do parametru oddzielonego dwukropkiem.

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

Ograniczenia trasy niestandardowej

Ograniczenia trasy niestandardowej można utworzyć, implementując interfejs IHttpRouteConstraint . Na przykład następujące ograniczenie ogranicza parametr do wartości całkowitej innej niż zero.

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;
    }
}

Poniższy kod pokazuje, jak zarejestrować ograniczenie:

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

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

Teraz możesz zastosować ograniczenie w swoich trasach:

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

Możesz również zastąpić całą klasę DefaultInlineConstraintResolver , implementując interfejs IInlineConstraintResolver . Spowoduje to zastąpienie wszystkich wbudowanych ograniczeń, chyba że implementacja interfejsu IInlineConstraintResolver w szczególności je doda.

Opcjonalne parametry identyfikatora URI i wartości domyślne

Parametr URI można ustawić jako opcjonalny, dodając znak zapytania do parametru trasy. Jeśli parametr trasy jest opcjonalny, musisz zdefiniować wartość domyślną dla parametru metody.

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

W tym przykładzie /api/books/locale/1033 i /api/books/locale zwróć ten sam zasób.

Alternatywnie możesz określić wartość domyślną w szablonie trasy w następujący sposób:

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

Jest to prawie takie samo jak w poprzednim przykładzie, ale występuje niewielka różnica w zachowaniu, gdy jest stosowana wartość domyślna.

  • W pierwszym przykładzie ("{lcid:int?}") domyślna wartość 1033 jest przypisywana bezpośrednio do parametru metody, więc parametr będzie miał tę dokładną wartość.
  • W drugim przykładzie ("{lcid:int=1033}") wartość domyślna "1033" przechodzi przez proces powiązania modelu. Domyślny element binder model-binder przekonwertuje wartość "1033" na wartość liczbową 1033. Można jednak podłączyć powiązanie modelu niestandardowego, co może zrobić coś innego.

(W większości przypadków, chyba że w potoku istnieją niestandardowe powiązania modelu, te dwa formularze będą równoważne).

Nazwy tras

W internetowym interfejsie API każda trasa ma nazwę. Nazwy tras są przydatne do generowania linków, dzięki czemu można dołączyć link do odpowiedzi HTTP.

Aby określić nazwę trasy, ustaw właściwość Name atrybutu. W poniższym przykładzie pokazano, jak ustawić nazwę trasy, a także jak używać nazwy trasy podczas generowania linku.

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;
    }
}

Kolejność tras

Gdy platforma próbuje dopasować identyfikator URI do trasy, ocenia trasy w określonej kolejności. Aby określić kolejność, ustaw właściwość Order atrybutu route. Niższe wartości są najpierw oceniane. Domyślna wartość zamówienia to zero.

Poniżej przedstawiono sposób określania całkowitej kolejności:

  1. Porównaj właściwość Order atrybutu route.

  2. Przyjrzyj się każdemu segmentowi identyfikatora URI w szablonie trasy. Dla każdego segmentu kolejność jest następująca:

    1. Segmenty literałów.
    2. Parametry trasy z ograniczeniami.
    3. Parametry trasy bez ograniczeń.
    4. Segmenty parametrów z symbolami wieloznacznymi z ograniczeniami.
    5. Segmenty parametrów z symbolami wieloznacznymi bez ograniczeń.
  3. W przypadku remisu trasy są uporządkowane według porównania ciągów porządkowych bez uwzględniania wielkości liter (OrdinalIgnoreCase) szablonu trasy.

Oto przykład: Załóżmy, że definiujesz następujący kontroler:

[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) { ... }
}

Te trasy są uporządkowane w następujący sposób.

  1. zamówienia/szczegóły
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. zamówienia/oczekujące

Zwróć uwagę, że "details" jest segmentem literału i pojawia się przed ciągiem "{id}", ale właściwość "Pending" jest wyświetlana jako ostatnia, ponieważ właściwość Order ma wartość 1. (W tym przykładzie założono, że nie ma klientów o nazwie "details" lub "pending". Ogólnie rzecz biorąc, staraj się unikać niejednoznacznych tras. W tym przykładzie lepszym szablonem trasy jest GetByCustomer "customers/{customerName}" )