Convenzioni di routing in API Web ASP.NET 2 Odata

Questo articolo descrive le convenzioni di routing usate dall'API Web 2 in ASP.NET 4.x per gli endpoint OData.

Quando l'API Web ottiene una richiesta OData, esegue il mapping della richiesta a un nome controller e a un nome di azione. Il mapping è basato sul metodo HTTP e sull'URI. Ad esempio, GET /odata/Products(1) esegue il mapping a ProductsController.GetProduct.

Nella parte 1 di questo articolo vengono descritte le convenzioni di routing OData predefinite. Queste convenzioni sono progettate specificamente per gli endpoint OData e sostituiscono il sistema di routing predefinito dell'API Web. La sostituzione avviene quando si chiama MapODataRoute.

Nella parte 2 viene illustrato come aggiungere convenzioni di routing personalizzate. Attualmente le convenzioni predefinite non coprono l'intera gamma di URI OData, ma è possibile estenderli per gestire casi aggiuntivi.

Convenzioni di routing predefinite

Prima di descrivere le convenzioni di routing OData nell'API Web, è utile comprendere gli URI OData. Un URI OData è costituito da:

  • Radice del servizio
  • Percorso della risorsa
  • Opzioni di query

Screenshot che mostra l'aspetto delle convenzioni di routing dei dati O, visualizzando le opzioni radice del servizio, percorso delle risorse e query da sinistra a destra.

Per il routing, la parte importante è il percorso della risorsa. Il percorso della risorsa è suddiviso in segmenti. Ad esempio, /Products(1)/Supplier ha tre segmenti:

  • Products fa riferimento a un set di entità denominato "Products".
  • 1 è una chiave di entità, selezionando una singola entità dal set.
  • Supplier è una proprietà di navigazione che seleziona un'entità correlata.

Quindi questo percorso sceglie il fornitore del prodotto 1.

Nota

I segmenti di percorso OData non corrispondono sempre ai segmenti URI. Ad esempio, "1" viene considerato un segmento di percorso.

Nomi dei controller. Il nome del controller viene sempre derivato dal set di entità nella radice del percorso della risorsa. Ad esempio, se il percorso della risorsa è /Products(1)/Supplier, l'API Web cerca un controller denominato ProductsController.

Nomi di azione. I nomi delle azioni sono derivati dai segmenti di percorso e dal modello di dati dell'entità (EDM), come indicato nelle tabelle seguenti. In alcuni casi sono disponibili due opzioni per il nome dell'azione. Ad esempio, "Get" o "GetProducts".

Esecuzione di query sulle entità

Richiesta URI di esempio Nome dell'azione Azione di esempio
GET /entityset /Prodotti GetEntitySet o Get GetProducts
GET /entityset(key) /Products(1) GetEntityType o Get GetProduct
GET /entityset(key)/cast /Products(1)/Models.Book GetEntityType o Get GetBook

Per altre informazioni, vedere Creare un endpoint OData Read-Only.

Creazione, aggiornamento ed eliminazione di entità

Richiesta URI di esempio Nome dell'azione Azione di esempio
POST /entityset /Prodotti PostEntityType o Post PostProduct
PUT /entityset(key) /Products(1) PutEntityType o Put PutProduct
PUT /entityset(key)/cast /Products(1)/Models.Book PutEntityType o Put PutBook
PATCH /entityset(key) /Products(1) PatchEntityType o Patch PatchProduct
PATCH /entityset(key)/cast /Products(1)/Models.Book PatchEntityType o Patch PatchBook
DELETE /entityset(key) /Products(1) DeleteEntityType o Delete DeleteProduct
DELETE /entityset(key)/cast /Products(1)/Models.Book DeleteEntityType o Delete DeleteBook

Esecuzione di query su una proprietà di navigazione

Richiesta URI di esempio Nome dell'azione Azione di esempio
GET /entityset(key)/navigation /Products(1)/Supplier GetNavigationFromEntityType o GetNavigation GetSupplierFromProduct
GET /entityset(key)/cast/navigation /Products(1)/Models.Book/Author GetNavigationFromEntityType o GetNavigation GetAuthorFromBook

Per altre informazioni, vedere Working with Entity Relations.For more information, see Working with Entity Relations.

Creazione ed eliminazione di collegamenti

