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


Поддержка отношений сущностей в OData версии 3 с помощью веб-API 2

Майк Уассон

Скачать завершенный проект

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

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

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

  • Веб-API 2
  • OData версии 3
  • Entity Framework 6

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

Сначала необходимо добавить новый тип сущности в веб-канал OData. Мы добавим Supplier класс.

using System.ComponentModel.DataAnnotations;

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

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

Далее мы создадим отношение, добавив Supplier свойство в Product класс :

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

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

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

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

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

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

GET /Products(1)/Supplier

Здесь "Поставщик" — это свойство навигации Product для типа . В этом случае Supplier относится к одному элементу, но свойство навигации также может возвращать коллекцию (отношение "один ко многим" или "многие ко многим").

Чтобы поддержать этот запрос, добавьте следующий метод в ProductsController класс :

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

Параметр key является ключом продукта. Метод возвращает связанную сущность — в данном случае экземпляр Supplier . Важное значение имеют имя метода и имя параметра. Как правило, если свойство навигации называется "X", необходимо добавить метод GetX. Метод должен принимать параметр с именем key, соответствующий типу данных родительского ключа.

Также важно включить атрибут [FromOdataUri] в параметр key . Этот атрибут предписывает веб-API использовать правила синтаксиса OData при анализе ключа из URI запроса.

OData поддерживает создание или удаление связей между двумя сущностями. В терминологии OData связь является "ссылкой". Каждая ссылка имеет универсальный код ресурса (URI) с сущностью/$links/сущностью формы. Например, ссылка от продукта к поставщику выглядит следующим образом:

/Products(1)/$links/Supplier

Чтобы создать новую ссылку, клиент отправляет запрос POST на универсальный код ресурса (URI) ссылки. Текст запроса — это универсальный код ресурса (URI) целевой сущности. Например, предположим, что есть поставщик с ключом CTSO. Чтобы создать ссылку из "Product(1)" на "Поставщик('CTSO')", клиент отправляет следующий запрос:

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

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

Чтобы удалить ссылку, клиент отправляет запрос DELETE на универсальный код ресурса (URI) ссылки.

Создание связей

Чтобы клиент мог создавать ссылки поставщика продуктов, добавьте следующий код в ProductsController класс :

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

Этот метод принимает три параметра:

  • key: ключ к родительской сущности (продукт)
  • navigationProperty: имя свойства навигации. В этом примере единственным допустимым свойством навигации является "Поставщик".
  • link: URI OData связанной сущности. Это значение берется из текста запроса. Например, URI ссылки может иметь значение "http://localhost/odata/Suppliers('CTSO'), то есть поставщик с идентификатором = "CTSO".

Метод использует ссылку для поиска поставщика. Если соответствующий поставщик найден, метод задает Product.Supplier свойство и сохраняет результат в базе данных.

Самая сложная часть — анализ URI ссылки. По сути, необходимо смоделировать результат отправки запроса GET в этот универсальный код ресурса (URI). В следующем вспомогательном методе показано, как это сделать. Метод вызывает процесс маршрутизации веб-API и возвращает экземпляр ODataPath , представляющий проанализированный путь OData. Для URI ссылки одним из сегментов должен быть ключ сущности. (В противном случае клиент отправил неправильный URI.)

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

Удаление ссылок

Чтобы удалить ссылку, добавьте следующий код в ProductsController класс :

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

    }
}

В этом примере свойство навигации является одной Supplier сущностью. Если свойство навигации является коллекцией, универсальный код ресурса (URI) для удаления ссылки должен содержать ключ для связанной сущности. Пример:

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

Этот запрос удаляет заказ 1 из клиента 1. В этом случае метод DeleteLink будет иметь следующую сигнатуру:

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

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