Поддержка отношений сущностей в OData v3 с веб-API 2Supporting Entity Relations in OData v3 with Web API 2

по Майк Уоссонby Mike Wasson

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

Большинство наборов данных определяют связи между сущностями: у клиентов есть заказы. книги имеют авторов; продукты имеют поставщики.Most data sets define relations between entities: Customers have orders; books have authors; products have suppliers. С помощью OData клиенты могут перемещаться по связям сущностей.Using OData, clients can navigate over entity relations. С учетом продукта можно найти поставщика.Given a product, you can find the supplier. Также можно создавать и удалять связи.You can also create or remove relationships. Например, можно задать поставщика для продукта.For example, you can set the supplier for a product.

В этом руководстве показано, как поддерживать эти операции в веб-API ASP.NET.This tutorial shows how to support these operations in ASP.NET Web API. Руководство строится на руководстве по созданию конечной точки OData v3 с веб-API 2.The tutorial builds on the tutorial Creating an OData v3 Endpoint with Web API 2.

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

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

Добавление сущности поставщикаAdd a Supplier Entity

Сначала необходимо добавить новый тип сущности в наш веб-канал OData.First we need to add a new entity type to our OData feed. Мы добавим класс Supplier.We'll add a Supplier class.

using System.ComponentModel.DataAnnotations;

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

Этот класс использует строку для ключа сущности.This class uses a string for the entity key. На практике это может быть менее распространенным, чем использование целочисленного ключа.In practice, that might be less common than using an integer key. Но стоит рассмотреть, как OData обрабатывает другие типы ключей помимо целых чисел.But it's worth seeing how OData handles other key types besides integers.

Далее мы создадим отношение, добавив свойство Supplier к классу Product:Next, we'll create a relation by adding a Supplier property to the Product class:

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.Add a new DbSet to the ProductServiceContext class, so that Entity Framework will include the Supplier table in the database.

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:In WebApiConfig.cs, add a "Suppliers" entity to the EDM model:

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

Чтобы получить поставщик для продукта, клиент отправляет запрос GET:To get the supplier for a product, the client sends a GET request:

GET /Products(1)/Supplier

Здесь "поставщик" — это свойство навигации типа Product.Here "Supplier" is a navigation property on the Product type. В этом случае Supplier ссылается на один элемент, но свойство навигации также может возвращать коллекцию (связь «один ко многим» или «многие ко многим»).In this case, Supplier refers to a single item, but a navigation property can also return a collection (one-to-many or many-to-many relation).

Для поддержки этого запроса добавьте в класс ProductsController следующий метод:To support this request, add the following method to the ProductsController class:

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

Ключевым параметром является ключ продукта.The key parameter is the key of the product. Метод возвращает связанную сущность—в данном случае — экземпляр Supplier.The method returns the related entity—in this case, a Supplier instance. Имя метода и имя параметра являются важными.The method name and parameter name are both important. В общем случае, если свойство навигации имеет имя «X», необходимо добавить метод с именем «Жеткс».In general, if the navigation property is named "X", you need to add a method named "GetX". Метод должен принимать параметр с именемKey, соответствующий типу данных родительского ключа.The method must take a parameter named "key" that matches the data type of the parent's key.

Также важно включить атрибут [фромодатаури] в параметр Key .It is also important to include the [FromOdataUri] attribute in the key parameter. Этот атрибут указывает, что веб-API будет использовать правила синтаксиса OData при анализе ключа из URI запроса.This attribute tells Web API to use OData syntax rules when it parses the key from the request URI.

OData поддерживает создание или удаление связей между двумя сущностями.OData supports creating or removing relationships between two entities. В терминологии OData связь является "связью".In OData terminology, the relationship is a "link." Каждая ссылка имеет универсальный код ресурса (URI) с формой Entity/$Links илиEntity.Each link has a URI with the form entity/$links/entity. Например, ссылка от Product к поставщику выглядит следующим образом:For example, the link from product to supplier looks like this:

/Products(1)/$links/Supplier

