Usar $select, $expand y $value en ASP.NET Web API 2 OData

de Mike Wasson

Información general y ejemplos de código para las opciones de $expand, $select y $value en OData Web API 2 para ASP.NET 4.x. Estas opciones permiten a un cliente controlar la representación que devuelve del servidor.

  • $expand hace que las entidades relacionadas se incluyan insertadas en la respuesta.
  • $select selecciona un subconjunto de propiedades que se incluirán en la respuesta.
  • $value obtiene el valor sin formato de una propiedad.

Esquema de ejemplo

En este artículo, usaré un servicio OData que define tres entidades: Producto, Proveedor y Categoría. Cada producto tiene una categoría y un proveedor.

Diagram that shows a sample schema for the O Data service, defining a Products, Suppliers, and Categories as its entities.

Estas son las clases de C# que definen los modelos de entidad:

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

Observe que la Product clase define las propiedades de navegación para Supplier y Category. La Category clase define una propiedad de navegación para los productos de cada categoría.

Para crear un punto de conexión de OData para este esquema, use la Visual Studio 2013 scaffolding, tal y como se describe en Creación de un punto de conexión de OData en ASP.NET Web API. Agregue controladores independientes para Producto, Categoría y Proveedor.

Habilitación de $expand y $select

En Visual Studio 2013, el scaffolding de OData de la API web crea un controlador que admite automáticamente $expand y $select. Por referencia, estos son los requisitos para admitir $expand y $select en un controlador.

En el caso de las colecciones, el método del controlador Get debe devolver un objeto IQueryable.

[Queryable]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

Para entidades únicas, devuelve un SingleResult<T>, donde T es un IQueryable que contiene cero o una entidades.

[Queryable]
public SingleResult<Category> GetCategory([FromODataUri] int key)
{
    return SingleResult.Create(db.Categories.Where(c => c.ID == key));
}

Además, decora los Get métodos con el atributo [Queryable], como se muestra en los fragmentos de código anteriores. Como alternativa, llame a EnableQuerySupport en el objeto HttpConfiguration en el inicio. (Para obtener más información, consulte Compatibilidad con las opciones de consulta de OData.)

Uso de $expand

Al consultar una entidad o colección de OData, la respuesta predeterminada no incluye entidades relacionadas. Por ejemplo, esta es la respuesta predeterminada para el conjunto de entidades Categorias:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories",
  "value":[
    {"ID":1,"Name":"Apparel"},
    {"ID":2,"Name":"Toys"}
  ]
}

Como puede ver, la respuesta no incluye ningún producto, aunque la entidad Categoría tenga un vínculo de navegación Productos. Sin embargo, el cliente puede usar $expand para obtener la lista de productos de cada categoría. La opción $expand va en la cadena de consulta de la solicitud:

GET http://localhost/odata/Categories?$expand=Products

Ahora el servidor incluirá los productos de cada categoría, alineados con las categorías. Esta es la carga de la respuesta:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories",
  "value":[
    {
      "Products":[
        {"ID":1,"Name":"Hat","Price":"15.00","CategoryId":1,"SupplierId":"CTSO"},
        {"ID":2,"Name":"Scarf","Price":"12.00","CategoryId":1,"SupplierId":"CTSO"},
        {"ID":3,"Name":"Socks","Price":"5.00","CategoryId":1,"SupplierId":"FBRK"}
      ],
      "ID":1,
      "Name":"Apparel"
    },
    {
      "Products":[
        {"ID":4,"Name":"Yo-yo","Price":"4.95","CategoryId":2,"SupplierId":"WING"},
        {"ID":5,"Name":"Puzzle","Price":"8.00","CategoryId":2,"SupplierId":"WING"}
      ],
      "ID":2,
      "Name":"Toys"
    }
  ]
}

Observe que cada entrada de la matriz "value" contiene una lista de Productos.

La opción $expand toma una lista separada por comas de propiedades de navegación que se van a expandir. La siguiente solicitud expande la categoría y el proveedor de un producto.

GET http://localhost/odata/Products(1)?$expand=Category,Supplier

Este es el cuerpo de la respuesta:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products/@Element",
  "Category": {"ID":1,"Name":"Apparel"},
  "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
  "ID":1,
  "Name":"Hat",
  "Price":"15.00",
  "CategoryId":1,
  "SupplierId":"CTSO"
}

Puede expandir más de un nivel de propiedad de navegación. En el ejemplo siguiente se incluyen todos los productos de una categoría y también el proveedor de cada producto.

