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

por Mike Wasson

Esta visão geral com exemplos de código demonstra o suporte às Opções de Consulta OData no ASP.NET Web API 2 para ASP.NET 4.x.

OData define parâmetros que podem ser usados para modificar uma consulta OData. O cliente envia esses parâmetros na cadeia de caracteres de consulta do URI de solicitação. Por exemplo, para classificar os resultados, um cliente usa o parâmetro $orderby:

http://localhost/Products?$orderby=Name

A especificação OData chama essas opções de consulta de parâmetros. Você pode habilitar opções de consulta OData para qualquer controlador de API Web em seu projeto — o controlador não precisa ser um ponto de extremidade OData. Isso oferece uma maneira conveniente de adicionar recursos como filtragem e classificação a qualquer aplicativo de API Web.

Antes de habilitar as opções de consulta, leia o tópico Diretrizes de segurança do OData.

Habilitando opções de consulta OData

A API Web dá suporte às seguintes opções de consulta OData:

Opção Descrição
$expand Expande entidades relacionadas embutidas.
$filter Filtra os resultados, com base em uma condição booliana.
$inlinecount Informa ao servidor para incluir a contagem total de entidades correspondentes na resposta. (Útil para paginação do lado do servidor.)
$orderby Classifica os resultados.
$select Seleciona quais propriedades incluir na resposta.
$skip Ignora os primeiros n resultados.
$top Retorna apenas os primeiros n resultados.

Para usar as opções de consulta OData, você deve habilitá-las explicitamente. Você pode habilitá-los globalmente para todo o aplicativo ou habilitá-los para controladores específicos ou ações específicas.

Para habilitar as opções de consulta OData globalmente, chame EnableQuerySupport na classe HttpConfiguration na inicialização:

public static void Register(HttpConfiguration config)
{
    // ...

    config.EnableQuerySupport();

    // ...
}

O método EnableQuerySupport habilita as opções de consulta globalmente para qualquer ação do controlador que retorna um tipo IQueryable . Se você não quiser que as opções de consulta sejam habilitadas para todo o aplicativo, habilite-as para ações específicas do controlador adicionando o atributo [Queryable] ao método de ação.

public class ProductsController : ApiController
{
    [Queryable]
    IQueryable<Product> Get() {}
}

Consultas de Exemplo

Esta seção mostra os tipos de consultas possíveis usando as opções de consulta OData. Para obter detalhes específicos sobre as opções de consulta, consulte a documentação do OData em www.odata.org.

Para obter informações sobre $expand e $select, consulte Usando $select, $expand e $value no ASP.NET Web API OData.

Paginação controlada pelo cliente

Para grandes conjuntos de entidades, talvez o cliente queira limitar o número de resultados. Por exemplo, um cliente pode mostrar 10 entradas por vez, com links "próximo" para obter a próxima página de resultados. Para fazer isso, o cliente usa as opções $top e $skip.

http://localhost/Products?$top=10&$skip=20

A opção $top fornece o número máximo de entradas a serem retornadas e a opção $skip fornece o número de entradas a serem ignoradas. O exemplo anterior busca as entradas de 21 a 30.

Filtragem

A opção $filter permite que um cliente filtre os resultados aplicando uma expressão booliana. As expressões de filtro são bastante poderosas; eles incluem operadores lógicos e aritméticos, funções de cadeia de caracteres e funções de data.

Retorne todos os produtos com categoria igual a "Brinquedos". http://localhost/Products?$filter=Category eq 'Toys'
Retornar todos os produtos com preço inferior a 10. http://localhost/Products?$filter=Price lt 10
Operadores lógicos: retornam todos os produtos em que o preço >= 5 e o preço <= 15. http://localhost/Products?$filter=Price ge 5 e Price le 15
Funções de cadeia de caracteres: retorna todos os produtos com "zz" no nome. http://localhost/Products?$filter=substringof('zz',Name)
Funções de data: retornar todos os produtos com ReleaseDate após 2005. http://localhost/Products?$filter=year(ReleaseDate) gt 2005

Classificação

Para classificar os resultados, use o filtro $orderby.

Classificar por preço. http://localhost/Products?$orderby=Price
Classificar por preço em ordem decrescente (mais alto para menor). http://localhost/Products?$orderby=Price desc
Classifique por categoria e, em seguida, classifique por preço em ordem decrescente dentro das categorias. http://localhost/odata/Products?$orderby=Category,Price desc

Paginação Server-Driven

Se o banco de dados contiver milhões de registros, você não deseja enviá-los todos em uma carga. Para evitar isso, o servidor pode limitar o número de entradas que envia em uma única resposta. Para habilitar a paginação do servidor, defina a propriedade PageSize no atributo Queryable . O valor é o número máximo de entradas a serem retornadas.

[Queryable(PageSize=10)]
public IQueryable<Product> Get() 
{
    return products.AsQueryable();
}

Se o controlador retornar o formato OData, o corpo da resposta conterá um link para a próxima página de dados:

