Prise en charge des options de requête OData dans API Web ASP.NET 2

par Mike Wasson

Cette vue d’ensemble avec des exemples de code illustre la prise en charge des options de requête OData dans API Web ASP.NET 2 pour ASP.NET 4.x.

OData définit les paramètres qui peuvent être utilisés pour modifier une requête OData. Le client envoie ces paramètres dans la chaîne de requête de l’URI de requête. Par exemple, pour trier les résultats, un client utilise le paramètre $orderby :

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

La spécification OData appelle ces options de requête de paramètres. Vous pouvez activer les options de requête OData pour n’importe quel contrôleur d’API web dans votre projet. Le contrôleur n’a pas besoin d’être un point de terminaison OData. Cela vous offre un moyen pratique d’ajouter des fonctionnalités telles que le filtrage et le tri à n’importe quelle application API web.

Avant d’activer les options de requête, lisez la rubrique Conseils de sécurité OData.

Activation des options de requête OData

L’API web prend en charge les options de requête OData suivantes :

Option Description
$expand Développe les entités associées inline.
$filter Filtre les résultats en fonction d’une condition booléenne.
$inlinecount Indique au serveur d’inclure le nombre total d’entités correspondantes dans la réponse. (Utile pour la pagination côté serveur.)
$orderby Trie les résultats.
$select Sélectionne les propriétés à inclure dans la réponse.
$skip Ignore les n premiers résultats.
$top Retourne uniquement le premier n des résultats.

Pour utiliser les options de requête OData, vous devez les activer explicitement. Vous pouvez les activer globalement pour l’ensemble de l’application ou les activer pour des contrôleurs spécifiques ou des actions spécifiques.

Pour activer globalement les options de requête OData, appelez EnableQuerySupport sur la classe HttpConfiguration au démarrage :

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

    config.EnableQuerySupport();

    // ...
}

La méthode EnableQuerySupport active les options de requête globalement pour toute action de contrôleur qui retourne un type IQueryable . Si vous ne souhaitez pas que les options de requête soient activées pour l’ensemble de l’application, vous pouvez les activer pour des actions de contrôleur spécifiques en ajoutant l’attribut [Interrogeable] à la méthode d’action.

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

Exemples de requêtes

Cette section présente les types de requêtes possibles à l’aide des options de requête OData. Pour plus d’informations sur les options de requête, reportez-vous à la documentation OData sur www.odata.org.

Pour plus d’informations sur $expand et $select, consultez Utilisation de $select, de $expand et de $value dans API Web ASP.NET OData.

Pagination pilotée par le client

Pour les jeux d’entités volumineux, le client peut vouloir limiter le nombre de résultats. Par exemple, un client peut afficher 10 entrées à la fois, avec des liens « suivant » pour obtenir la page de résultats suivante. Pour ce faire, le client utilise les options $top et $skip.

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

L’option $top indique le nombre maximal d’entrées à retourner, et l’option $skip indique le nombre d’entrées à ignorer. L’exemple précédent extrait les entrées 21 à 30.

Filtrage

L’option $filter permet à un client de filtrer les résultats en appliquant une expression booléenne. Les expressions de filtre sont assez puissantes ; ils incluent des opérateurs logiques et arithmétiques, des fonctions de chaîne et des fonctions de date.

Retourne tous les produits dont la catégorie est égale à « Jouets ». http://localhost/Products?$filter=Category eq 'Toys'
Retourner tous les produits dont le prix est inférieur à 10. http://localhost/Products?$filter=Price lt 10
Opérateurs logiques : retourne tous les produits dont le prix >= 5 et le prix <= 15. http://localhost/Products?$filter=Price ge 5 et Price le 15
Fonctions de chaîne : retourne tous les produits avec « zz » dans le nom. http://localhost/Products?$filter=substringof('zz',Name)
Fonctions de date : retourne tous les produits avec ReleaseDate après 2005. http://localhost/Products?$filter=year(ReleaseDate) gt 2005

Tri

Pour trier les résultats, utilisez le filtre $orderby.