GET http://localhost/odata/Categories(1)?$expand=Products/Supplier

Este es el cuerpo de la respuesta:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories/@Element",
  "Products":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":1,"Name":"Hat","Price":"15.00","CategoryId":1,"SupplierId":"CTSO"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":2,"Name":"Scarf","Price":"12.00","CategoryId":1,"SupplierId":"CTSO"
    },{
      "Supplier":{
        "Key":"FBRK","Name":"Fabrikam, Inc."
      },"ID":3,"Name":"Socks","Price":"5.00","CategoryId":1,"SupplierId":"FBRK"
    }
  ],"ID":1,"Name":"Apparel"
}

De forma predeterminada, la API web limita la profundidad de expansión máxima a 2. Esto impide que el cliente envíe solicitudes complejas como $expand=Orders/OrderDetails/Product/Supplier/Region, lo que podría ser ineficaz para consultar y crear respuestas grandes. Para invalidar el valor predeterminado, establezca la propiedad MaxExpansionDepth en el atributo [Queryable].

[Queryable(MaxExpansionDepth=4)]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

Para obtener más información sobre la opción $expand, vea Expandir opción de consulta del sistema ($expand) en la documentación oficial de OData.

Uso de $select

La opción $select especifica un subconjunto de propiedades que se incluirán en el cuerpo de la respuesta. Por ejemplo, para obtener solo el nombre y el precio de cada producto, use la siguiente consulta:

GET http://localhost/odata/Products?$select=Price,Name

Este es el cuerpo de la respuesta:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Price,Name",
  "value":[
    {"Price":"15.00","Name":"Hat"},
    {"Price":"12.00","Name":"Scarf"},
    {"Price":"5.00","Name":"Socks"},
    {"Price":"4.95","Name":"Yo-yo"},
    {"Price":"8.00","Name":"Puzzle"}
  ]
}

Puede combinar $select y $expand en la misma consulta. Asegúrese de incluir la propiedad expandida en la opción $select. Por ejemplo, la siguiente solicitud obtiene el nombre del producto y el proveedor.

GET http://localhost/odata/Products?$select=Name,Supplier&$expand=Supplier

Este es el cuerpo de la respuesta:

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Name,Supplier",
  "value":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Hat"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Scarf"
    },
    {
      "Supplier":{"Key":"FBRK","Name":"Fabrikam, Inc."},
      "Name":"Socks"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Yo-yo"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Puzzle"
   }
  ]
}

También puede seleccionar las propiedades dentro de una propiedad expandida. La siguiente solicitud expande Productos y selecciona el nombre de la categoría más el nombre del producto.

GET http://localhost/odata/Categories?$expand=Products&$select=Name,Products/Name

Este es el cuerpo de la respuesta:

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories&$select=Name,Products/Name",
  "value":[ 
    {
      "Products":[ {"Name":"Hat"},{"Name":"Scarf"},{"Name":"Socks"} ],
      "Name":"Apparel"
    },
    {
      "Products":[ {"Name":"Yo-yo"},{"Name":"Puzzle"} ],
      "Name":"Toys"
    }
  ]
}

Para obtener más información sobre la opción de $select, vea Seleccionar opción de consulta del sistema ($select) en la documentación oficial de OData.

Obtener propiedades individuales de una entidad ($value)

Hay dos maneras de que un cliente de OData obtenga una propiedad individual de una entidad. El cliente puede obtener el valor en formato OData u obtener el valor sin procesar de la propiedad.

La siguiente solicitud obtiene una propiedad en formato OData.

GET http://localhost/odata/Products(1)/Name

Esta es una respuesta de ejemplo en formato JSON:

HTTP/1.1 200 OK
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 90

{
  "odata.metadata":"http://localhost:14239/odata/$metadata#Edm.String",
  "value":"Hat"
}

Para obtener el valor sin formato de la propiedad, anexe $value al URI:

GET http://localhost/odata/Products(1)/Name/$value

Esta es la respuesta. Observe que el tipo de contenido es "text/plain", no JSON.

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Hat

Para admitir estas consultas en el controlador de OData, agregue un método denominado GetProperty, donde Property es el nombre de la propiedad. Por ejemplo, el método para obtener la propiedad Nombre se denominaría GetName. El método debe devolver el valor de esa propiedad:

public async Task<IHttpActionResult> GetName(int key)
{
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product.Name);
}