Condividi tramite


Routing degli attributi in API Web ASP.NET 2

Il routing è il modo in cui l'API Web corrisponde a un URI a un'azione. L'API Web 2 supporta un nuovo tipo di routing, denominato routing degli attributi. Come suggerisce il nome, il routing degli attributi usa attributi per definire le route. Il routing degli attributi offre un maggiore controllo sugli URI nell'API Web. Ad esempio, è possibile creare facilmente URI che descrivono gerarchie di risorse.

Lo stile precedente del routing, denominato routing basato su convenzioni, è ancora completamente supportato. In effetti, è possibile combinare entrambe le tecniche nello stesso progetto.

Questo argomento illustra come abilitare il routing degli attributi e descrive le varie opzioni per il routing degli attributi. Per un'esercitazione end-to-end che usa il routing degli attributi, vedere Creare un'API REST con routing degli attributi nell'API Web 2.

Prerequisiti

Visual Studio 2017 Community, Professional o Enterprise Edition

In alternativa, usare Gestione pacchetti NuGet per installare i pacchetti necessari. Dal menu Strumenti in Visual Studio selezionare Gestione pacchetti NuGet e quindi console di Gestione pacchetti. Immettere il comando seguente nella finestra Console di Gestione pacchetti:

Install-Package Microsoft.AspNet.WebApi.WebHost

Perché il routing degli attributi?

La prima versione dell'API Web usa il routing basato sulle convenzioni . In questo tipo di routing si definiscono uno o più modelli di route, che sono fondamentalmente stringhe con parametri. Quando il framework riceve una richiesta, corrisponde all'URI rispetto al modello di route. Per altre informazioni sul routing basato sulle convenzioni, vedere Routing in API Web ASP.NET.

Un vantaggio del routing basato sulle convenzioni è che i modelli vengono definiti in un'unica posizione e le regole di routing vengono applicate in modo coerente in tutti i controller. Sfortunatamente, il routing basato su convenzioni rende difficile supportare determinati modelli di URI comuni nelle API RESTful. Ad esempio, le risorse spesso contengono risorse figlio: i clienti hanno ordini, film hanno attori, libri hanno autori e così via. È naturale creare URI che riflettano queste relazioni:

/customers/1/orders

Questo tipo di URI è difficile da creare usando il routing basato su convenzioni. Anche se può essere eseguita, i risultati non vengono ridimensionati correttamente se sono presenti molti controller o tipi di risorse.

Con il routing degli attributi, è semplice definire una route per questo URI. È sufficiente aggiungere un attributo all'azione del controller:

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

Ecco alcuni altri modelli che semplificano il routing degli attributi.

Controllo delle versioni API

In questo esempio "/api/v1/products" viene instradato a un controller diverso da "/api/v2/products".

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

Segmenti URI di overload

In questo esempio "1" è un numero di ordine, ma "in sospeso" esegue il mapping a una raccolta.

/orders/1 /orders/pending

Più tipi di parametri

In questo esempio "1" è un numero di ordine, ma "2013/06/16" specifica una data.

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

Abilitazione del routing degli attributi

Per abilitare il routing degli attributi, chiamare MapHttpAttributeRoutes durante la configurazione. Questo metodo di estensione è definito nella classe 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.
        }
    }
}

Il routing degli attributi può essere combinato con il routing basato sulle convenzioni . Per definire route basate su convenzioni, chiamare il metodo 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 }
        );
    }
}

Per altre informazioni sulla configurazione dell'API Web, vedere Configurazione di API Web ASP.NET 2.

Nota: migrazione dall'API Web 1

Prima dell'API Web 2, i modelli di progetto API Web generavano codice simile al seguente:

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

Se il routing degli attributi è abilitato, questo codice genererà un'eccezione. Se si aggiorna un progetto API Web esistente per usare il routing degli attributi, assicurarsi di aggiornare questo codice di configurazione al seguente:

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

Nota

Per altre informazioni, vedere Configurazione dell'API Web con hosting ASP.NET.

Aggiunta di attributi di route

