Supporto delle opzioni di query OData in API Web ASP.NET 2

di Mike Wasson

Questa panoramica con esempi di codice illustra le opzioni di query OData supportate in API Web ASP.NET 2 per ASP.NET 4.x.

OData definisce i parametri che possono essere usati per modificare una query OData. Il client invia questi parametri nella stringa di query dell'URI della richiesta. Ad esempio, per ordinare i risultati, un client usa il parametro $orderby:

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

La specifica OData chiama queste opzioni di query per i parametri. È possibile abilitare le opzioni di query OData per qualsiasi controller API Web nel progetto. Il controller non deve essere un endpoint OData. In questo modo è possibile aggiungere funzionalità come il filtro e l'ordinamento a qualsiasi applicazione API Web.

Prima di abilitare le opzioni di query, leggere l'argomento Indicazioni sulla sicurezza OData.

Abilitazione delle opzioni di query OData

L'API Web supporta le opzioni di query OData seguenti:

Opzione Descrizione
$expand Espande le entità correlate inline.
$filter Filtra i risultati in base a una condizione booleana.
$inlinecount Indica al server di includere il numero totale di entità corrispondenti nella risposta. Utile per il paging sul lato server.
$orderby Ordina i risultati.
$select Seleziona le proprietà da includere nella risposta.
$skip Ignora i primi n risultati.
$top Restituisce solo i primi n risultati.

Per usare le opzioni di query OData, è necessario abilitarle in modo esplicito. È possibile abilitarli a livello globale per l'intera applicazione o abilitarli per controller specifici o azioni specifiche.

Per abilitare le opzioni di query OData a livello globale, chiamare EnableQuerySupport nella classe HttpConfiguration all'avvio:

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

    config.EnableQuerySupport();

    // ...
}

Il metodo EnableQuerySupport abilita le opzioni di query a livello globale per qualsiasi azione del controller che restituisce un tipo IQueryable . Se non si vuole abilitare le opzioni di query per l'intera applicazione, è possibile abilitarle per azioni del controller specifiche aggiungendo l'attributo [Queryable] al metodo di azione.

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

Query di esempio

In questa sezione vengono illustrati i tipi di query possibili usando le opzioni di query OData. Per informazioni dettagliate specifiche sulle opzioni di query, vedere la documentazione di OData all'indirizzo www.odata.org.

Per informazioni su $expand e $select, vedere Uso di $select, $expand e $value in API Web ASP.NET OData.

Paging basato su client

Per i set di entità di grandi dimensioni, il client potrebbe voler limitare il numero di risultati. Ad esempio, un client potrebbe visualizzare 10 voci alla volta, con collegamenti "next" per ottenere la pagina successiva dei risultati. A tale scopo, il client usa le opzioni $top e $skip.

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

L'opzione $top fornisce il numero massimo di voci da restituire e l'opzione $skip fornisce il numero di voci da ignorare. Nell'esempio precedente vengono recuperate le voci da 21 a 30.

Filtro

L'opzione $filter consente a un client di filtrare i risultati applicando un'espressione booleana. Le espressioni di filtro sono molto potenti; includono operatori logici e aritmetici, funzioni stringa e funzioni di data.

Restituisce tutti i prodotti con categoria uguale a "Toys". http://localhost/Products?$filter=Category eq 'Toys'
Restituisce tutti i prodotti con prezzo inferiore a 10. http://localhost/Products?$filter=Price lt 10
Operatori logici: restituisce tutti i prodotti in cui prezzo >= 5 e prezzo <= 15. http://localhost/Products?$filter=Price ge 5 e Price le 15
Funzioni stringa: restituisce tutti i prodotti con "zz" nel nome. http://localhost/Products?$filter=substringof('zz',Name)
Funzioni di data: restituisce tutti i prodotti con ReleaseDate dopo il 2005. http://localhost/Products?$filter=year(ReleaseDate) gt 2005

Ordinamento

Per ordinare i risultati, usare il filtro $orderby.

