Partilhar via


Suporte a ações OData no ASP.NET Web API 2

por Mike Wasson

Baixar Projeto Concluído

No OData, as ações são uma maneira de adicionar comportamentos do lado do servidor que não são facilmente definidos como operações CRUD em entidades. Alguns usos para ações incluem:

  • Implementando transações complexas.
  • Manipulando várias entidades ao mesmo tempo.
  • Permitir atualizações somente para determinadas propriedades de uma entidade.
  • Enviar informações para o servidor que não está definido em uma entidade.

Versões de software usadas no tutorial

  • API Web 2
  • OData Versão 3
  • Entity Framework 6

Exemplo: Classificação de um produto

Neste exemplo, queremos permitir que os usuários classifiquem os produtos e exponham as classificações médias de cada produto. No banco de dados, armazenaremos uma lista de classificações, com chave para produtos.

Este é o modelo que podemos usar para representar as classificações no 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; }
}

Mas não queremos que os clientes POSTEm um ProductRating objeto em uma coleção "Ratings". Intuitivamente, a classificação é associada à coleção Products e o cliente só deve precisar postar o valor de classificação.

Portanto, em vez de usar as operações CRUD normais, definimos uma ação que um cliente pode invocar em um Produto. Na terminologia do OData, a ação está associada a entidades product.

As ações têm efeitos colaterais no servidor. Por esse motivo, eles são invocados usando solicitações HTTP POST. As ações podem ter parâmetros e tipos de retorno, que são descritos nos metadados do serviço. O cliente envia os parâmetros no corpo da solicitação e o servidor envia o valor retornado no corpo da resposta. Para invocar a ação "Produto de Taxa", o cliente envia um POST para um URI como o seguinte:

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

Os dados na solicitação POST são simplesmente a classificação do produto:

{"Rating":2}

Declarar a ação no modelo de dados de entidade

Na configuração da API Web, adicione a ação ao EDM (modelo de dados de entidade):

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

Esse código define "RateProduct" como uma ação que pode ser executada em entidades product. Ele também declara que a ação usa um parâmetro int chamado "Rating" e retorna um valor int .

Adicionar a ação ao controlador

A ação "RateProduct" está associada a Entidades de produto. Para implementar a ação, adicione um método chamado RateProduct ao controlador De produtos:

[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 o nome do método corresponde ao nome da ação no EDM. O método tem dois parâmetros:

  • key: a chave para o produto classificar.
  • parâmetros: um dicionário de valores de parâmetro de ação.

Se você estiver usando as convenções de roteamento padrão, o parâmetro de chave deverá ser nomeado como "chave". Também é importante incluir o atributo [FromOdataUri] , conforme mostrado. Esse atributo informa à API Web para usar regras de sintaxe OData ao analisar a chave do URI de solicitação.

Use o dicionário de parâmetros para obter os parâmetros de ação:

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

Se o cliente enviar os parâmetros de ação no formato correto, o valor de ModelState.IsValid será true. Nesse caso, você pode usar o dicionário ODataActionParameters para obter os valores de parâmetro. Neste exemplo, a ação RateProduct usa um único parâmetro chamado "Rating".

Metadados de ação

Para exibir os metadados de serviço, envie uma solicitação GET para /odata/$metadata. Aqui está a parte dos metadados que declara a ação 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>

O elemento FunctionImport declara a ação. A maioria dos campos é autoexplicativa, mas dois valem a pena observar:

  • IsBindable significa que a ação pode ser invocada na entidade de destino, pelo menos em parte do tempo.
  • IsAlwaysBindable significa que a ação sempre pode ser invocada na entidade de destino.

A diferença é que algumas ações estão sempre disponíveis para os clientes, mas outras ações podem depender do estado da entidade. Por exemplo, suponha que você defina uma ação de "Compra". Você só pode comprar um item que esteja em estoque. Se o item estiver sem estoque, um cliente não poderá invocar essa ação.

Quando você define o EDM, o método Action cria uma ação sempre associável:

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

Falarei sobre ações não sempre associáveis (também chamadas de ações transitórias ) mais adiante neste tópico.

Invocando a ação

Agora vamos ver como um cliente invocaria essa ação. Suponha que o cliente queira dar uma classificação de 2 para o produto com ID = 4. Aqui está um exemplo de mensagem de solicitação, usando o formato JSON para o corpo da solicitação:

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

{"Rating":2}

Esta é a mensagem de resposta:

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
}

Associando uma ação a um conjunto de entidades

No exemplo anterior, a ação está associada a uma única entidade: o cliente classifica um único produto. Você também pode associar uma ação a uma coleção de entidades. Basta fazer as seguintes alterações:

No EDM, adicione a ação à propriedade Collection da entidade.

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

No método do controlador, omita o parâmetro key .

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

Agora, o cliente invoca a ação no conjunto de entidades Produtos:

http://localhost/odata/Products/RateAllProducts

Ações com parâmetros de coleção

As ações podem ter parâmetros que levam uma coleção de valores. No EDM, use CollectionParameter<T> para declarar o parâmetro .

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

Isso declara um parâmetro chamado "Ratings" que usa uma coleção de valores int . No método do controlador, você ainda obtém o valor do parâmetro do objeto ODataActionParameters, mas agora o valor é um valor int> ICollection<:

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

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

    // ...
}

Ações transitórias

No exemplo "RateProduct", os usuários sempre podem classificar um produto, portanto, a ação está sempre disponível. Mas algumas ações dependem do estado da entidade. Por exemplo, em um serviço de aluguel de vídeo, a ação "CheckOut" nem sempre está disponível. (Depende se uma cópia desse vídeo está disponível.) Esse tipo de ação é chamado de ação transitória .

Nos metadados de serviço, uma ação transitória tem IsAlwaysBindable igual a false. Na verdade, esse é o valor padrão, portanto, os metadados terão esta aparência:

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

É por isso que isso importa: se uma ação for transitória, o servidor precisará informar ao cliente quando a ação está disponível. Ele faz isso incluindo um link para a ação na entidade. Aqui está um exemplo para uma entidade Movie:

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

A propriedade "#CheckOut" contém um link para a ação CheckOut. Se a ação não estiver disponível, o servidor omitirá o link.

Para declarar uma ação transitória no EDM, chame o método TransientAction :

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

Além disso, você deve fornecer uma função que retorna um link de ação para uma determinada entidade. Defina essa função chamando HasActionLink. Você pode escrever a função como uma expressão 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);

Se a ação estiver disponível, a expressão lambda retornará um link para a ação. O serializador OData inclui esse link quando serializa a entidade. Quando a ação não está disponível, a função retorna null.

Recursos adicionais