Supporto delle relazioni tra entità in OData v3 con l'API Web 2

di Mike Wasson

Scaricare il progetto completato

La maggior parte dei set di dati definisce le relazioni tra le entità: i clienti hanno ordini; i libri hanno autori; i prodotti hanno fornitori. Usando OData, i client possono spostarsi tra le relazioni di entità. Dato un prodotto, è possibile trovare il fornitore. È anche possibile creare o rimuovere relazioni. Ad esempio, è possibile impostare il fornitore per un prodotto.

Questa esercitazione illustra come supportare queste operazioni in API Web ASP.NET. L'esercitazione si basa sull'esercitazione Creazione di un endpoint OData v3 con l'API Web 2.

Versioni software usate nell'esercitazione

  • API Web 2
  • OData versione 3
  • Entity Framework 6

Aggiungere un'entità Supplier

Prima di tutto è necessario aggiungere un nuovo tipo di entità al feed OData. Verrà aggiunta una Supplier classe.

using System.ComponentModel.DataAnnotations;

namespace ProductService.Models
{
    public class Supplier
    {
        [Key]
        public string Key { get; set; }
        public string Name { get; set; }
    }
}

Questa classe usa una stringa per la chiave di entità. In pratica, potrebbe essere meno comune rispetto all'uso di una chiave integer. Ma vale la pena vedere in che modo OData gestisce altri tipi di chiave oltre ai numeri interi.

Verrà quindi creata una relazione aggiungendo una Supplier proprietà alla Product classe :

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }

    // New code
    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

Aggiungere un nuovo DbSet alla ProductServiceContext classe , in modo che Entity Framework includa la Supplier tabella nel database.

public class ProductServiceContext : DbContext
{
    public ProductServiceContext() : base("name=ProductServiceContext")
    {
    }

    public System.Data.Entity.DbSet<ProductService.Models.Product> Products { get; set; }
    // New code:
    public System.Data.Entity.DbSet<ProductService.Models.Supplier> Suppliers { get; set; }
}

In WebApiConfig.cs aggiungere un'entità "Suppliers" al modello EDM:

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
// New code:
builder.EntitySet<Supplier>("Suppliers");

Per ottenere il fornitore per un prodotto, il client invia una richiesta GET:

GET /Products(1)/Supplier

Qui "Supplier" è una proprietà di navigazione sul Product tipo. In questo caso, Supplier fa riferimento a un singolo elemento, ma una proprietà di navigazione può restituire anche una raccolta (relazione uno-a-molti o molti-a-molti).

Per supportare questa richiesta, aggiungere il metodo seguente alla ProductsController classe :

// GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
    Product product = _context.Products.FirstOrDefault(p => p.ID == key);
    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return product.Supplier;
}

Il parametro key è la chiave del prodotto. Il metodo restituisce l'entità correlata, in questo caso un'istanza Supplier di . Il nome del metodo e il nome del parametro sono entrambi importanti. In generale, se la proprietà di navigazione è denominata "X", è necessario aggiungere un metodo denominato "GetX". Il metodo deve accettare un parametro denominato "key" che corrisponde al tipo di dati della chiave padre.

È anche importante includere l'attributo [FromOdataUri] nel parametro key . Questo attributo indica all'API Web di usare le regole di sintassi OData quando analizza la chiave dall'URI della richiesta.

OData supporta la creazione o la rimozione di relazioni tra due entità. Nella terminologia OData la relazione è un "collegamento". Ogni collegamento ha un URI con l'entità modulo/$links/entità. Ad esempio, il collegamento dal prodotto al fornitore è simile al seguente:

/Products(1)/$links/Supplier

Per creare un nuovo collegamento, il client invia una richiesta POST all'URI del collegamento. Il corpo della richiesta è l'URI dell'entità di destinazione. Si supponga, ad esempio, che esista un fornitore con la chiave "CTSO". Per creare un collegamento da "Product(1)" a "Supplier('CTSO')", il client invia una richiesta simile alla seguente:

POST http://localhost/odata/Products(1)/$links/Supplier
Content-Type: application/json
Content-Length: 50

{"url":"http://localhost/odata/Suppliers('CTSO')"}

Per eliminare un collegamento, il client invia una richiesta DELETE all'URI del collegamento.

Creazione di collegamenti