{
  "odata.metadata":"http://localhost/$metadata#Products",
  "value":[
    { "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
    { "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
    // Others not shown
  ],
  "odata.nextLink":"http://localhost/Products?$skip=10"
}

O cliente pode usar esse link para buscar a próxima página. Para saber o número total de entradas no conjunto de resultados, o cliente pode definir a opção de consulta $inlinecount com o valor "allpages".

http://localhost/Products?$inlinecount=allpages

O valor "allpages" informa ao servidor para incluir a contagem total na resposta:

{
  "odata.metadata":"http://localhost/$metadata#Products",
  "odata.count":"50",
  "value":[
    { "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
    { "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
    // Others not shown
  ]
}

Observação

Os links da próxima página e a contagem embutida exigem o formato OData. O motivo é que o OData define campos especiais no corpo da resposta para manter o link e a contagem.

Para formatos não OData, ainda é possível dar suporte a links de próxima página e contagem embutida, encapsulando os resultados da consulta em um objeto PageResult<T> . No entanto, ele requer um pouco mais de código. Veja um exemplo:

public PageResult<Product> Get(ODataQueryOptions<Product> options)
{
    ODataQuerySettings settings = new ODataQuerySettings()
    {
        PageSize = 5
    };

    IQueryable results = options.ApplyTo(_products.AsQueryable(), settings);

    return new PageResult<Product>(
        results as IEnumerable<Product>, 
        Request.GetNextPageLink(), 
        Request.GetInlineCount());
}

Aqui está um exemplo de resposta JSON:

{
  "Items": [
    { "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
    { "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },

    // Others not shown
    
  ],
  "NextPageLink": "http://localhost/api/values?$inlinecount=allpages&$skip=10",
  "Count": 50
}

Limitando as opções de consulta

As opções de consulta dão ao cliente muito controle sobre a consulta que é executada no servidor. Em alguns casos, talvez você queira limitar as opções disponíveis por motivos de segurança ou desempenho. O atributo [Queryable] tem algumas propriedades internas para isso. Veja alguns exemplos.

Permita apenas $skip e $top, para dar suporte à paginação e nada mais:

[Queryable(AllowedQueryOptions=
    AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]

Permita a ordenação somente por determinadas propriedades, para impedir a classificação em propriedades que não são indexadas no banco de dados:

[Queryable(AllowedOrderByProperties="Id")] // comma-separated list of properties

Permitir a função lógica "eq", mas nenhuma outra função lógica:

[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]

Não permita operadores aritméticos:

[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]

Você pode restringir as opções globalmente construindo uma instância QueryableAttribute e passando-a para a função EnableQuerySupport :

var queryAttribute = new QueryableAttribute()
{
    AllowedQueryOptions = AllowedQueryOptions.Top | AllowedQueryOptions.Skip,
    MaxTop = 100
};
                
config.EnableQuerySupport(queryAttribute);

Invocando opções de consulta diretamente

Em vez de usar o atributo [Queryable] , você pode invocar as opções de consulta diretamente no controlador. Para fazer isso, adicione um parâmetro ODataQueryOptions ao método do controlador. Nesse caso, você não precisa do atributo [Queryable] .

public IQueryable<Product> Get(ODataQueryOptions opts)
{
    var settings = new ODataValidationSettings()
    {
        // Initialize settings as needed.
        AllowedFunctions = AllowedFunctions.AllMathFunctions
    };

    opts.Validate(settings);

    IQueryable results = opts.ApplyTo(products.AsQueryable());
    return results as IQueryable<Product>;
}

A API Web preenche o ODataQueryOptions da cadeia de caracteres de consulta URI. Para aplicar a consulta, passe um IQueryable para o método ApplyTo . O método retorna outro IQueryable.

Para cenários avançados, se você não tiver um provedor de consultas IQueryable , poderá examinar o ODataQueryOptions e converter as opções de consulta em outro formulário. (Por exemplo, consulte a postagem no blog de RaghuRam Nadiminti Traduzindo consultas OData para HQL)

Validação de consulta

O atributo [Queryable] valida a consulta antes de executá-la. A etapa de validação é executada no método QueryableAttribute.ValidateQuery . Você também pode personalizar o processo de validação.

Confira também Diretrizes de Segurança do OData.

Primeiro, substitua uma das classes de validador definidas no namespace Web.Http.OData.Query.Validators . Por exemplo, a classe de validador a seguir desabilita a opção 'desc' para a opção $orderby.

public class MyOrderByValidator : OrderByQueryValidator
{
    // Disallow the 'desc' parameter for $orderby option.
    public override void Validate(OrderByQueryOption orderByOption,
                                    ODataValidationSettings validationSettings)
    {
        if (orderByOption.OrderByNodes.Any(
                node => node.Direction == OrderByDirection.Descending))
        {
            throw new ODataException("The 'desc' option is not supported.");
        }
        base.Validate(orderByOption, validationSettings);
    }
}

Subclasse o atributo [Queryable] para substituir o método ValidateQuery .

public class MyQueryableAttribute : QueryableAttribute
{
    public override void ValidateQuery(HttpRequestMessage request, 
        ODataQueryOptions queryOptions)
    {
        if (queryOptions.OrderBy != null)
        {
            queryOptions.OrderBy.Validator = new MyOrderByValidator();
        }
        base.ValidateQuery(request, queryOptions);
    }
}

Em seguida, defina seu atributo personalizado globalmente ou por controlador:

// Globally:
config.EnableQuerySupport(new MyQueryableAttribute());

// Per controller:
public class ValuesController : ApiController
{
    [MyQueryable]
    public IQueryable<Product> Get()
    {
        return products.AsQueryable();
    }
}

Se você estiver usando ODataQueryOptions diretamente, defina o validador nas opções:

public IQueryable<Product> Get(ODataQueryOptions opts)
{
    if (opts.OrderBy != null)
    {
        opts.OrderBy.Validator = new MyOrderByValidator();
    }

    var settings = new ODataValidationSettings()
    {
        // Initialize settings as needed.
        AllowedFunctions = AllowedFunctions.AllMathFunctions
    };

    // Validate
    opts.Validate(settings);

    IQueryable results = opts.ApplyTo(products.AsQueryable());
    return results as IQueryable<Product>;
}