Richiesta URI di esempio Nome dell'azione
POST /entityset(key)/$links/navigation /Products(1)/$links/Supplier CreateLink
PUT /entityset(key)/$links/navigation /Products(1)/$links/Supplier CreateLink
DELETE /entityset(key)/$links/navigation /Products(1)/$links/Supplier DeleteLink
DELETE /entityset(key)/$links/navigation(relatedKey) /Products/(1)/$links/Suppliers(1) DeleteLink

Per altre informazioni, vedere Working with Entity Relations.For more information, see Working with Entity Relations.

Proprietà

Richiede l'API Web 2

Richiesta URI di esempio Nome dell'azione Azione di esempio
GET /entityset(key)/property /Products(1)/Name GetPropertyFromEntityType o GetProperty GetNameFromProduct
GET /entityset(key)/cast/property /Products(1)/Models.Book/Author GetPropertyFromEntityType o GetProperty GetTitleFromBook

Actions

Richiesta URI di esempio Nome dell'azione Azione di esempio
POST /entityset(key)/action /Products(1)/Rate ActionNameOnEntityType o ActionName RateOnProduct
POST /entityset(key)/cast/action /Products(1)/Models.Book/CheckOut ActionNameOnEntityType o ActionName CheckOutOnBook

Per altre informazioni, vedere Azioni OData.

Firme del metodo

Ecco alcune regole per le firme del metodo:

  • Se il percorso contiene una chiave, l'azione deve avere un parametro denominato key.
  • Se il percorso contiene una chiave in una proprietà di navigazione, l'azione deve avere un parametro denominato relatedKey.
  • Decorare i parametri key e relatedKey con il parametro [FromODataUri].
  • Le richieste POST e PUT accettano un parametro del tipo di entità.
  • Le richieste PATCH accettano un parametro di tipo Delta<T>, dove T è il tipo di entità.

Per riferimento, di seguito è riportato un esempio che mostra le firme dei metodi per ogni convenzione di routing OData predefinita.

public class ProductsController : ODataController
{
    // GET /odata/Products
    public IQueryable<Product> Get()