Trier par prix. http://localhost/Products?$orderby=Price
Trier par prix dans l’ordre décroissant (du plus élevé au plus bas). http://localhost/Products?$orderby=Price desc
Triez par catégorie, puis par prix dans l’ordre décroissant dans les catégories. http://localhost/odata/Products?$orderby=Category,Price desc

pagination Server-Driven

Si votre base de données contient des millions d’enregistrements, vous ne souhaitez pas les envoyer tous en une seule charge utile. Pour éviter cela, le serveur peut limiter le nombre d’entrées qu’il envoie dans une seule réponse. Pour activer la pagination du serveur, définissez la propriété PageSize dans l’attribut Interrogeable . La valeur est le nombre maximal d’entrées à retourner.

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

Si votre contrôleur retourne le format OData, le corps de la réponse contient un lien vers la page de données suivante :

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

Le client peut utiliser ce lien pour extraire la page suivante. Pour connaître le nombre total d’entrées dans le jeu de résultats, le client peut définir l’option de requête $inlinecount avec la valeur « allpages ».

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

La valeur « allpages » indique au serveur d’inclure le nombre total dans la réponse :

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

Notes

Les liens de page suivante et le nombre inline nécessitent tous deux le format OData. La raison en est qu’OData définit des champs spéciaux dans le corps de la réponse pour contenir le lien et le nombre.

Pour les formats non-OData, il est toujours possible de prendre en charge les liens de page suivante et le nombre inline, en encapsulant les résultats de la requête dans un objet PageResult<T> . Toutefois, cela nécessite un peu plus de code. Voici un exemple :

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

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

Limitation des options de requête

Les options de requête donnent au client un contrôle important sur la requête exécutée sur le serveur. Dans certains cas, vous pouvez limiter les options disponibles pour des raisons de sécurité ou de performances. L’attribut [Interrogeable] a des propriétés intégrées pour cela. Voici quelques exemples.

Autorisez uniquement $skip et $top, pour prendre en charge la pagination et rien d’autre :

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

Autorisez le classement uniquement par certaines propriétés, pour empêcher le tri sur les propriétés qui ne sont pas indexées dans la base de données :

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

Autorisez la fonction logique « eq », mais aucune autre fonction logique :

[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]

N’autorisez aucun opérateur arithmétique :

[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]

Vous pouvez restreindre les options globalement en construisant un instance QueryableAttribute et en le transmettant à la fonction EnableQuerySupport :

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

Appel direct des options de requête

Au lieu d’utiliser l’attribut [Interrogeable], vous pouvez appeler les options de requête directement dans votre contrôleur. Pour ce faire, ajoutez un paramètre ODataQueryOptions à la méthode du contrôleur. Dans ce cas, vous n’avez pas besoin de l’attribut [Interrogeable].

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

L’API web remplit les ODataQueryOptions à partir de la chaîne de requête URI. Pour appliquer la requête, passez un IQueryable à la méthode ApplyTo . La méthode retourne un autre IQueryable.

Pour les scénarios avancés, si vous n’avez pas de fournisseur de requête IQueryable , vous pouvez examiner les ODataQueryOptions et traduire les options de requête sous une autre forme. (Par exemple, consultez le billet de blog de RaghuRam Nadiminti Traduire des requêtes OData en HQL)

Validation des requêtes

L’attribut [Interrogeable] valide la requête avant de l’exécuter. L’étape de validation est effectuée dans la méthode QueryableAttribute.ValidateQuery . Vous pouvez également personnaliser le processus de validation.

Consultez également conseils de sécurité OData.

Tout d’abord, remplacez l’une des classes de validateur définies dans l’espace de noms Web.Http.OData.Query.Validateors . Par exemple, la classe de validateur suivante désactive l’option « desc » pour l’option $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);
    }
}

Sous-classez l’attribut [Queryable] pour remplacer la méthode 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);
    }
}

Ensuite, définissez votre attribut personnalisé globalement ou par contrôleur :

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

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

Si vous utilisez ODataQueryOptions directement, définissez le validateur sur les options :

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