Per consentire a un client di creare collegamenti product-supplier, aggiungere il codice seguente alla ProductsController classe :

[AcceptVerbs("POST", "PUT")]
public async Task<IHttpActionResult> CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
            
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
            
    switch (navigationProperty)
    {
        case "Supplier":
            string supplierKey = GetKeyFromLinkUri<string>(link);
            Supplier supplier = await db.Suppliers.FindAsync(supplierKey);
            if (supplier == null)
            {
                return NotFound();
            }
            product.Supplier = supplier;
            await db.SaveChangesAsync();
            return StatusCode(HttpStatusCode.NoContent);

        default:
            return NotFound();
    }
}

Questo metodo accetta tre parametri, ovvero

  • key: chiave per l'entità padre (il prodotto)
  • navigationProperty: nome della proprietà di navigazione. In questo esempio, l'unica proprietà di navigazione valida è "Supplier".
  • link: URI OData dell'entità correlata. Questo valore viene ricavato dal corpo della richiesta. Ad esempio, l'URI del collegamento potrebbe essere "http://localhost/odata/Suppliers('CTSO'), ovvero il fornitore con ID = 'CTSO'.

Il metodo usa il collegamento per cercare il fornitore. Se viene trovato il fornitore corrispondente, il metodo imposta la Product.Supplier proprietà e salva il risultato nel database.

La parte più difficile consiste nell'analizzare l'URI del collegamento. In pratica, è necessario simulare il risultato dell'invio di una richiesta GET a tale URI. Il metodo helper seguente illustra come eseguire questa operazione. Il metodo richiama il processo di routing dell'API Web e recupera un'istanza ODataPath che rappresenta il percorso OData analizzato. Per un URI di collegamento, uno dei segmenti deve essere la chiave di entità. In caso contrario, il client ha inviato un URI non valido.

// Helper method to extract the key from an OData link URI.
private TKey GetKeyFromLinkUri<TKey>(Uri link)
{
    TKey key = default(TKey);

    // Get the route that was used for this request.
    IHttpRoute route = Request.GetRouteData().Route;

    // Create an equivalent self-hosted route. 
    IHttpRoute newRoute = new HttpRoute(route.RouteTemplate, 
        new HttpRouteValueDictionary(route.Defaults), 
        new HttpRouteValueDictionary(route.Constraints),
        new HttpRouteValueDictionary(route.DataTokens), route.Handler);

    // Create a fake GET request for the link URI.
    var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link);

    // Send this request through the routing process.
    var routeData = newRoute.GetRouteData(
        Request.GetConfiguration().VirtualPathRoot, tmpRequest);

    // If the GET request matches the route, use the path segments to find the key.
    if (routeData != null)
    {
        ODataPath path = tmpRequest.GetODataPath();
        var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
        if (segment != null)
        {
            // Convert the segment into the key type.
            key = (TKey)ODataUriUtils.ConvertFromUriLiteral(
                segment.Value, ODataVersion.V3);
        }
    }
    return key;
}

Eliminazione di collegamenti

Per eliminare un collegamento, aggiungere il codice seguente alla ProductsController classe :

public async Task<IHttpActionResult> DeleteLink([FromODataUri] int key, string navigationProperty)
{
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }

    switch (navigationProperty)
    {
        case "Supplier":
            product.Supplier = null;
            await db.SaveChangesAsync();
            return StatusCode(HttpStatusCode.NoContent);

        default:
            return NotFound();

    }
}

In questo esempio la proprietà di navigazione è una singola Supplier entità. Se la proprietà di navigazione è una raccolta, l'URI per eliminare un collegamento deve includere una chiave per l'entità correlata. Ad esempio:

DELETE /odata/Customers(1)/$links/Orders(1)

Questa richiesta rimuove l'ordine 1 dal cliente 1. In questo caso, il metodo DeleteLink avrà la firma seguente:

void DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty);

Il parametro relatedKey fornisce la chiave per l'entità correlata. Quindi nel DeleteLink metodo cercare l'entità primaria dal parametro chiave , trovare l'entità correlata dal parametro relatedKey e quindi rimuovere l'associazione. A seconda del modello di dati, potrebbe essere necessario implementare entrambe le versioni di DeleteLink. L'API Web chiamerà la versione corretta in base all'URI della richiesta.