Compatibilidad de las acciones de OData en ASP.NET Web API 2

por Mike Wasson

Descargar proyecto completado

En OData, las acciones son una manera de agregar comportamientos del lado servidor que no se definen fácilmente como operaciones CRUD en entidades. Algunos usos de las acciones son:

  • Implementación de transacciones complejas.
  • Manipular varias entidades a la vez.
  • Permitir actualizaciones solo en determinadas propiedades de una entidad.
  • Enviar información al servidor que no está definida en una entidad.

Versiones de software usadas en el tutorial

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

Ejemplo: clasificación de un producto

En este ejemplo, queremos que los usuarios puedan valorar los productos y, a continuación, exponer el promedio de las clasificaciones de cada producto. En la base de datos, se almacenará una lista de clasificaciones con clave en los productos.

Este es el modelo que se puede usar para representar las clasificaciones en Entity Framework:

public class ProductRating
{
    public int ID { get; set; }

    [ForeignKey("Product")]
    public int ProductID { get; set; }
    public virtual Product Product { get; set; }  // Navigation property

    public int Rating { get; set; }
}

Pero no queremos que los clientes PUBLIQUEn un objeto ProductRating en una colección "ratings". De forma intuitiva, la clasificación está asociada a la colección de productos y el cliente solo debe publicar el valor de clasificación.

Por lo tanto, en lugar de usar las operaciones CRUD normales, se define una acción que un cliente puede invocar en un producto. En la terminología de OData, la acción se enlaza a las entidades product.

Las acciones tienen efectos secundarios en el servidor. Por esta razón, se invocan mediante solicitudes HTTP POST. Las acciones pueden tener parámetros y tipos devueltos, que se describen en los metadatos del servicio. El cliente envía los parámetros en el cuerpo de la solicitud y el servidor envía el valor devuelto en el cuerpo de la respuesta. Para invocar la acción "Rate Product", el cliente envía un POST a un URI como el siguiente:

http://localhost/odata/Products(1)/RateProduct

Los datos de la solicitud POST son simplemente la clasificación del producto:

{"Rating":2}

Declare la acción en el Entity Data Model

En la configuración de la API Web, agregue la acción a Entity Data Model (EDM):

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        builder.EntitySet<Supplier>("Suppliers");
        builder.EntitySet<ProductRating>("Ratings");

        // New code: Add an action to the EDM, and define the parameter and return type.
        ActionConfiguration rateProduct = builder.Entity<Product>().Action("RateProduct");
        rateProduct.Parameter<int>("Rating");
        rateProduct.Returns<double>();

        config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
    }
}

Este código define "RateProduct" como una acción que se puede realizar en las entidades del producto. También declara que la acción toma un parámetro int denominado "rating" y devuelve un valor int .

Agregar la acción al controlador

La acción "RateProduct" se enlaza a las entidades product. Para implementar la acción, agregue un método denominado RateProduct al controlador Products:

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

    int rating = (int)parameters["Rating"];

    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }

    product.Ratings.Add(new ProductRating() { Rating = rating });
    db.SaveChanges();

    double average = product.Ratings.Average(x => x.Rating);

    return Ok(average);
}

Observe que el nombre de método coincide con el nombre de la acción en el EDM. El método tiene dos parámetros:

  • key: la clave del producto que se va a evaluar.
  • parámetros: un diccionario de valores de parámetro de acción.

Si utiliza las convenciones de enrutamiento predeterminadas, el parámetro de clave se debe denominar "Key". También es importante incluir el atributo [FromOdataUri] , como se muestra. Este atributo indica a la API Web que use las reglas de sintaxis de OData cuando analiza la clave del URI de solicitud.

Use el Diccionario de parámetros para obtener los parámetros de acción:

if (!ModelState.IsValid)
{
    return BadRequest();
}
int rating = (int)parameters["Rating"];

Si el cliente envía los parámetros de acción en el formato correcto, el valor de ModelState. IsValid es true. En ese caso, puede usar el diccionario ODataActionParameters para obtener los valores de los parámetros. En este ejemplo, la acción de RateProduct toma un parámetro único denominado "rating".

Metadatos de acción

Para ver los metadatos del servicio, envíe una solicitud GET a/OData/$metadata. Esta es la parte de los metadatos que declara la acción RateProduct:

<FunctionImport Name="RateProduct" m:IsAlwaysBindable="true" IsBindable="true" ReturnType="Edm.Double">
  <Parameter Name="bindingParameter" Type="ProductService.Models.Product"/>
  <Parameter Name="Rating" Nullable="false" Type="Edm.Int32"/>
</FunctionImport>

El elemento FunctionImport declara la acción. La mayoría de los campos se explican por sí solos, pero hay dos que merece la pena mencionar:

  • IsBindable significa que la acción se puede invocar en la entidad de destino, al menos parte del tiempo.
  • IsAlwaysBindable significa que la acción siempre se puede invocar en la entidad de destino.

