Condividi tramite


Routing e selezione di azioni nell'API Web di ASP.NET

Questo articolo descrive come ASP.NET'API Web instrada una richiesta HTTP a una determinata azione su un controller.

Annotazioni

Per una panoramica generale del routing, vedere Routing in ASP.NET API Web.

Questo articolo esamina i dettagli del processo di routing. Se si crea un progetto API Web e si scopre che alcune richieste non vengono indirizzate nel modo previsto, si spera che questo articolo possa essere utile.

Il routing ha tre fasi principali:

  1. Corrispondenza dell'URI a un modello di percorso.
  2. Selezione di un controller.
  3. Selezione di un'azione.

È possibile sostituire alcune parti del processo con comportamenti personalizzati. In questo articolo viene descritto il comportamento predefinito. Alla fine, annoto i luoghi in cui è possibile personalizzare il comportamento.

Modelli di route

Un modello di percorso è simile a un percorso URI, ma può contenere valori segnaposto, indicati con parentesi graffe:

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

Quando si crea una route, è possibile specificare i valori predefiniti per alcuni o tutti i segnaposto:

defaults: new { category = "all" }

È anche possibile fornire vincoli, che limitano la modalità di corrispondenza di un segmento URI con un segnaposto:

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

Il framework tenta di trovare la corrispondenza con i segmenti nel percorso URI del modello. I valori letterali nel modello devono corrispondere esattamente. Un segnaposto corrisponde a qualsiasi valore, a meno che non si specifichino vincoli. Il framework non corrisponde ad altre parti dell'URI, ad esempio il nome host o i parametri di query. Il framework seleziona la prima route nella tabella di route che corrisponde all'URI.

Esistono due segnaposto speciali: "{controller}" e "{action}".

  • "{controller}" fornisce il nome del controller.
  • "{action}" fornisce il nome dell'azione. Nell'API Web, la convenzione consueta consiste nell'omettere "{action}".

Impostazioni predefinite

Se fornisci valori predefiniti, la route corrisponderà a un URI che manca di quei segmenti. Per esempio:

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

GLI URI http://localhost/api/products/all e http://localhost/api/products corrispondono alla route precedente. Nell'ultimo URI, al segmento mancante {category} viene assegnato il valore allpredefinito .

Dizionario delle rotte

Se il framework trova una corrispondenza per un URI, crea un dizionario contenente il valore per ogni segnaposto. Le chiavi sono i nomi segnaposto, senza includere le parentesi graffe. I valori vengono ricavati dal percorso URI o dalle impostazioni predefinite. Il dizionario viene archiviato nell'oggetto IHttpRouteData .

Durante questa fase di corrispondenza della route, i segnaposto speciali "{controller}" e "{action}" vengono considerati proprio come gli altri segnaposto. Vengono semplicemente archiviati nel dizionario con gli altri valori.

Un valore predefinito può avere il valore speciale RouteParameter.Optional. Se a un segnaposto viene assegnato questo valore, il valore non viene aggiunto al dizionario di route. Per esempio:

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

Per il percorso URI "api/products", il dizionario di route conterrà:

  • controller: "prodotti"
  • categoria: "tutti"

Per "api/products/toys/123", tuttavia, il dizionario di route conterrà:

  • controller: "prodotti"
  • categoria: "giocattoli"
  • id: "123"

Le impostazioni predefinite possono includere anche un valore che non viene visualizzato in nessun punto del modello di route. Se la route corrisponde, tale valore viene archiviato nel dizionario. Per esempio:

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

Se il percorso URI è "api/root/8", il dizionario conterrà due valori:

  • controller: "clienti"
  • id: "8"

Selezione di un controller

La selezione del controller viene gestita dal metodo IHttpControllerSelector.SelectController . Questo metodo accetta un'istanza HttpRequestMessage e restituisce un HttpControllerDescriptor. L'implementazione predefinita viene fornita dalla classe DefaultHttpControllerSelector . Questa classe usa un algoritmo semplice:

  1. Cercare nel dizionario di route la chiave "controller".
  2. Prendere il valore per questa chiave e aggiungere la stringa "Controller" per ottenere il nome del tipo di controller.
  3. Cercare un controller API Web con questo nome di tipo.

