Compatibilidad de las opciones de consulta de OData en ASP.NET Web API 2

por Mike Wasson

Esta información general con ejemplos de código muestra las opciones de consulta de OData admitidas en ASP.NET Web de API 2 para ASP.NET 4.x.

OData define parámetros que se pueden usar para modificar una consulta de OData. El cliente envía estos parámetros en la cadena de consulta del URI de solicitud. Por ejemplo, para ordenar los resultados, un cliente usa el parámetro $orderby:

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

La especificación OData llama a estos parámetros opciones de consulta. Puede habilitar las opciones de consulta de OData para cualquier controlador de API web del proyecto; el controlador no tiene que ser un punto de conexión de OData. Esto le proporciona una manera cómoda de agregar características como el filtrado y la ordenación a cualquier aplicación de API web.

Antes de habilitar las opciones de consulta, lea el tema Guía de seguridad de OData.

Habilitación de las opciones de consulta de OData

Web API también admite las siguientes opciones de consulta de OData:

Opción Descripción
$expand Expande las entidades relacionadas de forma alineada.
$filter Filtra los resultados en función de una condición booleana.
$inlinecount Indica al servidor que incluya el recuento total de entidades coincidentes en la respuesta. (Útil para la paginación del lado servidor).
$orderby Ordena los resultados.
$select Selecciona las propiedades que se van a incluir en la respuesta.
$skip Omite los primeros n resultados.
$top Devuelve solo los primeros n resultados.

Para usar las opciones de consulta de OData, debe habilitarlas explícitamente. Puede habilitarlas globalmente para toda la aplicación o habilitarlas para controladores específicos o acciones específicas.

Para habilitar las opciones de consulta de OData globalmente, llame a EnableQuerySupport en la clase HttpConfiguration en el inicio:

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

    config.EnableQuerySupport();

    // ...
}

El método EnableQuerySupport habilita las opciones de consulta globalmente para cualquier acción del controlador que devuelva un tipo IQueryable. Si no desea habilitar las opciones de consulta para toda la aplicación, puede habilitarlas para acciones de controlador específicas si agrega el atributo [Queryable] al método de acción.

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

Consultas de ejemplo

En esta sección se muestran los tipos de consultas que son posibles mediante las opciones de consulta de OData. Para obtener detalles específicos sobre las opciones de consulta, consulte la documentación de OData en www.odata.org.

Para obtener información sobre $expand y $select, consulte Uso de $select, $expand y $value en ASP.NET Web API OData.

Paginado Client-Driven

En el caso de los conjuntos de entidades grandes, es posible que el cliente quiera limitar el número de resultados. Por ejemplo, un cliente podría mostrar 10 entradas a la vez, con vínculos "siguiente" para obtener la siguiente página de resultados. Para ello, el cliente usa las opciones $top y $skip.

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

La opción $top proporciona el número máximo de entradas que se van a devolver y la opción $skip proporciona el número de entradas que se van a omitir. En el ejemplo anterior se capturan las entradas de 21 a 30.

Filtering

La opción $filter permite a un cliente filtrar los resultados mediante una expresión booleana. Las expresiones de filtro son bastante eficaces; incluyen operadores lógicos y aritméticos, funciones de cadena y funciones de fecha.

Devolver todos los productos con la categoría igual a "Toys". http://localhost/Products?$filter=Category eq 'Toys'
Devolver todos los productos con un precio inferior a 10. http://localhost/Products?$filter=Price lt 10
Operadores lógicos: devolver todos los productos donde precio sea >= 5 y <= 15. http://localhost/Products?$filter=Price ge 5 and Price le 15
Funciones de cadena: devolver todos los productos con "zz" en el nombre. http://localhost/Products?$filter=substringof('zz',Name)
Funciones de fecha: devolver todos los productos con ReleaseDate después de 2005. http://localhost/Products?$filter=year(ReleaseDate) gt 2005