Ordina per prezzo. http://localhost/Products?$orderby=Price
Ordina per prezzo in ordine decrescente (più alto al più basso). http://localhost/Products?$orderby=Price desc
Ordinare per categoria, quindi ordinare in base al prezzo in ordine decrescente all'interno delle categorie. http://localhost/odata/Products?$orderby=Category,Price desc

paging Server-Driven

Se il database contiene milioni di record, non si vuole inviarli tutti in un unico payload. Per evitare questo problema, il server può limitare il numero di voci inviate in una singola risposta. Per abilitare il paging del server, impostare la proprietà PageSize nell'attributo Queryable . Il valore è il numero massimo di voci da restituire.

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

Se il controller restituisce il formato OData, il corpo della risposta conterrà un collegamento alla pagina successiva di dati:

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

Il client può usare questo collegamento per recuperare la pagina successiva. Per informazioni sul numero totale di voci nel set di risultati, il client può impostare l'opzione di query $inlinecount con il valore "allpages".

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

Il valore "allpages" indica al server di includere il conteggio totale nella risposta:

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

I collegamenti di pagina successiva e il conteggio inline richiedono entrambi il formato OData. Il motivo è che OData definisce campi speciali nel corpo della risposta per contenere il collegamento e il conteggio.

Per i formati non OData, è comunque possibile supportare i collegamenti di pagina successiva e il conteggio inline, eseguendo il wrapping dei risultati della query in un oggetto PageResult<T> . Tuttavia, richiede un po'più di codice. Esempio:

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

Di seguito è riportato un esempio di risposta 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
}

Limitazione delle opzioni di query

Le opzioni di query offrono al client un elevato controllo sulla query eseguita nel server. In alcuni casi, è possibile limitare le opzioni disponibili per motivi di sicurezza o prestazioni. L'attributo [Queryable] include alcune proprietà predefinite. Di seguito sono riportati alcuni esempi.

Consentire solo $skip e $top, per supportare il paging e nient'altro:

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

Consentire l'ordinamento solo in base a determinate proprietà, per impedire l'ordinamento in base alle proprietà non indicizzate nel database:

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

Consentire la funzione logica "eq", ma non altre funzioni logiche:

[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]

Non consentire operatori aritmetici:

[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]

È possibile limitare le opzioni a livello globale creando un'istanza di QueryableAttribute e passandola alla funzione EnableQuerySupport :

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

Richiamo diretto delle opzioni di query

Anziché usare l'attributo [Queryable] , è possibile richiamare le opzioni di query direttamente nel controller. A tale scopo, aggiungere un parametro ODataQueryOptions al metodo controller. In questo caso, non è necessario l'attributo [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>;
}

L'API Web popola ODataQueryOptions dalla stringa di query URI. Per applicare la query, passare un oggetto IQueryable al metodo ApplyTo . Il metodo restituisce un altro oggetto IQueryable.

Per gli scenari avanzati, se non si dispone di un provider di query IQueryable , è possibile esaminare ODataQueryOptions e convertire le opzioni di query in un altro modulo. (Ad esempio, vedere il post di blog di RaghuRam Nadiminti Traducendo query OData in HQL)

Convalida query

L'attributo [Queryable] convalida la query prima di eseguirla. Il passaggio di convalida viene eseguito nel metodo QueryableAttribute.ValidateQuery . È anche possibile personalizzare il processo di convalida.

Vedere anche Indicazioni sulla sicurezza OData.

Eseguire prima di tutto l'override di una delle classi validator definite nello spazio dei nomi Web.Http.OData.Query.Validators . Ad esempio, la classe validator seguente disabilita l'opzione 'desc' per l'opzione $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);
    }
}

Sottoclasse l'attributo [Queryable] per eseguire l'override del metodo 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);
    }
}

Impostare quindi l'attributo personalizzato a livello globale o per controller:

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

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

Se si usa direttamente ODataQueryOptions , impostare il validator nelle opzioni:

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