Vztahy entit v OData v4 s využitím webového rozhraní API ASP.NET 2.2

Mike Wasson

Většina datových sad definuje vztahy mezi entitami: Zákazníci mají objednávky; knihy mají autory; výrobky mají dodavatele. Pomocí OData můžou klienti procházet vztahy entit. Vzhledem k produktu můžete najít dodavatele. Můžete také vytvořit nebo odebrat relace. Můžete například nastavit dodavatele produktu.

V tomto kurzu se dozvíte, jak tyto operace v OData v4 podporovat pomocí ASP.NET webového rozhraní API. Kurz vychází z kurzu Vytvoření koncového bodu OData v4 pomocí webového rozhraní API ASP.NET 2.

Verze softwaru použité v tomto kurzu

  • Webové rozhraní API 2.1
  • OData v4
  • Visual Studio 2017 (visual Studio 2017 si můžete stáhnout tady)
  • Entity Framework 6
  • .NET 4.5

Verze kurzů

Informace o OData verze 3 najdete v tématu Podpora vztahů entit v OData v3.

Přidání entity dodavatele

Nejprve potřebujeme související entitu. Do složky Models přidejte třídu s názvem Supplier .

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; }
    }
}

Přidejte do Product třídy vlastnost navigace:

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; }
    }
}

Přidejte do ProductsContext třídy novou DbSet, aby Entity Framework zahrnovala tabulku Supplier (Dodavatel) do databáze.

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

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

V souboru WebApiConfig.cs přidejte sadu entit Dodavatelé do datového modelu entity:

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());
}

Přidání kontroleru dodavatelů

SuppliersController Přidejte třídu do složky Controllers.

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

Neukazuji, jak přidat operace CRUD pro tento kontroler. Postup je stejný jako u kontroleru Products (viz Vytvoření koncového bodu OData v4).

Pokud chce klient získat dodavatele produktu, odešle požadavek GET:

GET /Products(1)/Supplier

Pro podporu tohoto požadavku přidejte do třídy následující metodu ProductsController :

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.
}

Tato metoda používá výchozí zásady vytváření názvů.

  • Název metody: GetX, kde X je vlastnost navigace.
  • Název parametru: klíč

Pokud budete postupovat podle těchto zásad vytváření názvů, webové rozhraní API automaticky mapuje požadavek HTTP na metodu kontroleru.

Příklad požadavku HTTP:

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

Příklad odpovědi HTTP:

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"
}

V předchozím příkladu má produkt jednoho dodavatele. Vlastnost navigace může také vrátit kolekci. Následující kód získá produkty pro dodavatele:

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.
}

V tomto případě metoda vrátí IQueryable místo SingleResult<T>.

Příklad požadavku HTTP:

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

Příklad odpovědi HTTP:

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
    }
  ]
}

Vytvoření relace mezi entitami

OData podporuje vytváření nebo odebírání relací mezi dvěma existujícími entitami. V terminologii OData v4 je relace "odkaz". (V OData v3 se relace označovala jako odkaz. Rozdíly v protokolech nejsou pro tento kurz důležité.)

Odkaz má vlastní identifikátor URI s tvarem /Entity/NavigationProperty/$ref. Tady je například identifikátor URI, který řeší odkaz mezi produktem a jeho dodavatelem:

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

Pokud chcete přidat relaci, odešle klient na tuto adresu požadavek POST nebo PUT.

  • PUT, pokud je vlastnost navigace jedinou entitou, například Product.Supplier.
  • POST, pokud je vlastnost navigace kolekce, například Supplier.Products.

Tělo požadavku obsahuje identifikátor URI druhé entity v relaci. Tady je příklad požadavku:

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

V tomto příkladu klient odešle požadavek PUT na /Products(6)/Supplier/$refadresu , což je identifikátor URI $ref produktu Supplier s ID = 6. Pokud požadavek proběhne úspěšně, server odešle odpověď 204 (bez obsahu):

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

Tady je metoda kontroleru pro přidání relace do Product:

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.
}

Parametr navigationProperty určuje, který vztah se má nastavit. (Pokud entita obsahuje více než jednu navigační vlastnost, můžete přidat další case příkazy.)

Parametr link obsahuje identifikátor URI dodavatele. Webové rozhraní API automaticky analyzuje text požadavku, aby získalo hodnotu tohoto parametru.

K vyhledání dodavatele potřebujeme ID (nebo klíč), který je součástí parametru link . K tomu použijte následující pomocnou metodu:

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;
        }

    }
}

V podstatě tato metoda používá knihovnu OData k rozdělení cesty identifikátoru URI na segmenty, vyhledání segmentu, který obsahuje klíč, a převod klíče na správný typ.

Odstranění relace mezi entitami

Pokud chcete odstranit relaci, klient odešle požadavek HTTP DELETE na identifikátor URI $ref:

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

Tady je metoda kontroleru pro odstranění vztahu mezi produktem a dodavatelem:

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.
}

V tomto případě je "1" konec relace 1:N, takže relaci můžete odebrat tak, Product.Supplier že nastavíte Product.Supplier na null.

Na konci relace "N" musí klient určit, která související entita se má odebrat. Za tímto účelem klient odešle identifikátor URI související entity v řetězci dotazu požadavku. Pokud chcete například odebrat produkt 1 z dodavatele 1:

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

Abychom to mohli ve webovém rozhraní API podporovat, musíme do DeleteRef metody zahrnout další parametr. Tady je metoda kontroleru pro odebrání produktu z Supplier.Products relace.

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.
}

Parametr key je klíčem pro dodavatele a parametr relatedKey je klíčem k odebrání produktu z Products relace. Všimněte si, že webové rozhraní API automaticky získá klíč z řetězce dotazu.