Condividi tramite


Selezione di routing e azione in API Web ASP.NET

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

Nota

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

Questo articolo esamina i dettagli del processo di routing. Se si crea un progetto API Web e si trova 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 route.
  2. Selezione di un controller.
  3. Selezione di un'azione.

È possibile sostituire alcune parti del processo con i propri 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 route è simile a un percorso URI, ma può avere 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 il modo in cui un segmento URI può corrispondere a un segnaposto:

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

Il framework tenta di corrispondere ai 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 corrispondente 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 consueta convenzione consiste nell'omettere "{action}".

Valori predefiniti

Se si specificano valori predefiniti, la route corrisponderà a un URI mancante. Ad 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. In quest'ultimo URI il segmento mancante {category} viene assegnato il valore allpredefinito .

Dizionario route

Se il framework trova una corrispondenza per un URI, crea un dizionario contenente il valore per ogni segnaposto. Le chiavi sono i nomi segnaposto, non incluse le parentesi graffe. I valori vengono presi 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 trattati esattamente come gli altri segnaposto. Vengono semplicemente archiviati nel dizionario con gli altri valori.

Un valore predefinito può avere il valore speciale RouteParameter.Optional. Se un segnaposto viene assegnato questo valore, il valore non viene aggiunto al dizionario di route. Ad 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: "products"
  • categoria: "all"

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

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

Le impostazioni predefinite possono includere anche un valore che non viene visualizzato ovunque nel modello di route. Se la route corrisponde, tale valore viene archiviato nel dizionario. Ad 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 di HttpRequestMessage e restituisce un httpControllerDescriptor. L'implementazione predefinita viene fornita dalla classe DefaultHttpControllerSelector . Questa classe usa un algoritmo semplice:

  1. Cercare il dizionario di route per 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 esiste alcun tipo di corrispondenza 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 in "Controller".

Selezione azioni

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

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

  • Metodo HTTP della richiesta.
  • Segnaposto "{action}" nel modello di route, se presente.
  • Parametri delle azioni nel controller.

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

Quali metodi sul controller vengono 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", quindi per convenzione l'azione supporta tale metodo HTTP.
  3. Se nessuna delle versioni 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 acquisiti dall'URI.
  • I tipi complessi vengono presi dal corpo della richiesta.

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

Nota

È possibile eseguire l'override delle regole di associazione predefinite. Vedere Associazione di parametri WebAPI sotto il cappuccio.

Con questo background, ecco l'algoritmo di selezione delle azioni.

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

  2. Se il dizionario di route ha una voce "action", rimuovere le azioni il cui nome non corrisponde a questo valore.

  3. Provare a corrispondere ai parametri dell'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. In questo elenco provare a trovare una corrispondenza per ogni nome di parametro, nel dizionario di route o nella stringa di query URI. Le corrispondenze sono senza 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 un'azione soddisfa questi criteri, selezionare quella con la maggior parte dei parametri corrispondente.
  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, si vuole 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 può 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 questo caso, tuttavia, il framework non può sapere in anticipo se il parametro verrà associato a un particolare URI. Per scoprire, sarebbe necessario richiamare l'associazione. L'obiettivo dell'algoritmo di selezione consiste nel selezionare un'azione dalla descrizione statica, prima di richiamare eventuali associazioni. Pertanto, 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 di route, se presente.
  • Per ogni parametro dell'azione, se il parametro viene preso 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 corrispondere al numero massimo 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 route

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

  • controller: "products"
  • 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

Dalla voce "controller" nel dizionario di route il tipo di controller è ProductsController.

Selezione azioni

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 è necessario corrispondere al nome dell'azione.

Provare quindi a corrispondere ai nomi dei parametri per le azioni, esaminando solo le azioni GET.

Azione Parametri da trovare in corrispondenza
GetAll Nessuno
GetById "id"
FindProductsByName "name"

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

Il GetAll metodo corrisponde in modo semplice. Il GetById metodo corrisponde anche perché il dizionario di route contiene "id". Il FindProductsByName metodo non corrisponde.

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

  • id = 1
  • version = 1.5

Si noti che anche se la versione non è stata usata nell'algoritmo di selezione, il valore del parametro proviene 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 di 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));