Поделиться через


Отношения сущностей в OData версии 4 с использованием веб-API ASP.NET 2.2

Майк Уосон

Большинство наборов данных определяют отношения между сущностями: клиенты имеют заказы; книги имеют авторов; у продуктов есть поставщики. С помощью OData клиенты могут перемещаться по отношениям сущностей. При использовании продукта вы можете найти поставщика. Вы также можете создавать или удалять связи. Например, можно задать поставщика для продукта.

В этом руководстве показано, как поддерживать эти операции в OData версии 4 с помощью веб-API ASP.NET. В основе этого руководства — создание конечной точки OData версии 4 с помощью веб-API ASP.NET 2.

Версии программного обеспечения, используемые в этом руководстве

  • Веб-API 2.1
  • OData v4
  • Visual Studio 2017 (скачайте Visual Studio 2017 здесь)
  • Entity Framework 6
  • .NET 4.5

Версии учебников

Сведения о OData версии 3 см. в разделе Поддержка отношений сущностей в OData версии 3.

Добавление сущности поставщика

Примечание

В основе этого руководства — создание конечной точки OData версии 4 с помощью веб-API ASP.NET 2.

Во-первых, нам нужна связанная сущность. Добавьте класс с именем Supplier в папку 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; }
    }
}

Добавьте свойство навигации в класс :Product

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

Добавьте новый dbSet в ProductsContext класс , чтобы Entity Framework включал таблицу "Поставщик" в базу данных.

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

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

В Файле WebApiConfig.cs добавьте набор сущностей "Поставщики" в модель данных сущности:

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

Добавление контроллера поставщиков

Добавьте SuppliersController класс в папку 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);
        }
    }
}

Я не буду показывать, как добавить операции CRUD для этого контроллера. Действия выполняются так же, как и для контроллера Products (см. раздел Создание конечной точки OData версии 4).

Чтобы получить поставщика для продукта, клиент отправляет запрос GET:

GET /Products(1)/Supplier

Для поддержки этого запроса добавьте следующий метод в 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.
}

Этот метод использует соглашение об именовании по умолчанию

  • Имя метода: GetX, где X — это свойство навигации.
  • Имя параметра: key

Если следовать этому соглашению об именовании, веб-API автоматически сопоставляет HTTP-запрос с методом контроллера.

Пример HTTP-запроса:

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

Пример 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"
}

В предыдущем примере у продукта есть один поставщик. Свойство навигации также может возвращать коллекцию. Следующий код получает продукты для поставщика:

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

В этом случае метод возвращает IQueryable вместо SingleResult<T>

Пример HTTP-запроса:

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

Пример 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
    }
  ]
}

Создание связи между сущностями

OData поддерживает создание или удаление связей между двумя существующими сущностями. В терминологии OData версии 4 связь является "ссылкой". (В OData версии 3 связь называлась ссылкой. Различия в протоколах не имеют значения в этом руководстве.)

Ссылка имеет собственный универсальный код ресурса (URI) в формате /Entity/NavigationProperty/$ref. Например, ниже приведен универсальный код ресурса (URI) для обращения к ссылке между продуктом и его поставщиком:

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

Чтобы добавить связь, клиент отправляет запрос POST или PUT на этот адрес.

  • PUT, если свойство навигации является одной сущностью, например Product.Supplier.
  • POST, если свойство навигации является коллекцией, например Supplier.Products.

Текст запроса содержит универсальный код ресурса (URI) другой сущности в отношении. Ниже приведен пример запроса:

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

В этом примере клиент отправляет запрос PUT в /Products(6)/Supplier/$ref, который является $ref URI для Supplier продукта с идентификатором = 6. Если запрос выполнен успешно, сервер отправляет ответ 204 (без содержимого):

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

Ниже приведен метод контроллера для добавления связи к 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.
}

Параметр navigationProperty указывает, какую связь следует задать. (Если в сущности несколько свойств навигации, можно добавить дополнительные case операторы.)

Параметр link содержит универсальный код ресурса (URI) поставщика. Веб-API автоматически анализирует текст запроса, чтобы получить значение для этого параметра.

Чтобы найти поставщика, нам нужен идентификатор (или ключ), который является частью параметра link . Для этого используйте следующий вспомогательный метод:

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

    }
}

По сути, этот метод использует библиотеку OData для разделения пути URI на сегменты, поиска сегмента, содержащего ключ, и преобразования ключа в правильный тип.

Удаление связи между сущностями

Чтобы удалить связь, клиент отправляет HTTP-запрос DELETE в URI $ref:

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

Ниже приведен метод контроллера для удаления связи между продуктом и поставщиком:

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

В этом случае Product.Supplier является концом "1" отношения "1 ко многим", поэтому вы можете удалить эту связь, задав для значение Product.Suppliernull.

В конце отношения "многие" клиент должен указать, какую связанную сущность следует удалить. Для этого клиент отправляет универсальный код ресурса (URI) связанной сущности в строке запроса запроса. Например, чтобы удалить "Продукт 1" из "Поставщик 1", выполните приведенные ниже действия.

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

Для поддержки этого в веб-API необходимо включить дополнительный параметр в DeleteRef метод . Ниже приведен метод контроллера для удаления продукта из Supplier.Products отношения.

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

Параметр key — это ключ для поставщика, а параметр relatedKey — это ключ, который продукт удаляет из Products связи. Обратите внимание, что веб-API автоматически получает ключ из строки запроса.