Relazioni entità in OData v4 Usando API Web ASP.NET 2.2

di Mike Wasson

La maggior parte dei set di dati definisce le relazioni tra entità: i clienti hanno ordini; i libri hanno autori; i prodotti hanno fornitori. Usando OData, i client possono passare a 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 OData v4 usando API Web ASP.NET. L'esercitazione si basa sull'esercitazione Creare un endpoint OData v4 usando API Web ASP.NET 2.

Versioni software usate nell'esercitazione

  • API Web 2.1
  • OData v4
  • Visual Studio 2017 (scaricare Visual Studio 2017 qui)
  • Entity Framework 6
  • .NET 4.5

Versioni dell'esercitazione

Per OData versione 3, vedere Supporto delle relazioni di entità in OData v3.

Aggiungere un'entità fornitore

Nota

L'esercitazione si basa sull'esercitazione Creare un endpoint OData v4 usando API Web ASP.NET 2.

Prima di tutto, è necessaria un'entità correlata. Aggiungere una classe denominata Supplier nella cartella Models.

using System.Collections.Generic;

namespace ProductService.Models
{
    public class Supplier
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public ICollection<Product> Products { get; set; }
    }
}

Aggiungere una proprietà di Product spostamento alla classe:

using System.ComponentModel.DataAnnotations.Schema;

namespace ProductService.Models
{
    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 int? SupplierId { get; set; }
        public virtual Supplier Supplier { get; set; }
    }
}

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

public class ProductsContext : DbContext
{
    static ProductsContext()
    {
        Database.SetInitializer(new ProductInitializer());
    }

    public DbSet<Product> Products { get; set; }
    // New code:
    public DbSet<Supplier> Suppliers { get; set; }
}

In WebApiConfig.cs aggiungere un'entità "Suppliers" impostata sul modello di dati dell'entità:

public static void Register(HttpConfiguration config)
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Product>("Products");
    // New code:
    builder.EntitySet<Supplier>("Suppliers");
    config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
}

Aggiungere un controller fornitori

Aggiungere una SuppliersController classe alla cartella Controller.

using ProductService.Models;
using System.Linq;
using System.Web.OData;

namespace ProductService.Controllers
{
    public class SuppliersController : ODataController
    {
        ProductsContext db = new ProductsContext();

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

Non mostrerò come aggiungere operazioni CRUD per questo controller. I passaggi sono uguali a per il controller Products (vedere Creare un endpoint OData v4).

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

GET /Products(1)/Supplier

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

public class ProductsController : ODataController
{
    // GET /Products(1)/Supplier
    [EnableQuery]
    public SingleResult<Supplier> GetSupplier([FromODataUri] int key)
    {
        var result = db.Products.Where(m => m.Id == key).Select(m => m.Supplier);
        return SingleResult.Create(result);
    }
 
   // Other controller methods not shown.
}

Questo metodo usa una convenzione di denominazione predefinita

  • Nome metodo: GetX, dove X è la proprietà di spostamento.
  • Nome parametro: chiave

Se si segue questa convenzione di denominazione, l'API Web esegue automaticamente il mapping della richiesta HTTP al metodo controller.

Richiesta HTTP di esempio:

GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

Risposta HTTP di esempio:

HTTP/1.1 200 OK
Content-Length: 125
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 00:44:27 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Suppliers/$entity","Id":2,"Name":"Wingtip Toys"
}

Nell'esempio precedente, un prodotto ha un fornitore. Una proprietà di spostamento può anche restituire una raccolta. Il codice seguente ottiene i prodotti per un fornitore:

public class SuppliersController : ODataController
{
    // GET /Suppliers(1)/Products
    [EnableQuery]
    public IQueryable<Product> GetProducts([FromODataUri] int key)
    {
        return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products);
    }

    // Other controller methods not shown.
}

In questo caso, il metodo restituisce un IQueryable anziché un T SingleResult<>

Richiesta HTTP di esempio:

GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

Risposta HTTP di esempio:

HTTP/1.1 200 OK
Content-Length: 372
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 01:06:54 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Products","value":[
    {
      "Id":1,"Name":"Hat","Price":14.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":2,"Name":"Socks","Price":6.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":4,"Name":"Pogo Stick","Price":29.99,"Category":"Toys","SupplierId":2
    }
  ]
}

Creazione di una relazione tra entità

OData supporta la creazione o la rimozione di relazioni tra due entità esistenti. Nella terminologia OData v4 la relazione è un "riferimento". In OData v3 la relazione è stata chiamata collegamento. Le differenze del protocollo non sono importanti per questa esercitazione.

Un riferimento ha un proprio URI, con il modulo /Entity/NavigationProperty/$ref. Ad esempio, ecco l'URI per risolvere il riferimento tra un prodotto e il relativo fornitore:

http:/host/Products(1)/Supplier/$ref

