Acciones y funciones de OData v4 mediante ASP.NET Web API 2.2

por Mike Wasson

En OData, las acciones y las funciones son una manera de agregar comportamientos del lado servidor que no se definen fácilmente como operaciones CRUD en entidades. En este tutorial se muestra cómo agregar acciones y funciones a un punto de conexión de OData v4 mediante Web API 2.2. El tutorial se basa en el tutorial Creación de un punto de conexión de OData v4 mediante ASP.NET Web API 2

Versiones de software usadas en el tutorial

  • Web API 2.2
  • OData v4
  • Visual Studio 2013 (descargue Visual Studio 2017 aquí)
  • .NET 4.5

Versiones del tutorial

Para la versión 3 de OData, consulte Acciones de OData en ASP.NET Web API 2.

La diferencia entre las acciones y las funciones es que las acciones pueden tener efectos secundarios y las funciones no. Tanto las acciones y como las funciones pueden devolver datos. Algunos usos para acciones incluyen:

  • Transacciones complejas.
  • Manipular varias entidades a la vez.
  • Permitir actualizaciones solo a determinadas propiedades de una entidad.
  • Enviar datos que no son una entidad.

Las funciones son útiles para devolver información que no se corresponde directamente con una entidad o colección.

Una acción (o función) puede tener como destino una sola entidad o una colección. En la terminología de OData, este es el enlace. También puede tener acciones o funciones "sin enlazar", a las que se llama como operaciones estáticas en el servicio.

Ejemplo: Agregar una acción

Vamos a definir una acción para evaluar un producto.

En primer lugar, agregue un modelo ProductRating para representar las clasificaciones.

namespace ProductService.Models
{
    public class ProductRating
    {
        public int ID { get; set; }
        public int Rating { get; set; }
        public int ProductID { get; set; }
        public virtual Product Product { get; set; }  
    }
}

Agregue también un DbSet a la clase ProductsContext para que EF cree una tabla Calificación en la base de datos.

public class ProductsContext : DbContext
{
    public ProductsContext() 
            : base("name=ProductsContext")
    {
    }

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

Agregar la acción al EDM

En WebApiConfig.cs, agregue el código siguiente:

ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");

// New code:
builder.Namespace = "ProductService";
builder.EntityType<Product>()
    .Action("Rate")
    .Parameter<int>("Rating");

El método EntityTypeConfiguration.Action agrega una acción al modelo de datos de entidad (EDM). El método Parameter especifica un parámetro de tipo para la acción.

Este código también establece el espacio de nombres para el EDM. El espacio de nombres es importante porque el URI de la acción incluye el nombre de la acción completo:

http://localhost/Products(1)/ProductService.Rate

Nota:

En una configuración típica de IIS, el punto de esta dirección URL hará que IIS devuelva el error 404. Para resolverlo, agregue la sección siguiente al archivo Web.Config:

<system.webServer>
    <handlers>
      <clear/>
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="/*" 
          verb="*" type="System.Web.Handlers.TransferRequestHandler" 
          preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
</system.webServer>

Agregar un método de controlador para la acción

Para habilitar la acción "Rate", agregue el siguiente método a ProductsController:

[HttpPost]
public async Task<IHttpActionResult> Rate([FromODataUri] int key, ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    int rating = (int)parameters["Rating"];
    db.Ratings.Add(new ProductRating
    {
        ProductID = key,
        Rating = rating
    });

    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateException e)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return StatusCode(HttpStatusCode.NoContent);
}

Observe que el nombre del método coincide con el nombre de la acción. El atributo [HttpPost] especifica que el método es un método POST de HTTP.

Para invocar la acción, el cliente envía una solicitud POST de HTTP como la siguiente:

POST http://localhost/Products(1)/ProductService.Rate HTTP/1.1
Content-Type: application/json
Content-Length: 12

{"Rating":5}

La acción "Rate" está enlazada a instancias de Product, por lo que el URI de la acción es el nombre de acción completo anexado al URI de la entidad. (Recuerde que establecimos el espacio de nombres EDM en "ProductService", por lo que el nombre de la acción completo es "ProductService.Rate").

El cuerpo de la solicitud contiene los parámetros de acción como una carga JSON. Web API convierte automáticamente la carga JSON en un objeto ODataActionParameters, que es simplemente un diccionario de valores de parámetros. Use este diccionario para acceder a los parámetros del método de controlador.

Si el cliente envía los parámetros de acción en el formato incorrecto, el valor de ModelState.IsValid es false. Compruebe esta marca en el método del controlador y devuelva un error si IsValid es false.

if (!ModelState.IsValid)
{
    return BadRequest();
}

Ejemplo: Agregar una función

Ahora vamos a agregar una función OData que devuelva el producto más caro. Como antes, el primer paso consiste en agregar la función al EDM. En WebApiConfig.cs, agregue el código siguiente.

ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
builder.EntitySet<Supplier>("Suppliers");

// New code:
builder.Namespace = "ProductService";
builder.EntityType<Product>().Collection
    .Function("MostExpensive")
    .Returns<double>();

En este caso, la función está enlazada a la colección Products, en lugar de a instancias de Product individuales. Los clientes invocan la función mediante el envío de una solicitud GET:

GET http://localhost:38479/Products/ProductService.MostExpensive

Este es el método de controlador para esta función:

public class ProductsController : ODataController
{
    [HttpGet]
    public IHttpActionResult MostExpensive()
    {
        var product = db.Products.Max(x => x.Price);
        return Ok(product);
    }

    // Other controller methods not shown.
}

Observe que el nombre del método coincide con el nombre de la función. El atributo [HttpGet] especifica que el método es un método GET de HTTP.

Esta es la respuesta HTTP:

HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0
Date: Sat, 28 Jun 2014 00:44:07 GMT
Content-Length: 85

{
  "@odata.context":"http://localhost:38479/$metadata#Edm.Decimal","value":50.00
}

Ejemplo: Agregar una función no enlazada

El ejemplo anterior era una función enlazada a una colección. En este ejemplo siguiente, crearemos una función no enlazada. Las funciones no enlazadas se llaman como operaciones estáticas en el servicio. La función de este ejemplo devolverá el impuesto sobre las ventas de un código postal determinado.

En el archivo WebApiConfig, agregue la función al EDM:

ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");

// New code:
builder.Function("GetSalesTaxRate")
    .Returns<double>()
    .Parameter<int>("PostalCode");

Observe que estamos llamando a Function directamente en ODataModelBuilder en lugar del tipo de entidad o colección. Esto indica al generador de modelos que la función no está enlazada.

Este es el método de controlador que implementa la función:

[HttpGet]
[ODataRoute("GetSalesTaxRate(PostalCode={postalCode})")]
public IHttpActionResult GetSalesTaxRate([FromODataUri] int postalCode)
{
    double rate = 5.6;  // Use a fake number for the sample.
    return Ok(rate);
}

No importa en qué controlador de Web API coloque este método. Puede colocarlo en ProductsController o definir un controlador independiente. El atributo [ODataRoute] define la plantilla de URI para la función.

Este es un ejemplo de solicitud de cliente:

GET http://localhost:38479/GetSalesTaxRate(PostalCode=10) HTTP/1.1

La respuesta HTTP:

HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0
Date: Sat, 28 Jun 2014 01:05:32 GMT
Content-Length: 82

{
  "@odata.context":"http://localhost:38479/$metadata#Edm.Double","value":5.6
}