La diferencia es que algunas acciones siempre están disponibles para los clientes, pero otras acciones pueden depender del estado de la entidad. Por ejemplo, supongamos que define una acción de "compra". Solo puede comprar un elemento que esté en existencias. Si el elemento está agotado, un cliente no puede invocar esa acción.

Al definir el EDM, el método de acción crea una acción que se puede enlazar siempre:

builder.Entity<Product>().Action("RateProduct"); // Always bindable

Hablaré sobre acciones no siempre enlazables (también denominadas acciones transitorias ) más adelante en este tema.

Invocar la acción

Ahora veamos cómo un cliente invocaría esta acción. Supongamos que el cliente desea proporcionar una clasificación de 2 al producto con el identificador 4. Este es un mensaje de solicitud de ejemplo, con el formato JSON para el cuerpo de la solicitud:

POST http://localhost/odata/Products(4)/RateProduct HTTP/1.1
Content-Type: application/json
Content-Length: 12

{"Rating":2}

Este es el mensaje de respuesta:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DataServiceVersion: 3.0
Date: Tue, 22 Oct 2013 19:04:00 GMT
Content-Length: 89

{
  "odata.metadata":"http://localhost:21900/odata/$metadata#Edm.Double","value":2.75
}

Enlazar una acción a un conjunto de entidades

En el ejemplo anterior, la acción se enlaza a una sola entidad: el cliente califica un único producto. También puede enlazar una acción a una colección de entidades. Solo tiene que realizar los cambios siguientes:

En el EDM, agregue la acción a la propiedad de colección de la entidad.

var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");

En el método de controlador, omita el parámetro key .

[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
    // ....
}

Ahora el cliente invoca la acción en el conjunto de entidades Products:

http://localhost/odata/Products/RateAllProducts

Acciones con parámetros de colección

Las acciones pueden tener parámetros que toman una colección de valores. En el EDM, use CollectionParameter<t> para declarar el parámetro.

rateAllProducts.CollectionParameter<int>("Ratings");

Esto declara un parámetro denominado "ratings" que toma una colección de valores int . En el método de controlador, todavía se obtiene el valor del parámetro del objeto ODataActionParameters , pero ahora el valor es un valor int<int> :

[HttpPost]
public void RateAllProducts(ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }

    var ratings = parameters["Ratings"] as ICollection<int>; 

    // ...
}

Acciones transitorias

En el ejemplo "RateProduct", los usuarios siempre pueden clasificar un producto, por lo que la acción siempre está disponible. Pero algunas acciones dependen del estado de la entidad. Por ejemplo, en un servicio de alquiler de vídeo, la acción "desproteger" no siempre está disponible. (Depende de si hay una copia de ese vídeo disponible). Este tipo de acción se denomina acción transitoria .

En los metadatos del servicio, una acción transitoria tiene IsAlwaysBindable igual a false. Ese es realmente el valor predeterminado, por lo que los metadatos tendrán el siguiente aspecto:

<FunctionImport Name="CheckOut" IsBindable="true">
    <Parameter Name="bindingParameter" Type="ProductsService.Models.Product" />
</FunctionImport>

Este es el motivo por el que esto es importante: Si una acción es transitoria, el servidor debe indicar al cliente si la acción está disponible. Para ello, incluye un vínculo a la acción en la entidad. Este es un ejemplo de una entidad de película:

{
  "odata.metadata":"http://localhost:17916/odata/$metadata#Movies/@Element",
  "#CheckOut":{ "target":"http://localhost:17916/odata/Movies(1)/CheckOut" },
  "ID":1,"Title":"Sudden Danger 3","Year":2012,"Genre":"Action"
}

La propiedad "#CheckOut" contiene un vínculo a la acción de desprotección. Si la acción no está disponible, el servidor omite el vínculo.

Para declarar una acción transitoria en el EDM, llame al método TransientAction :

var checkoutAction = builder.Entity<Movie>().TransientAction("CheckOut");

Además, debe proporcionar una función que devuelva un vínculo de acción para una entidad determinada. Establezca esta función llamando a HasActionLink. Puede escribir la función como una expresión lambda:

checkoutAction.HasActionLink(ctx =>
{
    var movie = ctx.EntityInstance as Movie;
    if (movie.IsAvailable) {
        return new Uri(ctx.Url.ODataLink(
            new EntitySetPathSegment(ctx.EntitySet), 
            new KeyValuePathSegment(movie.ID.ToString()),
            new ActionPathSegment(checkoutAction.Name)));
    }
    else
    {
        return null;
    }
}, followsConventions: true);

Si la acción está disponible, la expresión lambda devuelve un vínculo a la acción. El serializador de OData incluye este vínculo cuando serializa la entidad. Cuando la acción no está disponible, la función devuelve null.

Recursos adicionales

Ejemplo de acciones de OData