Relacje jednostek w usłudze OData w wersji 4 przy użyciu interfejsu API sieci Web 2.2 ASP.NET

Autor: Mike Wasson

Większość zestawów danych definiuje relacje między jednostkami: klienci mają zamówienia; książki mają autorów; produkty mają dostawców. Za pomocą protokołu OData klienci mogą nawigować po relacjach między jednostkami. Biorąc pod uwagę produkt, możesz znaleźć dostawcę. Można również tworzyć lub usuwać relacje. Na przykład można ustawić dostawcę produktu.

W tym samouczku pokazano, jak obsługiwać te operacje w usłudze OData w wersji 4 przy użyciu ASP.NET internetowego interfejsu API. Samouczek jest oparty na samouczku Create an OData v4 Endpoint Using ASP.NET Web API 2 (Tworzenie punktu końcowego OData w wersji 4 przy użyciu ASP.NET internetowego interfejsu API 2).

Wersje oprogramowania używane w samouczku

  • Internetowy interfejs API 2.1
  • OData 4
  • Visual Studio 2017 (pobierz program Visual Studio 2017 tutaj)
  • Entity Framework 6
  • .NET 4.5

Wersje samouczka

Aby uzyskać informacje o danych OData w wersji 3, zobacz Obsługa relacji jednostek w usłudze OData w wersji 3.

Dodawanie jednostki dostawcy

Najpierw potrzebujemy powiązanej jednostki. Dodaj klasę o nazwie Supplier w folderze 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; }
    }
}

Dodaj właściwość nawigacji do Product klasy:

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

Dodaj nowy zestaw dbSet do ProductsContext klasy , aby program Entity Framework zawierał tabelę Supplier (Dostawca) w bazie danych.

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

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

W pliku WebApiConfig.cs dodaj jednostkę "Dostawcy" ustawioną na model danych jednostki:

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

Dodawanie kontrolera dostawców

Dodaj klasę SuppliersController do folderu 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);
        }
    }
}

Nie pokażę, jak dodać operacje CRUD dla tego kontrolera. Kroki są takie same jak w przypadku kontrolera Products (zobacz Tworzenie punktu końcowego OData w wersji 4).

Aby uzyskać dostawcę produktu, klient wysyła żądanie GET:

GET /Products(1)/Supplier

Aby obsłużyć to żądanie, dodaj następującą metodę ProductsController do klasy :

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

Ta metoda używa domyślnej konwencji nazewnictwa

  • Nazwa metody: GetX, gdzie X jest właściwością nawigacji.
  • Nazwa parametru: klucz

Jeśli przestrzegasz tej konwencji nazewnictwa, internetowy interfejs API automatycznie mapuje żądanie HTTP na metodę kontrolera.

Przykładowe żądanie HTTP:

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

Przykładowa odpowiedź 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"
}

W poprzednim przykładzie produkt ma jednego dostawcę. Właściwość nawigacji może również zwrócić kolekcję. Poniższy kod pobiera produkty dla dostawcy:

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

W tym przypadku metoda zwraca wartość IQueryable zamiast typu SingleResult<T>

Przykładowe żądanie HTTP:

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

Przykładowa odpowiedź 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
    }
  ]
}

Tworzenie relacji między jednostkami

Usługa OData obsługuje tworzenie lub usuwanie relacji między dwoma istniejącymi jednostkami. W terminologii OData w wersji 4 relacja jest "odwołaniem". (W usłudze OData w wersji 3 relacja była nazywana linkiem. Różnice między protokołami nie mają znaczenia w tym samouczku).

Odwołanie ma własny identyfikator URI z formularzem /Entity/NavigationProperty/$ref. Na przykład poniżej przedstawiono identyfikator URI do adresowania odwołania między produktem a jego dostawcą:

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

Aby dodać relację, klient wysyła żądanie POST lub PUT do tego adresu.

  • PUT, jeśli właściwość nawigacji jest pojedynczą jednostką, taką jak Product.Supplier.
  • POST, jeśli właściwość nawigacji jest kolekcją, taką jak Supplier.Products.

Treść żądania zawiera identyfikator URI innej jednostki w relacji. Oto przykładowe żądanie:

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

W tym przykładzie klient wysyła żądanie PUT do /Products(6)/Supplier/$refelementu , który jest identyfikatorem URI $ref dla Supplier produktu o identyfikatorze ID = 6. Jeśli żądanie powiedzie się, serwer wyśle odpowiedź 204 (brak zawartości):

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

Oto metoda kontrolera umożliwiająca dodanie relacji do klasy 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 określa, która relacja ma być ustawiona. (Jeśli w jednostce znajduje się więcej niż jedna właściwość nawigacji, możesz dodać więcej case instrukcji).

Parametr linku zawiera identyfikator URI dostawcy. Internetowy interfejs API automatycznie analizuje treść żądania, aby uzyskać wartość dla tego parametru.

Aby wyszukać dostawcę, potrzebujemy identyfikatora (lub klucza), który jest częścią parametru linku . W tym celu użyj następującej metody pomocniczej:

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

    }
}

Zasadniczo ta metoda używa biblioteki OData do dzielenia ścieżki identyfikatora URI na segmenty, znajdowania segmentu zawierającego klucz i konwertowania klucza na poprawny typ.

Usuwanie relacji między jednostkami

Aby usunąć relację, klient wysyła żądanie HTTP DELETE do identyfikatora URI $ref:

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

Oto metoda kontrolera umożliwiająca usunięcie relacji między produktem a dostawcą:

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

W tym przypadku Product.Supplier wartość to "1" koniec relacji od 1 do wielu, więc możesz usunąć relację tylko przez ustawienie wartości Product.Suppliernull.

Na końcu relacji "wiele" klient musi określić, która powiązana jednostka ma zostać usunięta. W tym celu klient wysyła identyfikator URI powiązanej jednostki w ciągu zapytania żądania. Aby na przykład usunąć produkt "Product 1" z "Supplier 1":

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

Aby to umożliwić w internetowym interfejsie API, musimy uwzględnić dodatkowy parametr w metodzie DeleteRef . Oto metoda kontrolera, aby usunąć produkt z Supplier.Products relacji.

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 klucza jest kluczem dostawcy, a parametr relatedKey jest kluczem do usunięcia produktu z Products relacji. Należy pamiętać, że internetowy interfejs API automatycznie pobiera klucz z ciągu zapytania.