Compartir vía


Compatibilidad con las relaciones de entidad en OData v3 con Web API 2

de Mike Wasson

Descargar el proyecto completado

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 ASP.NET Web API. El tutorial se basa en el tutorial Creación de un punto de conexión de OData v3 con Web API 2.

Versiones de software usadas en el tutorial

  • Web API 2
  • OData Versión 3
  • Entity Framework 6

Agregar una entidad de proveedor

En primer lugar, es necesario agregar un nuevo tipo de entidad a nuestra fuente de OData. Agregaremos una clase Supplier.

using System.ComponentModel.DataAnnotations;

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

Esta clase usa una cadena para la clave de entidad. En la práctica, esto podría ser menos común que usar una clave entera. Pero vale la pena ver cómo OData controla otros tipos de claves además de enteros.

A continuación, crearemos una relación agregando una propiedadSupplier a la clase 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; }
}

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

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

En WebApiConfig.cs, agregue una entidad "Proveedores" al modelo EDM:

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

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

GET /Products(1)/Supplier

Aquí "Proveedor" es una propiedad de navegación en el tipo Product. En este caso, Supplier hace referencia a un solo elemento, pero una propiedad de navegación también puede devolver una colección (relación uno a varios o varios a varios).

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

El parámetro clave es la clave del producto. El método devuelve la entidad relacionada; en este caso, una instancia de Supplier. Tanto el nombre del método como el nombre del parámetro son importantes. En general, si la propiedad de navegación se denomina "X", debe agregar un método denominado "GetX". El método debe tomar un parámetro denominado "key" que coincida con el tipo de datos de la clave principal.

También es importante incluir el atributo [FromOdataUri] en el parámetro key. Este atributo indica a Web API que use las reglas de sintaxis de OData cuando analiza la clave del identificador URI de la solicitud.

OData admite la creación o eliminación de relaciones entre dos entidades existentes. En la terminología de OData, la relación es un "vínculo". Cada vínculo tiene un URI con el formulario entidad/$links/entidad. Por ejemplo, el vínculo del producto al proveedor tiene este aspecto:

/Products(1)/$links/Supplier

Para crear un vínculo, el cliente envía una solicitud POST al URI del vínculo. El cuerpo de la solicitud es el URI de la entidad de destino. Por ejemplo, supongamos que hay un proveedor con la clave "CTSO". Para crear un vínculo de "Product(1)" a "Supplier('CTSO')", el cliente envía una solicitud similar a la siguiente:

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

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

Para eliminar un vínculo, el cliente envía una solicitud DELETE al URI del vínculo.

Crear vínculos

Para permitir que un cliente cree vínculos de proveedor de productos, agregue el código siguiente a la clase 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();
    }
}

Este método usa tres parámetros:

  • key: La clave de la entidad primaria (el producto)
  • navigationProperty: La propiedad contiene el nombre de la propiedad de navegación. En este ejemplo, la única propiedad de navegación válida es "Proveedor".
  • link: El URI de OData de la entidad relacionada. Este valor se toma del cuerpo de la solicitud. Por ejemplo, el URI del vínculo podría ser "http://localhost/odata/Suppliers('CTSO'), lo que significa que el proveedor con el identificador = ‘CTSO'.

El método usa el vínculo para buscar el proveedor. Si se encuentra el proveedor coincidente, el método establece la propiedadProduct.Supplier y guarda el resultado en la base de datos.

La parte más difícil es analizar el URI de vínculo. Básicamente, debe simular el resultado de enviar una solicitud GET a ese URI. Para ello, use el siguiente método auxiliar. El método invoca el proceso de enrutamiento de la Web API y devuelve una instancia de ODataPath que representa la ruta de acceso de OData analizada. Para un URI de vínculo, uno de los segmentos debe ser la clave de entidad. (Si no es así, el cliente envió un URI incorrecto).

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

Eliminar vínculos

Para eliminar un vínculo, agregue el código siguiente a la clase 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();

    }
}

En este ejemplo, la propiedad de navegación es una sola entidad, como Supplier. Si la propiedad de navegación es una colección, el URI para eliminar un vínculo debe incluir una clave para la entidad relacionada. Por ejemplo:

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

Esta solicitud quita el pedido 1 del cliente 1. En este caso, el método DeleteLink tendrá la siguiente firma:

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

El parámetro relatedKey proporciona la clave para la entidad relacionada. Por lo tanto, en el método DeleteLink, busque la entidad principal por el parámetro key, busque la entidad relacionada por el parámetro relatedKey y, a continuación, quite la asociación. En función del modelo de datos, es posible que tenga que implementar ambas versiones de DeleteLink. Web API llamará a la versión correcta en función del URI de solicitud.