Ad esempio, se il dizionario di route contiene la coppia chiave-valore "controller" = "products", il tipo di controller è "ProductsController". Se non è presente alcun tipo corrispondente o più corrispondenze, il framework restituisce un errore al client.

Per il passaggio 3 , DefaultHttpControllerSelector usa l'interfaccia IHttpControllerTypeResolver per ottenere l'elenco dei tipi di controller API Web. L'implementazione predefinita di IHttpControllerTypeResolver restituisce tutte le classi pubbliche che (a) implementano IHttpController, (b) non sono astratte e (c) hanno un nome che termina con "Controller".

Selezione azione

Dopo aver selezionato il controller, il framework seleziona l'azione chiamando il metodo IHttpActionSelector.SelectAction . Questo metodo accetta un oggetto HttpControllerContext e restituisce un HttpActionDescriptor.

L'implementazione predefinita viene fornita dalla classe ApiControllerActionSelector . Per selezionare un'azione, esamina quanto segue:

  • Metodo HTTP della richiesta.
  • Il segnaposto "{action}" del modello di itinerario, se presente.
  • Parametri delle azioni nel controller.

Prima di esaminare l'algoritmo di selezione, è necessario comprendere alcuni aspetti sulle azioni del controller.

Quali metodi nel controller sono considerati "azioni"? Quando si seleziona un'azione, il framework esamina solo i metodi di istanza pubblica nel controller. Esclude inoltre i metodi "nome speciale" (costruttori, eventi, overload degli operatori e così via) e metodi ereditati dalla classe ApiController .

Metodi HTTP. Il framework sceglie solo le azioni che corrispondono al metodo HTTP della richiesta, determinate come indicato di seguito:

  1. È possibile specificare il metodo HTTP con un attributo: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost o HttpPut.
  2. In caso contrario, se il nome del metodo controller inizia con "Get", "Post", "Put", "Delete", "Head", "Options" o "Patch", per convenzione l'azione supporta tale metodo HTTP.
  3. Se nessuno dei precedenti, il metodo supporta POST.

Associazioni di parametri. Un'associazione di parametri è il modo in cui l'API Web crea un valore per un parametro. Ecco la regola predefinita per l'associazione di parametri:

  • I tipi semplici vengono ricavati dall'URI.
  • I tipi complessi vengono ricavati dal corpo della richiesta.

I tipi semplici includono tutti i tipi primitivi di .NET Framework, oltre a DateTime, Decimal, Guid, String e TimeSpan. Per ogni azione, al massimo un parametro può leggere il corpo della richiesta.

Annotazioni

È possibile eseguire l'override delle regole di associazione predefinite. Consultare Associazione di parametri WebAPI dietro le quinte.

Con tale sfondo, ecco l'algoritmo di selezione dell'azione.

  1. Creare un elenco di tutte le azioni nel controller che corrispondono al metodo di richiesta HTTP.

  2. Se il dizionario delle rotte ha una voce "action", rimuovere le azioni i cui nomi non corrispondono a questo valore.

  3. Provare a associare i parametri di azione all'URI, come indicato di seguito:

    1. Per ogni azione, ottenere un elenco dei parametri che sono un tipo semplice, in cui l'associazione ottiene il parametro dall'URI. Escludere parametri facoltativi.
    2. Da questo elenco provare a trovare una corrispondenza per ogni nome di parametro, nel dizionario di route o nella stringa di query URI. Le corrispondenze non fanno distinzione tra maiuscole e minuscole e non dipendono dall'ordine dei parametri.
    3. Selezionare un'azione in cui ogni parametro nell'elenco ha una corrispondenza nell'URI.
    4. Se più azioni soddisfano questi criteri, selezionare quella con la maggior parte dei parametri corrispondenti.
  4. Ignorare le azioni con l'attributo [NonAction].

Il passaggio 3 è probabilmente il più confuso. L'idea di base è che un parametro può ottenere il relativo valore dall'URI, dal corpo della richiesta o da un'associazione personalizzata. Per i parametri provenienti dall'URI, è necessario assicurarsi che l'URI contenga effettivamente un valore per tale parametro, nel percorso (tramite il dizionario di route) o nella stringa di query.

