Compartir a través de


Relaciones de entidades en OData v4 con ASP.NET Web API 2.2

de Mike Wasson

La mayoría de los conjuntos de datos definen las relaciones entre entidades: los clientes tienen pedidos; los libros tienen autores; los productos tienen proveedores. Con OData, los clientes pueden navegar por las relaciones de entidad. Dado un producto, puede encontrar el proveedor. También puede crear o quitar relaciones. Por ejemplo, puede establecer el proveedor de un producto.

En este tutorial, se muestra cómo admitir estas operaciones en OData v4 mediante ASP.NET Web API. El tutorial se basa en el tutorial Creación de un punto de conexión de OData v4 con ASP.NET Web API 2.

Versiones de software usadas en el tutorial

  • Web API 2.1
  • OData v4
  • Visual Studio 2017 (descargue Visual Studio 2017 aquí)
  • Entity Framework 6
  • .NET 4.5

Versiones del tutorial

Para la versión 3 de OData, consulte Compatibilidad con las relaciones de entidades en OData v3.

Agregar una entidad de proveedor

En primer lugar, necesitamos una entidad relacionada. Agregue una clase denominada Supplier a la carpeta 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; }
    }
}

Agregue una propiedad de navegación a la clase 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; }
    }
}

Agregue un nuevo DbSet a la clase ProductsContext para que Entity Framework incluya la tabla Supplier en la base de datos.

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

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

En WebApiConfig.cs, agregue un conjunto de entidades "Suppliers" establecido en el modelo de datos de entidades:

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

Agregar un controlador de proveedores

Agregue una clase SuppliersController a la carpeta 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);
        }
    }
}

No mostraré cómo agregar operaciones CRUD para este controlador. Los pasos son los mismos que para el controlador Products (consulte Creación de un punto de conexión de OData v4).

Para obtener el proveedor de un producto, el cliente envía una solicitud GET:

GET /Products(1)/Supplier

Para admitir esta solicitud, agregue el método siguiente a la clase 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.
}

Este método usa una convención de nomenclatura predeterminada

  • Nombre del método: GetX, donde X es la propiedad de navegación.
  • Nombre del parámetro: key

Si sigue esta convención de nomenclatura, Web API asigna automáticamente la solicitud HTTP al método de controlador.

Solicitud HTTP de ejemplo:

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

Respuesta HTTP de ejemplo:

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

En el ejemplo anterior, un producto tiene un proveedor. Una propiedad de navegación también puede devolver una colección. El código siguiente obtiene los productos de un proveedor:

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

En este caso, el método devuelve un IQueryable en lugar de un SingleResult<T>

Solicitud HTTP de ejemplo:

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

Respuesta HTTP de ejemplo:

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

Crear una relación entre entidades

OData admite la creación o eliminación de relaciones entre dos entidades existentes. En la terminología de OData v4, la relación es una "referencia". (En OData v3, la relación se denominaba link. Las diferencias de protocolo no importan para este tutorial).

Una referencia tiene su propio identificador URI, con el formato /Entity/NavigationProperty/$ref. Por ejemplo, este es el identificador URI para abordar la referencia entre un producto y su proveedor:

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

Para agregar una relación, el cliente envía una solicitud POST o PUT a esta dirección.

  • PUT si la propiedad de navegación es una sola entidad, como Product.Supplier.
  • POST si la propiedad de navegación es una colección, como Supplier.Products.

El cuerpo de la solicitud contiene el identificador URI de la otra entidad en la relación. Esta es una solicitud de ejemplo:

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

En este ejemplo, el cliente envía una solicitud PUT a /Products(6)/Supplier/$ref, que es el identificador URI $ref del producto Supplier con id. = 6. Si la solicitud se realiza correctamente, el servidor envía una respuesta 204 (sin contenido):

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

Este es el método de controlador para agregar una relación a: 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.
}

El parámetro navigationProperty especifica qué relación se va a establecer. (Si hay más de una propiedad de navegación en la entidad, puede agregar más instrucciones case).

El parámetro link contiene el identificador URI del proveedor. Web API analiza automáticamente el cuerpo de la solicitud para obtener el valor de este parámetro.

Para buscar el proveedor, necesitamos el identificador (o clave), que forma parte del parámetro link. Para ello, use el siguiente 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;
        }

    }
}

Básicamente, este método usa la biblioteca OData para dividir la ruta de acceso del identificador URI en segmentos, buscar el segmento que contiene la clave y convertir la clave en el tipo correcto.

Eliminar una relación entre entidades

Para eliminar una relación, el cliente envía una solicitud HTTP DELETE al identificador URI de $ref:

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

Este es el método de controlador para eliminar la relación entre un Product y un Supplier:

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

En este caso, Product.Supplier es el extremo "1" de una relación de 1 a varios, por lo que puede quitar la relación simplemente estableciendo Product.Supplier como null.

En el extremo "many" de una relación, el cliente debe especificar la entidad relacionada que se va a quitar. Para ello, el cliente envía el identificador URI de la entidad relacionada en la cadena de consulta de la solicitud. Por ejemplo, para quitar "Product 1" de "Supplier 1":

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

Para admitir esto en Web API, es necesario incluir un parámetro adicional en el método DeleteRef. Este es el método de controlador para quitar un producto de la relación 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.
}

El parámetro key es la clave del proveedor y el parámetro relatedKey es la clave para que el producto se quite de la relación Products. Tenga en cuenta que Web API obtiene automáticamente la clave de la cadena de consulta.