Obsługa opcji zapytań OData w interfejsie API sieci Web 2 ASP.NET

Autor: Mike Wasson

Ten przegląd z przykładami kodu przedstawia obsługę opcji zapytań OData w ASP.NET Web API 2 dla ASP.NET 4.x.

OData definiuje parametry, których można użyć do modyfikowania zapytania OData. Klient wysyła te parametry w ciągu zapytania identyfikatora URI żądania. Na przykład w celu posortowania wyników klient używa parametru $orderby:

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

Specyfikacja OData wywołuje te opcje zapytania parametrów. W projekcie można włączyć opcje zapytań OData dla dowolnego kontrolera internetowego interfejsu API — kontroler nie musi być punktem końcowym OData. Zapewnia to wygodny sposób dodawania funkcji, takich jak filtrowanie i sortowanie do dowolnej aplikacji internetowego interfejsu API.

Przed włączeniem opcji zapytania zapoznaj się z tematem Wskazówki dotyczące zabezpieczeń OData.

Włączanie opcji zapytania OData

Internetowy interfejs API obsługuje następujące opcje zapytania OData:

Opcja Opis
$expand Rozwija powiązane jednostki w tekście.
$filter Filtruje wyniki na podstawie warunku logicznego.
$inlinecount Informuje serwer o dołączeniu łącznej liczby pasujących jednostek w odpowiedzi. (Przydatne w przypadku stronicowania po stronie serwera).
$orderby Sortuje wyniki.
$select Wybiera właściwości do uwzględnienia w odpowiedzi.
$skip Pomija pierwsze n wyników.
$top Zwraca tylko pierwsze n wyników.

Aby użyć opcji zapytania OData, należy je jawnie włączyć. Można je włączyć globalnie dla całej aplikacji lub włączyć dla określonych kontrolerów lub określonych akcji.

Aby włączyć opcje zapytań OData globalnie, wywołaj metodę EnableQuerySupport w klasie HttpConfiguration podczas uruchamiania :

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

    config.EnableQuerySupport();

    // ...
}

Metoda EnableQuerySupport umożliwia globalne opcje zapytań dla każdej akcji kontrolera zwracającej typ IQueryable . Jeśli nie chcesz włączyć opcji zapytania dla całej aplikacji, możesz włączyć je dla określonych akcji kontrolera, dodając atrybut [Queryable] do metody akcji.

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

Przykładowe zapytania

W tej sekcji przedstawiono typy zapytań, które są możliwe przy użyciu opcji zapytania OData. Aby uzyskać szczegółowe informacje o opcjach zapytania, zapoznaj się z dokumentacją OData pod adresem www.odata.org.

Aby uzyskać informacje o $expand i $select, zobacz Using $select, $expand and $value in ASP.NET Web API OData (Używanie $select, $expand i $value w usłudze OData internetowego interfejsu API ASP.NET).

Stronicowanie oparte na kliencie

W przypadku dużych zestawów jednostek klient może chcieć ograniczyć liczbę wyników. Na przykład klient może wyświetlać 10 wpisów jednocześnie z linkami "next", aby uzyskać następną stronę wyników. W tym celu klient używa opcji $top i $skip.

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

Opcja $top daje maksymalną liczbę wpisów do zwrócenia, a opcja $skip daje liczbę wpisów do pominięcia. Poprzedni przykład pobiera wpisy od 21 do 30.

Filtrowanie

Opcja $filter umożliwia klientowi filtrowanie wyników przez zastosowanie wyrażenia logicznego. Wyrażenia filtru są dość zaawansowane; obejmują operatory logiczne i arytmetyczne, funkcje ciągów i funkcje daty.

Zwróć wszystkie produkty z kategorią równą "Toys". http://localhost/Products?$filter=Category eq "Toys"
Zwróć wszystkie produkty z ceną mniejszą niż 10. http://localhost/Products?$filter=Price lt 10
Operatory logiczne: Zwracaj wszystkie produkty, w których cena = 5 i cena ><= 15. http://localhost/Products?$filter=Price ge 5 i Price le 15
Funkcje ciągów: Zwracaj wszystkie produkty o nazwie "zz". http://localhost/Products?$filter=substringof('zz',Name)
Funkcje daty: Zwracaj wszystkie produkty z wartością ReleaseDate po 2005 roku. http://localhost/Products?$filter=year(ReleaseDate) gt 2005

Sortowanie

Aby posortować wyniki, użyj filtru $orderby.