Di seguito è riportato un esempio di route definita usando un attributo :

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

La stringa "customers/{customerId}/orders" è il modello URI per la route. L'API Web tenta di associare l'URI della richiesta al modello. In questo esempio "customers" e "orders" sono segmenti letterali e "{customerId}" è un parametro variabile. Gli URI seguenti corrispondono a questo modello:

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

È possibile limitare la corrispondenza usando vincoli, descritti più avanti in questo argomento.

Si noti che il parametro "{customerId}" nel modello di route corrisponde al nome del parametro customerId nel metodo . Quando l'API Web richiama l'azione del controller, tenta di associare i parametri di route. Ad esempio, se l'URI è http://example.com/customers/1/orders, l'API Web tenta di associare il valore "1" al parametro customerId nell'azione.

Un modello URI può avere diversi parametri:

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

Tutti i metodi controller che non dispongono di un attributo di route usano il routing basato su convenzione. In questo modo, è possibile combinare entrambi i tipi di routing nello stesso progetto.

Metodi HTTP

L'API Web seleziona anche le azioni in base al metodo HTTP della richiesta (GET, POST e così via). Per impostazione predefinita, l'API Web cerca una corrispondenza senza distinzione tra maiuscole e minuscole con l'inizio del nome del metodo del controller. Ad esempio, un metodo controller denominato PutCustomers corrisponde a una richiesta HTTP PUT.

È possibile eseguire l'override di questa convenzione decorando il metodo con uno degli attributi seguenti:

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

Nell'esempio seguente l'API Web esegue il mapping del metodo CreateBook alle richieste HTTP POST.

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

Per tutti gli altri metodi HTTP, inclusi i metodi non standard, usare l'attributo AcceptVerbs , che accetta un elenco di metodi HTTP.

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

Prefissi di route

Spesso, le route in un controller iniziano con lo stesso prefisso. Ad esempio:

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

È possibile impostare un prefisso comune per un intero controller usando l'attributo [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) { ... }
}

Usare una tilde (~) nell'attributo del metodo per eseguire l'override del prefisso di route:

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

    // ...
}

Il prefisso di route può includere parametri:

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

Vincoli di route

I vincoli di route consentono di limitare la corrispondenza dei parametri nel modello di route. La sintassi generale è "{parameter:constraint}". Ad esempio:

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

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

In questo caso, la prima route verrà selezionata solo se il segmento "id" dell'URI è un numero intero. In caso contrario, verrà scelta la seconda route.

Nella tabella seguente sono elencati i vincoli supportati.

Vincolo Descrizione Esempio
alpha Trova la corrispondenza con caratteri alfabeti latini maiuscoli o minuscoli (a-z, A-Z) {x:alpha}
bool Trova la corrispondenza con un valore booleano. {x:bool}
Datetime Trova la corrispondenza con un valore DateTime . {x:datetime}
decimal Trova la corrispondenza con un valore decimale. {x:decimal}
double Trova la corrispondenza con un valore a virgola mobile a 64 bit. {x:double}
float Trova la corrispondenza con un valore a virgola mobile a 32 bit. {x:float}
guid Trova la corrispondenza con un valore GUID. {x:guid}
INT Trova la corrispondenza con un valore intero a 32 bit. {x:int}
length Trova la corrispondenza di una stringa con la lunghezza specificata o all'interno di un intervallo di lunghezze specificato. {x:length(6)} {x:length(1,20)}
long Trova la corrispondenza con un valore intero a 64 bit. {x:long}
max Trova la corrispondenza di un numero intero con un valore massimo. {x:max(10)}
Maxlength Trova la corrispondenza di una stringa con una lunghezza massima. {x:maxlength(10)}
min Trova la corrispondenza di un numero intero con un valore minimo. {x:min(10)}
Minlength Trova la corrispondenza di una stringa con una lunghezza minima. {x:minlength(10)}
range Trova la corrispondenza di un numero intero all'interno di un intervallo di valori. {x:range(10,50)}
regex Trova la corrispondenza con un'espressione regolare. {x:regex(^\d{3}-\d{3}-\d{4}$)}

