Partilhar via


Relações de entidade no OData v4 usando ASP.NET Web API 2.2

por Mike Wasson

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 OData v4 usando ASP.NET Web API. O tutorial se baseia no tutorial Criar um ponto de extremidade OData v4 usando ASP.NET Web API 2.

Versões de software usadas no tutorial

  • API Web 2.1
  • OData v4
  • Visual Studio 2017 (baixe o Visual Studio 2017 aqui)
  • Entity Framework 6
  • .NET 4.5

Versões do tutorial

Para o OData Versão 3, consulte Supporting Entity Relations in OData v3.

Adicionar uma entidade de fornecedor

Observação

O tutorial se baseia no tutorial Criar um ponto de extremidade OData v4 usando ASP.NET Web API 2.

Primeiro, precisamos de uma entidade relacionada. Adicione uma classe chamada Supplier na pasta Modelos.

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

Adicione uma propriedade de navegação à Product classe :

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

Adicione um novo DbSet à classe , de modo que o ProductsContext Entity Framework inclua a tabela Fornecedor no banco de dados.

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

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

Em WebApiConfig.cs, adicione uma entidade "Fornecedores" definida ao modelo de dados da entidade:

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

Adicionar um controlador de fornecedores

Adicione uma SuppliersController classe à pasta Controladores.

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

Não mostrarei como adicionar operações CRUD para esse controlador. As etapas são as mesmas do controlador De produtos (consulte Criar um ponto de extremidade OData v4).

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

GET /Products(1)/Supplier

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

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

Esse método usa uma convenção de nomenclatura padrão

  • Nome do método: GetX, em que X é a propriedade de navegação.
  • Nome do parâmetro: chave

Se você seguir essa convenção de nomenclatura, a API Web mapeará automaticamente a solicitação HTTP para o método do controlador.

Exemplo de solicitação HTTP:

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

Exemplo de resposta 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"
}

No exemplo anterior, um produto tem um fornecedor. Uma propriedade de navegação também pode retornar uma coleção. O código a seguir obtém os produtos de um fornecedor:

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

Nesse caso, o método retorna um IQueryable em vez de um SingleResult<T>

Exemplo de solicitação HTTP:

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

Exemplo de resposta 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
    }
  ]
}

Criando uma relação entre entidades

O OData dá suporte à criação ou remoção de relações entre duas entidades existentes. Na terminologia OData v4, a relação é uma "referência". (No OData v3, a relação era chamada de link. As diferenças de protocolo não importam para este tutorial.)

Uma referência tem seu próprio URI, com o formulário /Entity/NavigationProperty/$ref. Por exemplo, aqui está o URI para abordar a referência entre um produto e seu fornecedor:

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

Para adicionar uma relação, o cliente envia uma solicitação POST ou PUT para esse endereço.

  • PUT se a propriedade de navegação for uma única entidade, como Product.Supplier.
  • POST se a propriedade de navegação for uma coleção, como Supplier.Products.

O corpo da solicitação contém o URI da outra entidade na relação. Veja um exemplo de solicitação:

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

Neste exemplo, o cliente envia uma solicitação PUT para /Products(6)/Supplier/$ref, que é o URI $ref do Supplier produto com ID = 6. Se a solicitação for bem-sucedida, o servidor enviará uma resposta 204 (Sem Conteúdo):

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

Aqui está o método do controlador para adicionar uma relação a um 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.
}

O parâmetro navigationProperty especifica qual relação definir. (Se houver mais de uma propriedade de navegação na entidade, você poderá adicionar mais case instruções.)

O parâmetro link contém o URI do fornecedor. A API Web analisa automaticamente o corpo da solicitação para obter o valor desse parâmetro.

Para pesquisar o fornecedor, precisamos da ID (ou chave), que faz parte do parâmetro de link . Para fazer isso, use o seguinte método auxiliar:

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

    }
}

Basicamente, esse método usa a biblioteca OData para dividir o caminho do URI em segmentos, localizar o segmento que contém a chave e converter a chave no tipo correto.

Excluindo uma relação entre entidades

Para excluir uma relação, o cliente envia uma solicitação HTTP DELETE para o URI $ref:

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

Aqui está o método do controlador para excluir a relação entre um Produto e um Fornecedor:

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

Nesse caso, Product.Supplier é o final "1" de uma relação de 1 para muitos, para que você possa remover a relação apenas definindo Product.Supplier como null.

No final "muitos" de uma relação, o cliente deve especificar qual entidade relacionada remover. Para fazer isso, o cliente envia o URI da entidade relacionada na cadeia de caracteres de consulta da solicitação. Por exemplo, para remover "Produto 1" de "Fornecedor 1":

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

Para dar suporte a isso na API Web, precisamos incluir um parâmetro extra no DeleteRef método . Aqui está o método do controlador para remover um produto da Supplier.Products relação.

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

O parâmetro de chave é a chave para o fornecedor e o parâmetro relatedKey é a chave para o produto remover da Products relação. Observe que a API Web obtém automaticamente a chave da cadeia de caracteres de consulta.