Sortuj według ceny. http://localhost/Products?$orderby=Price
Sortuj według ceny w kolejności malejącej (najwyższa do najniższej). http://localhost/Products?$orderby=Price desc
Sortuj według kategorii, a następnie sortuj według ceny w kolejności malejącej w kategoriach. http://localhost/odata/Products?$orderby=Category,Price desc

stronicowanie Server-Driven

Jeśli baza danych zawiera miliony rekordów, nie chcesz wysyłać ich wszystkich w jednym ładunku. Aby temu zapobiec, serwer może ograniczyć liczbę wpisów wysyłanych w jednej odpowiedzi. Aby włączyć stronicowanie serwera, ustaw właściwość PageSize w atrybucie Queryable . Wartość jest maksymalną liczbą wpisów do zwrócenia.

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

Jeśli kontroler zwróci format OData, treść odpowiedzi będzie zawierać link do następnej strony danych:

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

Klient może użyć tego linku, aby pobrać następną stronę. Aby poznać łączną liczbę wpisów w zestawie wyników, klient może ustawić opcję zapytania $inlinecount z wartością "allpages".

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

Wartość "allpages" informuje serwer o dołączeniu łącznej liczby w odpowiedzi:

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

Uwaga

Linki na następnej stronie i liczba śródliniowa wymagają formatu OData. Przyczyną jest to, że OData definiuje specjalne pola w treści odpowiedzi, aby przechowywać link i liczbę.

W przypadku formatów innych niż OData nadal można obsługiwać linki następnej strony i liczbę śródliniową, opakowując wyniki zapytania w obiekcie PageResult<T> . Jednak wymaga nieco więcej kodu. Oto przykład:

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

Oto przykładowa odpowiedź 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
}

Ograniczanie opcji zapytania

Opcje zapytania dają klientowi dużą kontrolę nad zapytaniem uruchamianym na serwerze. W niektórych przypadkach możesz ograniczyć dostępne opcje ze względów bezpieczeństwa lub wydajności. Atrybut [Queryable] ma kilka wbudowanych właściwości dla tego. Oto kilka przykładów.

Zezwalaj tylko na $skip i $top, aby obsługiwać stronicowanie i nic innego:

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

Zezwalaj na porządkowanie tylko według określonych właściwości, aby zapobiec sortowaniu właściwości, które nie są indeksowane w bazie danych:

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

Zezwalaj na funkcję logiczną "eq", ale nie ma innych funkcji logicznych:

[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]

Nie zezwalaj na żadne operatory arytmetyczne:

[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]

Opcje można ograniczyć globalnie, tworząc wystąpienie QueryableAttribute i przekazując je do funkcji EnableQuerySupport :

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

Wywoływanie opcji zapytania bezpośrednio

Zamiast używać atrybutu [Queryable] można wywołać opcje zapytania bezpośrednio w kontrolerze. W tym celu dodaj parametr ODataQueryOptions do metody kontrolera. W takim przypadku nie potrzebujesz atrybutu [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>;
}

Internetowy interfejs API wypełnia ciąg zapytania ODataQueryOptions z ciągu zapytania identyfikatora URI. Aby zastosować zapytanie, przekaż zapytanie IQueryable do metody ApplyTo . Metoda zwraca kolejną metodę IQueryable.

W przypadku zaawansowanych scenariuszy, jeśli nie masz dostawcy zapytań Z możliwością zapytania IQueryable , możesz sprawdzić protokół ODataQueryOptions i przetłumaczyć opcje zapytania na inny formularz. (Na przykład zobacz wpis w blogu RaghuRam Nadiminti tłumaczenia zapytań OData na HQL)

Sprawdzanie poprawności zapytania

Atrybut [Queryable] weryfikuje zapytanie przed jego wykonaniem. Krok weryfikacji jest wykonywany w metodzie QueryableAttribute.ValidateQuery . Można również dostosować proces weryfikacji.

Zobacz również wskazówki dotyczące zabezpieczeń OData.

Najpierw przesłoń jedną z klas modułu sprawdzania poprawności, która jest zdefiniowana w przestrzeni nazw Web.Http.OData.Query.Validators . Na przykład następująca klasa modułu sprawdzania poprawności wyłącza opcję "desc" dla opcji $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);
    }
}

Podklasa atrybutu [Queryable], aby zastąpić metodę 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);
    }
}

Następnie ustaw atrybut niestandardowy globalnie lub na kontroler:

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

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

Jeśli używasz funkcji ODataQueryOptions bezpośrednio, ustaw moduł sprawdzania poprawności w opcjach:

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