Per aggiungere una relazione, il client invia una richiesta POST o PUT a questo indirizzo.

  • PUT se la proprietà di spostamento è una singola entità, ad esempio Product.Supplier.
  • POST se la proprietà di spostamento è una raccolta, ad esempio Supplier.Products.

Il corpo della richiesta contiene l'URI dell'altra entità nella relazione. Ecco una richiesta di esempio:

PUT http://myproductservice.example.com/Products(6)/Supplier/$ref HTTP/1.1
OData-Version: 4.0;NetFx
OData-MaxVersion: 4.0;NetFx
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Content-Type: application/json;odata.metadata=minimal
User-Agent: Microsoft ADO.NET Data Services
Host: myproductservice.example.com
Content-Length: 70
Expect: 100-continue

{"@odata.id":"http://myproductservice.example.com/Suppliers(4)"}

In questo esempio il client invia una richiesta PUT a /Products(6)/Supplier/$ref, ovvero l'URI $ref per il Supplier prodotto con ID = 6. Se la richiesta ha esito positivo, il server invia una risposta 204 (Nessun contenuto):

HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT

Ecco il metodo controller per aggiungere una relazione a un Productoggetto :

public class ProductsController : ODataController
{
    [AcceptVerbs("POST", "PUT")]
    public async Task<IHttpActionResult> CreateRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = await db.Products.SingleOrDefaultAsync(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }
        switch (navigationProperty)
        {
            case "Supplier":
                // Note: The code for GetKeyFromUri is shown later in this topic.
                var relatedKey = Helpers.GetKeyFromUri<int>(Request, link);
                var supplier = await db.Suppliers.SingleOrDefaultAsync(f => f.Id == relatedKey);
                if (supplier == null)
                {
                    return NotFound();
                }

                product.Supplier = supplier;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();
        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

Il parametro navigationProperty specifica la relazione da impostare. Se nell'entità sono presenti più proprietà di spostamento, è possibile aggiungere altre case istruzioni.

Il parametro di collegamento contiene l'URI del fornitore. L'API Web analizza automaticamente il corpo della richiesta per ottenere il valore per questo parametro.

Per cercare il fornitore, è necessario l'ID (o la chiave), che fa parte del parametro di collegamento . A tale scopo, usare il metodo helper seguente:

using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;

namespace ProductService
{
    public static class Helpers
    {
        public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

            string serviceRoot = urlHelper.CreateODataLink(
                request.ODataProperties().RouteName, 
                request.ODataProperties().PathHandler, new List<ODataPathSegment>());
            var odataPath = request.ODataProperties().PathHandler.Parse(
                request.ODataProperties().Model, 
                serviceRoot, uri.LocalPath);

            var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
            if (keySegment == null)
            {
                throw new InvalidOperationException("The link does not contain a key.");
            }

            var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
            return (TKey)value;
        }

    }
}

Fondamentalmente, questo metodo usa la libreria OData per suddividere il percorso URI in segmenti, trovare il segmento che contiene la chiave e convertire la chiave nel tipo corretto.

Eliminazione di una relazione tra entità

Per eliminare una relazione, il client invia una richiesta HTTP DELETE all'URI $ref:

DELETE http://host/Products(1)/Supplier/$ref

Ecco il metodo controller per eliminare la relazione tra un prodotto e un fornitore:

public class ProductsController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = db.Products.SingleOrDefault(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }

        switch (navigationProperty)
        {
            case "Supplier":
                product.Supplier = null;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }        

    // Other controller methods not shown.
}

In questo caso, Product.Supplier è la fine "1" di una relazione da 1 a molti, quindi è possibile rimuovere la relazione semplicemente impostando Product.Supplier su null.

Nella fine "molti" di una relazione, il client deve specificare quale entità correlata rimuovere. A tale scopo, il client invia l'URI dell'entità correlata nella stringa di query della richiesta. Ad esempio, per rimuovere "Product 1" da "Supplier 1":

DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)

Per supportare questa operazione nell'API Web, è necessario includere un parametro aggiuntivo nel DeleteRef metodo. Ecco il metodo controller per rimuovere un prodotto dalla Supplier.Products relazione.

public class SuppliersController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        [FromODataUri] string relatedKey, string navigationProperty)
    {
        var supplier = await db.Suppliers.SingleOrDefaultAsync(p => p.Id == key);
        if (supplier == null)
        {
            return StatusCode(HttpStatusCode.NotFound);
        }

        switch (navigationProperty)
        {
            case "Products":
                var productId = Convert.ToInt32(relatedKey);
                var product = await db.Products.SingleOrDefaultAsync(p => p.Id == productId);

                if (product == null)
                {
                    return NotFound();
                }
                product.Supplier = null;
                break;
            default:
                return StatusCode(HttpStatusCode.NotImplemented);

        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

Il parametro chiave è la chiave per il fornitore e il parametro relatedKey è la chiave per il prodotto da rimuovere dalla Products relazione. Si noti che l'API Web ottiene automaticamente la chiave dalla stringa di query.