    // GET /odata/Products(1)
    public Product Get([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book
    public Book GetBook([FromODataUri] int key)

    // POST /odata/Products 
    public HttpResponseMessage Post(Product item)

    // PUT /odata/Products(1)
    public HttpResponseMessage Put([FromODataUri] int key, Product item)

    // PATCH /odata/Products(1)
    public HttpResponseMessage Patch([FromODataUri] int key, Delta<Product> item)

    // DELETE /odata/Products(1)
    public HttpResponseMessage Delete([FromODataUri] int key)

    // PUT /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage PutBook([FromODataUri] int key, Book item)

    // PATCH /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage PatchBook([FromODataUri] int key, Delta<Book> item)

    // DELETE /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage DeleteBook([FromODataUri] int key)

    //  GET /odata/Products(1)/Supplier
    public Supplier GetSupplierFromProduct([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book/Author
    public Author GetAuthorFromBook([FromODataUri] int key)

    // POST /odata/Products(1)/$links/Supplier
    public HttpResponseMessage CreateLink([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)

    // DELETE /odata/Products(1)/$links/Supplier
    public HttpResponseMessage DeleteLink([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)

    // DELETE /odata/Products(1)/$links/Parts(1)
    public HttpResponseMessage DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty)

    // GET odata/Products(1)/Name
    // GET odata/Products(1)/Name/$value
    public HttpResponseMessage GetNameFromProduct([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book/Title
    // GET /odata/Products(1)/ODataRouting.Models.Book/Title/$value
    public HttpResponseMessage GetTitleFromBook([FromODataUri] int key)
}

Convenzioni di routing personalizzate

Attualmente le convenzioni predefinite non coprono tutti gli URI OData possibili. È possibile aggiungere nuove convenzioni implementando l'interfaccia IODataRoutingConvention . Questa interfaccia ha due metodi:

string SelectController(ODataPath odataPath, HttpRequestMessage request);
string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, 
    ILookup<string, HttpActionDescriptor> actionMap);
  • SelectController restituisce il nome del controller.
  • SelectAction restituisce il nome dell'azione.

Per entrambi i metodi, se la convenzione non si applica a tale richiesta, il metodo deve restituire Null.

Il parametro ODataPath rappresenta il percorso della risorsa OData analizzato. Contiene un elenco di istanze ODataPathSegment , una per ogni segmento del percorso della risorsa. ODataPathSegment è una classe astratta; ogni tipo di segmento è rappresentato da una classe che deriva da ODataPathSegment.

La proprietà ODataPath.TemplatePath è una stringa che rappresenta la concatenazione di tutti i segmenti di percorso. Ad esempio, se l'URI è /Products(1)/Supplier, il modello di percorso è "~/entityset/key/navigation". Si noti che i segmenti non corrispondono direttamente ai segmenti URI. Ad esempio, la chiave di entità (1) è rappresentata come ODataPathSegment.

In genere, un'implementazione di IODataRoutingConvention esegue le operazioni seguenti:

  1. Confrontare il modello di percorso per verificare se questa convenzione si applica alla richiesta corrente. Se non è applicabile, restituisce Null.
  2. Se si applica la convenzione, utilizzare le proprietà delle istanze ODataPathSegment per derivare i nomi di controller e azioni.
  3. Per le azioni, aggiungere tutti i valori al dizionario di route che deve essere associato ai parametri dell'azione (in genere chiavi di entità).

Esaminiamo un esempio specifico. Le convenzioni di routing predefinite non supportano l'indicizzazione in una raccolta di navigazione. In altre parole, non esiste alcuna convenzione per gli URI come segue:

/odata/Products(1)/Suppliers(1)

Ecco una convenzione di routing personalizzata per gestire questo tipo di query.

using Microsoft.Data.Edm;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.OData.Routing;
using System.Web.Http.OData.Routing.Conventions;

namespace ODataRouting
{
    public class NavigationIndexRoutingConvention : EntitySetRoutingConvention
    {
        public override string SelectAction(ODataPath odataPath, HttpControllerContext context, 
            ILookup<string, HttpActionDescriptor> actionMap)
        {
            if (context.Request.Method == HttpMethod.Get && 
                odataPath.PathTemplate == "~/entityset/key/navigation/key")
            {
                NavigationPathSegment navigationSegment = odataPath.Segments[2] as NavigationPathSegment;
                IEdmNavigationProperty navigationProperty = navigationSegment.NavigationProperty.Partner;
                IEdmEntityType declaringType = navigationProperty.DeclaringType as IEdmEntityType;

                string actionName = "Get" + declaringType.Name;
                if (actionMap.Contains(actionName))
                {
                    // Add keys to route data, so they will bind to action parameters.
                    KeyValuePathSegment keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
                    context.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value;

                    KeyValuePathSegment relatedKeySegment = odataPath.Segments[3] as KeyValuePathSegment;
                    context.RouteData.Values[ODataRouteConstants.RelatedKey] = relatedKeySegment.Value;

                    return actionName;
                }
            }
            // Not a match.
            return null;
        }
    }
}

Note:

  1. Si deriva da EntitySetRoutingConvention, perché il metodo SelectController in tale classe è appropriato per questa nuova convenzione di routing. Ciò significa che non è necessario implementare nuovamente SelectController.
  2. La convenzione si applica solo alle richieste GET e solo quando il modello di percorso è "~/entityset/key/navigation/key".
  3. Il nome dell'azione è "Get{EntityType}", dove {EntityType} è il tipo della raccolta di navigazione. Ad esempio, "GetSupplier". È possibile usare qualsiasi convenzione di denominazione desiderata, assicurandosi che le azioni del controller corrispondano.
  4. L'azione accetta due parametri denominati key e relatedKey. Per un elenco di alcuni nomi di parametri predefiniti, vedere ODataRouteConstants.

Il passaggio successivo consiste nell'aggiungere la nuova convenzione all'elenco delle convenzioni di routing. Ciò si verifica durante la configurazione, come illustrato nel codice seguente:

using ODataRouting.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;
using System.Web.Http.OData.Routing;
using System.Web.Http.OData.Routing.Conventions;

namespace ODataRouting
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
            // Create EDM (not shown).

            // Create the default collection of built-in conventions.
            var conventions = ODataRoutingConventions.CreateDefault();
            // Insert the custom convention at the start of the collection.
            conventions.Insert(0, new NavigationIndexRoutingConvention());

            config.Routes.MapODataRoute(routeName: "ODataRoute",
                routePrefix: "odata",
                model: modelBuilder.GetEdmModel(),
                pathHandler: new DefaultODataPathHandler(),
                routingConventions: conventions);

        }
    }
}

Ecco alcune altre convenzioni di routing di esempio utili per studiare:

E naturalmente l'API Web stessa è open source, quindi è possibile visualizzare il codice sorgente per le convenzioni di routing predefinite. Questi sono definiti nello spazio dei nomi System.Web.Http.OData.Routing.Conventions .