Ordenación

Para ordenar los resultados, use el filtro $orderby.

Ordenar por precio. http://localhost/Products?$orderby=Price
Ordenar por precio en orden descendente (más alto a más bajo). http://localhost/Products?$orderby=Price desc
Ordenar por categoría y, a continuación, ordenar por precio en orden descendente dentro de las categorías. http://localhost/odata/Products?$orderby=Category,Price desc

Paginación controlada por servidor

Si la base de datos contiene millones de registros, es mejor no enviarlos todos en una carga. Para evitar esto, el servidor puede limitar el número de entradas que envía en una única respuesta. Para habilitar la paginación del servidor, establezca la propiedad PageSize en el atributo Queryable. El valor es el número máximo de entradas que se van a devolver.

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

Si el controlador devuelve el formato OData, el cuerpo de la respuesta contendrá un vínculo a la página siguiente de datos:

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

El cliente puede usar este vínculo para capturar la página siguiente. Para obtener información sobre el número total de entradas del conjunto de resultados, el cliente puede establecer la opción de consulta $inlinecount con el valor "allpages".

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

El valor "allpages" indica al servidor que incluya el recuento total en la respuesta:

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

Nota:

Los vínculos de página siguiente y el recuento en línea requieren formato OData. El motivo es que OData define campos especiales en el cuerpo de la respuesta para contener el vínculo y el recuento.

En el caso de los formatos que no son de OData, es posible admitir vínculos de página siguiente y recuento alineado, al ajustar los resultados de la consulta en un objeto PageResult<T>. Sin embargo, requiere un poco más de código. Este es un ejemplo:

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

Esta es una respuesta JSON de ejemplo:

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

Limitar las opciones de consulta

Las opciones de consulta proporcionan al cliente un gran control sobre la consulta que se ejecuta en el servidor. En algunos casos, puede que quiera limitar las opciones disponibles por motivos de seguridad o rendimiento. El atributo [Queryable] tiene algunas propiedades integradas para esto. Aquí hay algunos ejemplos.

Permitir solo $skip y $top, para admitir solamente la paginación:

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

Permitir ordenar solo por determinadas propiedades para evitar la ordenación en propiedades que no están indexadas en la base de datos:

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

Permitir la función lógica "eq", pero ninguna otra función lógica:

[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]

No permitir ningún operador aritmético:

[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]

Puede restringir las opciones globalmente si crea una instancia queryableAttribute y la pasa a la función EnableQuerySupport:

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

Invocar las opciones de consulta directamente

En lugar de usar el atributo [Queryable], puede invocar las opciones de consulta directamente en el controlador. Para ello, agregue un parámetro ODataQueryOptions al método de controlador. En este caso, no necesita el 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>;
}

La API web rellena ODataQueryOptions de la cadena de consulta de URI. Para aplicar la consulta, pase IQueryable al método ApplyTo. El método devuelve otro IQueryable.

En escenarios avanzados, si no tiene un proveedor de consultas IQueryable, puede examinar ODataQueryOptions y traducir las opciones de consulta en otro formulario. (Por ejemplo, consulte la entrada de blog de RaghuRam Nadiminti Traducción de consultas de OData a HQL)

Validación de consultas

El atributo [Queryable] valida la consulta antes de ejecutarla. El paso de validación se realiza en el método QueryableAttribute.ValidateQuery. También puede personalizar el proceso de validación.

Consulte también Guía de seguridad de OData.

En primer lugar, invalide una de las clases de validador definidas en el espacio de nombres Web.Http.OData.Query.Validators. Por ejemplo, la siguiente clase de validador deshabilita la opción "desc" para la opción $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);
    }
}

Convierta en subclase el atributo [Queryable] para invalidar el 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);
    }
}

A continuación, establezca el atributo personalizado global o por controlador:

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

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

Si usa ODataQueryOptions directamente, establezca el validador en las opciones:

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