Utilisation de $select, $expand et $value dans API Web ASP.NET 2 OData

par Mike Wasson

Vue d’ensemble et exemples de code pour les options $expand, $select et $value dans l’API web OData 2 pour ASP.NET 4.x. Ces options permettent à un client de contrôler la représentation qu’il obtient à partir du serveur.

  • $expand entraîne l’inclusion d’entités associées dans la réponse.
  • $select sélectionne un sous-ensemble de propriétés à inclure dans la réponse.
  • $value obtient la valeur brute d’une propriété.

Exemple de schéma

Pour cet article, je vais utiliser un service OData qui définit trois entités : Produit, Fournisseur et Catégorie. Chaque produit a une catégorie et un fournisseur.

Diagramme montrant un exemple de schéma pour le service O Data, définissant des produits, des fournisseurs et des catégories en tant qu’entités.

Voici les classes C# qui définissent les modèles d’entité :

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

Notez que la classe définit les Product propriétés de navigation pour et SupplierCategory. La Category classe définit une propriété de navigation pour les produits de chaque catégorie.

Pour créer un point de terminaison OData pour ce schéma, utilisez la Visual Studio 2013 la structure, comme décrit dans Création d’un point de terminaison OData dans API Web ASP.NET. Ajoutez des contrôleurs distincts pour Produit, Catégorie et Fournisseur.

Activation des $expand et des $select

Dans Visual Studio 2013, la structure OData de l’API web crée un contrôleur qui prend automatiquement en charge les $expand et les $select. Pour référence, voici les conditions requises pour prendre en charge $expand et $select dans un contrôleur.

Pour les collections, la méthode du Get contrôleur doit retourner un IQueryable.

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

Pour les entités uniques, retournez un T> SingleResult<, où T est un IQueryable qui contient zéro ou une entité.

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

En outre, décorez vos Get méthodes avec l’attribut [Interrogeable], comme indiqué dans les extraits de code précédents. Vous pouvez également appeler EnableQuerySupport sur l’objet HttpConfiguration au démarrage. (Pour plus d’informations, consultez Activation des options de requête OData.)

Utilisation de $expand

Lorsque vous interrogez une entité ou une collection OData, la réponse par défaut n’inclut pas les entités associées. Par exemple, voici la réponse par défaut pour l’ensemble d’entités Categories :

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

Comme vous pouvez le voir, la réponse n’inclut aucun produit, même si l’entité Catégorie a un lien de navigation Products. Toutefois, le client peut utiliser $expand pour obtenir la liste des produits pour chaque catégorie. L’option $expand va dans la chaîne de requête de la requête :

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

À présent, le serveur inclut les produits pour chaque catégorie, en ligne avec les catégories. Voici la charge utile de réponse :

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

Notez que chaque entrée du tableau « value » contient une liste Products.

L’option $expand prend une liste de propriétés de navigation séparées par des virgules pour se développer. La demande suivante développe à la fois la catégorie et le fournisseur d’un produit.

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

Voici le corps de la réponse :

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

Vous pouvez développer plusieurs niveaux de propriété de navigation. L’exemple suivant inclut tous les produits d’une catégorie, ainsi que le fournisseur de chaque produit.

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

Voici le corps de la réponse :

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

Par défaut, l’API web limite la profondeur d’expansion maximale à 2. Cela empêche le client d’envoyer des requêtes complexes telles que $expand=Orders/OrderDetails/Product/Supplier/Region, ce qui peut être inefficace pour interroger et créer des réponses volumineuses. Pour remplacer la valeur par défaut, définissez la propriété MaxExpansionDepth sur l’attribut [Interrogeable].

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

Pour plus d’informations sur l’option $expand, consultez Développer l’option de requête système ($expand) dans la documentation OData officielle.

Utilisation de $select

L’option $select spécifie un sous-ensemble de propriétés à inclure dans le corps de la réponse. Par exemple, pour obtenir uniquement le nom et le prix de chaque produit, utilisez la requête suivante :

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

Voici le corps de la réponse :

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

Vous pouvez combiner $select et $expand dans la même requête. Veillez à inclure la propriété développée dans l’option $select. Par exemple, la demande suivante obtient le nom du produit et le fournisseur.

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

Voici le corps de la réponse :

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

Vous pouvez également sélectionner les propriétés d’une propriété développée. La requête suivante développe Produits et sélectionne le nom de la catégorie plus le nom du produit.

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

Voici le corps de la réponse :

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

Pour plus d’informations sur l’option $select, consultez Sélectionner l’option de requête système ($select) dans la documentation OData officielle.

Obtention des propriétés individuelles d’une entité ($value)

Il existe deux façons pour un client OData d’obtenir une propriété individuelle à partir d’une entité. Le client peut obtenir la valeur au format OData ou obtenir la valeur brute de la propriété.

La requête suivante obtient une propriété au format OData.

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

Voici un exemple de réponse au format 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"
}

Pour obtenir la valeur brute de la propriété, ajoutez $value à l’URI :

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

Voici la réponse. Notez que le type de contenu est « text/plain », et non JSON.

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

Hat

Pour prendre en charge ces requêtes dans votre contrôleur OData, ajoutez une méthode nommée GetProperty, où Property est le nom de la propriété. Par exemple, la méthode permettant d’obtenir la propriété Name est nommée GetName. La méthode doit retourner la valeur de cette propriété :

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