Relações de entidade de suporte no OData v3 com a API Web 2

por Mike Wasson

Baixar Projeto Concluído

A maioria dos conjuntos de dados define relações entre entidades: os clientes têm pedidos; livros têm autores; os produtos têm fornecedores. Usando o OData, os clientes podem navegar por relações de entidade. Dado um produto, você pode encontrar o fornecedor. Você também pode criar ou remover relações. Por exemplo, você pode definir o fornecedor para um produto.

Este tutorial mostra como dar suporte a essas operações no ASP.NET Web API. O tutorial se baseia no tutorial Criando um ponto de extremidade OData v3 com a API Web 2.

Versões de software usadas no tutorial

  • API Web 2
  • OData Versão 3
  • Entity Framework 6

Adicionar uma entidade de fornecedor

Primeiro, precisamos adicionar um novo tipo de entidade ao nosso feed OData. Adicionaremos uma Supplier classe.

using System.ComponentModel.DataAnnotations;

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

Essa classe usa uma cadeia de caracteres para a chave de entidade. Na prática, isso pode ser menos comum do que usar uma chave de inteiro. Mas vale a pena ver como o OData lida com outros tipos de chave além de inteiros.

Em seguida, criaremos uma relação adicionando uma Supplier propriedade à Product classe :

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

Adicione um novo DbSet à classe , para que o ProductServiceContext Entity Framework inclua a Supplier tabela no banco de dados.

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

Em WebApiConfig.cs, adicione uma entidade "Fornecedores" ao modelo EDM:

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

Para obter o fornecedor de um produto, o cliente envia uma solicitação GET:

GET /Products(1)/Supplier

Aqui "Fornecedor" é uma propriedade de navegação no Product tipo . Nesse caso, Supplier refere-se a um único item, mas uma propriedade de navegação também pode retornar uma coleção (relação um para muitos ou muitos para muitos).

Para dar suporte a essa solicitação, adicione o seguinte método à ProductsController classe :

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

O parâmetro de chave é a chave do produto. O método retorna a entidade relacionada , nesse caso, uma Supplier instância. O nome do método e o nome do parâmetro são importantes. Em geral, se a propriedade de navegação for chamada "X", você precisará adicionar um método chamado "GetX". O método deve usar um parâmetro chamado "key" que corresponda ao tipo de dados da chave pai.

Também é importante incluir o atributo [FromOdataUri] no parâmetro de chave . Esse atributo informa à API Web para usar regras de sintaxe OData quando analisa a chave do URI de solicitação.

O OData dá suporte à criação ou remoção de relações entre duas entidades. Na terminologia OData, a relação é um "link". Cada link tem um URI com a entidade/entidade de formulário/$links/entidade. Por exemplo, o link de produto para fornecedor tem esta aparência:

/Products(1)/$links/Supplier

Para criar um novo link, o cliente envia uma solicitação POST para o URI do link. O corpo da solicitação é o URI da entidade de destino. Por exemplo, suponha que haja um fornecedor com a chave "CTSO". Para criar um link de "Product(1)" para "Supplier('CTSO')", o cliente envia uma solicitação como a seguinte:

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

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

Para excluir um link, o cliente envia uma solicitação DELETE para o URI do link.

Criar Links

Para permitir que um cliente crie links de fornecedor de produtos, adicione o seguinte código à ProductsController classe :

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

Esse método usa três parâmetros:

  • key: a chave para a entidade pai (o produto)
  • navigationProperty: o nome da propriedade de navegação. Neste exemplo, a única propriedade de navegação válida é "Supplier".
  • link: o URI OData da entidade relacionada. Esse valor é obtido do corpo da solicitação. Por exemplo, o URI do link pode ser "http://localhost/odata/Suppliers('CTSO'), o que significa que o fornecedor com ID = 'CTSO'.

O método usa o link para procurar o fornecedor. Se o fornecedor correspondente for encontrado, o método definirá a Product.Supplier propriedade e salvará o resultado no banco de dados.

A parte mais difícil é analisar o URI do link. Basicamente, você precisa simular o resultado do envio de uma solicitação GET para esse URI. O método auxiliar a seguir mostra como fazer isso. O método invoca o processo de roteamento da API Web e obtém de volta uma instância do ODataPath que representa o caminho OData analisado. Para um URI de link, um dos segmentos deve ser a chave de entidade. (Caso contrário, o cliente enviou um URI inválido.)

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

Excluindo links

Para excluir um link, adicione o seguinte código à ProductsController classe :

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

    }
}

Neste exemplo, a propriedade de navegação é uma única Supplier entidade. Se a propriedade de navegação for uma coleção, o URI para excluir um link deverá incluir uma chave para a entidade relacionada. Por exemplo:

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

Essa solicitação remove o pedido 1 do cliente 1. Nesse caso, o método DeleteLink terá a seguinte assinatura:

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

O parâmetro relatedKey fornece a chave para a entidade relacionada. Portanto, em seu DeleteLink método, procure a entidade primária pelo parâmetro de chave , localize a entidade relacionada pelo parâmetro relatedKey e remova a associação. Dependendo do modelo de dados, talvez seja necessário implementar ambas as versões do DeleteLink. A API Web chamará a versão correta com base no URI da solicitação.