Si consideri ad esempio l'azione seguente:

public void Get(int id)

Il parametro id viene associato all'URI. Pertanto, questa azione può corrispondere solo a un URI che contiene un valore per "id", nel dizionario di route o nella stringa di query.

I parametri facoltativi sono un'eccezione, perché sono facoltativi. Per un parametro facoltativo, è OK se l'associazione non riesce a ottenere il valore dall'URI.

I tipi complessi sono un'eccezione per un motivo diverso. Un tipo complesso può essere associato solo all'URI tramite un'associazione personalizzata. In tal caso, tuttavia, il framework non può sapere in anticipo se il parametro verrebbe associato a un particolare URI. Per scoprirlo, sarebbe necessario attivare il binding. L'obiettivo dell'algoritmo di selezione è selezionare un'azione dalla descrizione statica, prima di richiamare eventuali associazioni. Di conseguenza, i tipi complessi vengono esclusi dall'algoritmo corrispondente.

Dopo aver selezionato l'azione, vengono richiamate tutte le associazioni di parametri.

Riepilogo:

  • L'azione deve corrispondere al metodo HTTP della richiesta.
  • Il nome dell'azione deve corrispondere alla voce "action" nel dizionario delle route, se presente.
  • Per ogni parametro dell'azione, se il parametro viene tratto dall'URI, il nome del parametro deve essere trovato nel dizionario di route o nella stringa di query URI. I parametri e i parametri facoltativi con tipi complessi sono esclusi.
  • Provare a trovare la corrispondenza con il maggior numero di parametri. La corrispondenza migliore potrebbe essere un metodo senza parametri.

Esempio esteso

Itinerari:

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

Richiesta HTTP:

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

Corrispondenza delle rotte

L'URI corrisponde alla route denominata "DefaultApi". Il dizionario di route contiene le voci seguenti:

  • controller: "prodotti"
  • id: "1"

Il dizionario di route non contiene i parametri della stringa di query, "version" e "details", ma questi verranno comunque considerati durante la selezione dell'azione.

Selezione controller

Dall'elemento "controller" nel dizionario delle route, il tipo di controller è ProductsController.

Selezione azione

La richiesta HTTP è una richiesta GET. Le azioni del controller che supportano GET sono GetAll, GetByIde FindProductsByName. Il dizionario di route non contiene una voce per "action", quindi non dobbiamo abbinare il nome dell'azione.

Successivamente, si tenta di trovare la corrispondenza con i nomi dei parametri per le azioni, esaminando solo le azioni GET.

Action Parametri da abbinare
GetAll none
GetById "id"
FindProductsByName nome

Si noti che il parametro version di GetById non è considerato, perché è un parametro facoltativo.

Il GetAll metodo corrisponde in modo semplice. Il metodo GetById corrisponde anch'esso, poiché il dizionario di rotte contiene "id". Il FindProductsByName metodo non corrisponde.

Il GetById metodo vince, perché corrisponde a un parametro, anziché a nessun parametro per GetAll. Il metodo viene richiamato con i valori dei parametri seguenti:

  • id = 1
  • version = 1.5

Si noti che anche se la versione non è stata usata nell'algoritmo di selezione, il valore del parametro deriva dalla stringa di query URI.

Punti di estensione

L'API Web fornisce punti di estensione per alcune parti del processo di routing.

Interfaccia Descrizione
IHttpControllerSelector Seleziona il controller.
IHttpControllerTypeResolver Ottiene l'elenco dei tipi di controller. DefaultHttpControllerSelector sceglie il tipo di controller da questo elenco.
IAssembliesResolver Ottiene l'elenco degli assembly del progetto. L'interfaccia IHttpControllerTypeResolver usa questo elenco per trovare i tipi di controller.
IHttpControllerActivator Crea nuove istanze del controller.
IHttpActionSelector Seleziona l'azione.
IHttpActionInvoker Richiama l'azione.

Per fornire un'implementazione personalizzata per una di queste interfacce, usare l'insieme Services nell'oggetto HttpConfiguration :

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