Si noti che alcuni vincoli, ad esempio "min", accettano argomenti tra parentesi. È possibile applicare più vincoli a un parametro, separati da due punti.

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

Vincoli di route personalizzati

È possibile creare vincoli di route personalizzati implementando l'interfaccia IHttpRouteConstraint . Ad esempio, il vincolo seguente limita un parametro a un valore intero diverso da 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;
    }
}

Il codice seguente illustra come registrare il vincolo:

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

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

È ora possibile applicare il vincolo nelle route:

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

È anche possibile sostituire l'intera classe DefaultInlineConstraintResolver implementando l'interfaccia IInlineConstraintResolver . In questo modo verranno sostituiti tutti i vincoli predefiniti, a meno che l'implementazione di IInlineConstraintResolver non le aggigua in modo specifico.

Parametri URI facoltativi e valori predefiniti

È possibile rendere facoltativo un parametro URI aggiungendo un punto interrogativo al parametro di route. Se un parametro di route è facoltativo, è necessario definire un valore predefinito per il parametro del metodo.

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

In questo esempio /api/books/locale/1033 e /api/books/locale restituisce la stessa risorsa.

In alternativa, è possibile specificare un valore predefinito all'interno del modello di route, come indicato di seguito:

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

Questo è quasi lo stesso dell'esempio precedente, ma esiste una leggera differenza di comportamento quando viene applicato il valore predefinito.

  • Nel primo esempio ("{lcid:int?}"), il valore predefinito 1033 viene assegnato direttamente al parametro del metodo, quindi il parametro avrà questo valore esatto.
  • Nel secondo esempio ("{lcid:int=1033}"), il valore predefinito "1033" passa attraverso il processo di associazione di modelli. Lo strumento di associazione di modelli predefinito convertirà "1033" nel valore numerico 1033. Tuttavia, è possibile collegare un gestore di associazione di modelli personalizzato, che potrebbe eseguire un'operazione diversa.

Nella maggior parte dei casi, a meno che nella pipeline non siano presenti strumenti di associazione di modelli personalizzati, i due moduli saranno equivalenti.

Nomi di route

Nell'API Web ogni route ha un nome. I nomi di route sono utili per generare collegamenti, in modo da poter includere un collegamento in una risposta HTTP.

Per specificare il nome della route, impostare la proprietà Name sull'attributo . Nell'esempio seguente viene illustrato come impostare il nome della route e come usare il nome della route durante la generazione di un collegamento.

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

Ordine di itinerario

Quando il framework tenta di trovare una corrispondenza con un URI con una route, valuta le route in un ordine specifico. Per specificare l'ordine, impostare la proprietà Order sull'attributo di route. I valori inferiori vengono valutati per primi. Il valore predefinito dell'ordine è zero.

Ecco come viene determinato l'ordinamento totale:

  1. Confrontare la proprietà Order dell'attributo di route.

  2. Esaminare ogni segmento URI nel modello di route. Per ogni segmento, ordinare come segue:

    1. Segmenti letterali.
    2. Indirizzare i parametri con vincoli.
    3. Parametri di route senza vincoli.
    4. Segmenti di parametro con caratteri jolly con vincoli.
    5. Segmenti di parametro con caratteri jolly senza vincoli.
  3. Nel caso di un legame, le route vengono ordinate in base a un confronto tra stringhe ordinali senza distinzione tra maiuscole e minuscole (OrdinalIgnoreCase) del modello di route.

Ecco un esempio. Si supponga di definire il controller seguente:

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

Queste route vengono ordinate come indicato di seguito.

  1. ordini/dettagli
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. ordini/in sospeso

Si noti che "details" è un segmento letterale e viene visualizzato prima di "{id}", ma "in sospeso" viene visualizzato per ultimo perché la proprietà Order è 1. In questo esempio si presuppone che non ci siano clienti denominati "details" o "pending". In generale, provare a evitare route ambigue. In questo esempio, un modello di route migliore per GetByCustomer è "customers/{customerName}" )