Чтобы создать новую ссылку, клиент отправляет запрос POST в URI ссылки.To create a new link, the client sends a POST request to the link URI. Тело запроса — это URI целевой сущности.The body of the request is the URI of the target entity. Например, предположим, что имеется поставщик с ключом «КТСО».For example, suppose there is a supplier with the key "CTSO". Чтобы создать ссылку с "Product (1)" на "поставщик (" КТСО ")", клиент отправляет запрос, подобный следующему:To create a link from "Product(1)" to "Supplier('CTSO')", the client sends a request like the following:

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

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

Чтобы удалить ссылку, клиент отправляет запрос на удаление в URI ссылки.To delete a link, the client sends a DELETE request to the link URI.

Создание ссылокCreating Links

Чтобы разрешить клиенту создавать связи продуктов и поставщиков, добавьте следующий код в класс ProductsController:To enable a client to create product-supplier links, add the following code to the ProductsController class:

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

Этот метод принимает три параметра:This method takes three parameters:

  • ключ: ключ к родительской сущности (продукт).key: The key to the parent entity (the product)
  • navigationProperty: имя свойства навигации.navigationProperty: The name of the navigation property. В этом примере единственным допустимым свойством навигации является «поставщик».In this example, the only valid navigation property is "Supplier".
  • ссылка: URI OData связанной сущности.link: The OData URI of the related entity. Это значение берется из текста запроса.This value is taken from the request body. Например, URI ссылки может быть "http://localhost/odata/Suppliers('CTSO'), то есть поставщик с ИДЕНТИФИКАТОРом" КТСО ".For example, the link URI might be "http://localhost/odata/Suppliers('CTSO'), meaning the supplier with ID = ‘CTSO'.

Метод использует ссылку для поиска поставщика.The method uses the link to look up the supplier. Если найден соответствующий поставщик, метод задает свойство Product.Supplier и сохраняет результат в базе данных.If the matching supplier is found, the method sets the Product.Supplier property and saves the result to the database.

Самая сложная часть — это анализ универсального кода ресурса (URI) ссылки.The hardest part is parsing the link URI. По сути, необходимо имитировать результат отправки запроса GET на этот универсальный код ресурса (URI).Basically, you need to simulate the result of sending a GET request to that URI. В следующем вспомогательном методе показано, как это сделать.The following helper method shows how to do this. Метод вызывает процесс маршрутизации веб-API и возвращает экземпляр ODataPath , представляющий проанализированный путь OData.The method invokes the Web API routing process and gets back an ODataPath instance that represents the parsed OData path. Для URI ссылки один из сегментов должен быть ключом сущности.For a link URI, one of the segments should be the entity key. (Если нет, клиент отправил некорректный URI.)(If not, the client sent a bad 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;
}

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

Чтобы удалить ссылку, добавьте следующий код в класс ProductsController:To delete a link, add the following code to the ProductsController class:

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.In this example, the navigation property is a single Supplier entity. Если свойство навигации является коллекцией, URI для удаления ссылки должен включать ключ для связанной сущности.If the navigation property is a collection, the URI to delete a link must include a key for the related entity. Пример:For example:

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

Этот запрос удаляет заказ 1 из клиента 1.This request removes order 1 from customer 1. В этом случае метод Делетелинк будет иметь следующую сигнатуру:In this case, the DeleteLink method will have the following signature:

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

Параметр релатедкэй предоставляет ключ для связанной сущности.The relatedKey parameter gives the key for the related entity. Поэтому в методе DeleteLink найдите основную сущность по ключевому параметру, найдите связанную сущность с помощью параметра релатедкэй , а затем удалите связь.So in your DeleteLink method, look up the primary entity by the key parameter, find the related entity by the relatedKey parameter, and then remove the association. В зависимости от модели данных может потребоваться реализовать обе версии DeleteLink.Depending on your data model, you might need to implement both versions of DeleteLink. Веб-API будет вызывать правильную версию на основе URI запроса.Web API will call the correct version based on the request URI.