Unterstützung von OData-Abfrageoptionen in ASP.NET-Web-API 2

von Mike Wasson

Diese Übersicht mit Codebeispielen veranschaulicht die unterstützenden OData-Abfrageoptionen in ASP.NET-Web-API 2 für ASP.NET 4.x.

OData definiert Parameter, die zum Ändern einer OData-Abfrage verwendet werden können. Der Client sendet diese Parameter in der Abfragezeichenfolge des Anforderungs-URI. Beispielsweise verwendet ein Client zum Sortieren der Ergebnisse den parameter $orderby:

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

Die OData-Spezifikation ruft diese Parameterabfrageoptionen auf. Sie können OData-Abfrageoptionen für jeden Web-API-Controller in Ihrem Projekt aktivieren. Der Controller muss kein OData-Endpunkt sein. Dies bietet Ihnen eine bequeme Möglichkeit, Funktionen wie Das Filtern und Sortieren zu jeder Web-API-Anwendung hinzuzufügen.

Lesen Sie vor dem Aktivieren von Abfrageoptionen das Thema OData-Sicherheitsleitfaden.

Aktivieren von OData-Abfrageoptionen

Die Web-API unterstützt die folgenden OData-Abfrageoptionen:

Option BESCHREIBUNG
$expand Erweitert verwandte Entitäten inline.
$filter Filtert die Ergebnisse basierend auf einer booleschen Bedingung.
$inlinecount Weist den Server an, die Gesamtanzahl der übereinstimmenden Entitäten in die Antwort einzuschließen. (Nützlich für serverseitiges Paging.)
$orderby Sortiert die Ergebnisse.
$select Wählt aus, welche Eigenschaften in die Antwort eingeschlossen werden sollen.
$skip Überspringt die ersten n Ergebnisse.
$top Gibt nur das erste n ergebnis zurück.

Um OData-Abfrageoptionen verwenden zu können, müssen Sie sie explizit aktivieren. Sie können sie global für die gesamte Anwendung oder für bestimmte Controller oder bestimmte Aktionen aktivieren.

Um OData-Abfrageoptionen global zu aktivieren, rufen Sie enableQuerySupport für die HttpConfiguration-Klasse beim Start auf:

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

    config.EnableQuerySupport();

    // ...
}

Die EnableQuerySupport-Methode aktiviert Abfrageoptionen global für jede Controlleraktion, die einen IQueryable-Typ zurückgibt. Wenn Sie keine Abfrageoptionen für die gesamte Anwendung aktivieren möchten, können Sie sie für bestimmte Controlleraktionen aktivieren, indem Sie der Aktionsmethode das Attribut [Queryable] hinzufügen.

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

Beispielabfragen

In diesem Abschnitt werden die Arten von Abfragen gezeigt, die mithilfe der OData-Abfrageoptionen möglich sind. Ausführliche Informationen zu den Abfrageoptionen finden Sie in der OData-Dokumentation unter www.odata.org.

Informationen zu $expand und $select finden Sie unter Verwenden von $select, $expand und $value in ASP.NET-Web-API OData.

Clientgesteuertes Paging

Bei großen Entitätssätzen möchte der Client möglicherweise die Anzahl der Ergebnisse einschränken. Ein Client kann z. B. 10 Einträge gleichzeitig mit Links zum Nächsten anzeigen, um die nächste Seite mit Ergebnissen abzurufen. Dazu verwendet der Client die Optionen $top und $skip.

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

Die Option $top gibt die maximale Anzahl von zurückzugebenden Einträgen an, und die Option $skip gibt die Anzahl der zu überspringenden Einträge an. Im vorherigen Beispiel werden die Einträge 21 bis 30 abgerufen.

Filterung

Mit der Option $filter kann ein Client die Ergebnisse filtern, indem er einen booleschen Ausdruck anwendet. Die Filterausdrücke sind sehr leistungsfähig. sie umfassen logische und arithmetische Operatoren, Zeichenfolgenfunktionen und Datumsfunktionen.

Geben Sie alle Produkte mit der Kategorie "Spielzeug" zurück. http://localhost/Products?$filter=Category eq 'Toys'
Geben Sie alle Produkte mit einem Preis unter 10 zurück. http://localhost/Products?$filter=Price lt 10
Logische Operatoren: Geben alle Produkte zurück, bei denen Preis >= 5 und Preis <= 15 sind. http://localhost/Products?$filter=Price ge 5 und Price le 15
Zeichenfolgenfunktionen: Geben alle Produkte mit "zz" im Namen zurück. http://localhost/Products?$filter=substringof('zz',Name)
Datumsfunktionen: Geben alle Produkte mit ReleaseDate nach 2005 zurück. http://localhost/Products?$filter=year(ReleaseDate) gt 2005

Sortierung

Verwenden Sie zum Sortieren der Ergebnisse den filter $orderby.

Nach Preis sortieren. http://localhost/Products?$orderby=Price
Sortieren Sie nach Preis in absteigender Reihenfolge (höchste bis niedrigste). http://localhost/Products?$orderby=Price desc
Sortieren Sie nach Kategorie, und sortieren Sie dann nach Preis in absteigender Reihenfolge innerhalb von Kategorien. http://localhost/odata/Products?$orderby=Category,Price desc

Server-Driven Paging

Wenn Ihre Datenbank Millionen von Datensätzen enthält, möchten Sie sie nicht alle in einer Nutzlast senden. Um dies zu verhindern, kann der Server die Anzahl der Einträge begrenzen, die er in einer einzelnen Antwort sendet. Um server paging zu aktivieren, legen Sie die PageSize-Eigenschaft im Queryable-Attribut fest. Der Wert ist die maximale Anzahl von Zurückzugebenden Einträgen.

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

Wenn ihr Controller das OData-Format zurückgibt, enthält der Antworttext einen Link zur nächsten Seite der Daten:

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

Der Client kann diesen Link verwenden, um die nächste Seite abzurufen. Um die Gesamtzahl der Einträge im Resultset zu erfahren, kann der Client die $inlinecount-Abfrageoption mit dem Wert "allpages" festlegen.

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

Der Wert "allpages" weist den Server an, die Gesamtanzahl in die Antwort einzuschließen:

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

Hinweis

Links der nächsten Seite und Inlineanzahl erfordern das OData-Format. Der Grund dafür ist, dass OData spezielle Felder im Antworttext definiert, um den Link und die Anzahl zu enthalten.

Für Nicht-OData-Formate ist es weiterhin möglich, Links auf der nächsten Seite und die Inlineanzahl zu unterstützen, indem die Abfrageergebnisse in ein PageResult<T-Objekt> umschlossen werden. Es erfordert jedoch etwas mehr Code. Beispiel:

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

Hier sehen Sie ein Beispiel für eine JSON-Antwort:

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

Einschränken der Abfrageoptionen

Die Abfrageoptionen geben dem Client eine große Kontrolle über die Abfrage, die auf dem Server ausgeführt wird. In einigen Fällen können Sie die verfügbaren Optionen aus Sicherheits- oder Leistungsgründen einschränken. Das [Queryable] -Attribut verfügt über einige integrierte Eigenschaften dafür. Hier einige Beispiele.

Lassen Sie nur $skip und $top zu, um Paging und nichts anderes zu unterstützen:

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

Lassen Sie die Reihenfolge nur nach bestimmten Eigenschaften zu, um die Sortierung nach Eigenschaften zu verhindern, die in der Datenbank nicht indiziert sind:

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

Lassen Sie die logische Funktion "eq" zu, aber keine anderen logischen Funktionen:

[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]

Keine arithmetischen Operatoren zulassen:

[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]

Sie können Optionen global einschränken, indem Sie ein QueryableAttribute-instance erstellen und an die EnableQuerySupport-Funktion übergeben:

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

Direktes Aufrufen von Abfrageoptionen

Anstatt das [Queryable] -Attribut zu verwenden, können Sie die Abfrageoptionen direkt in Ihrem Controller aufrufen. Fügen Sie dazu der Controllermethode einen ODataQueryOptions-Parameter hinzu. In diesem Fall benötigen Sie das [Queryable] -Attribut nicht.

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

Die Web-API füllt die ODataQueryOptions aus der URI-Abfragezeichenfolge auf. Um die Abfrage anzuwenden, übergeben Sie ein IQueryable an die ApplyTo-Methode . Die -Methode gibt ein weiteres IQueryable zurück.

Wenn Sie in erweiterten Szenarien keinen IQueryable-Abfrageanbieter haben, können Sie die ODataQueryOptions untersuchen und die Abfrageoptionen in eine andere Form übersetzen. (Siehe beispielsweise RaghuRam Nadimintis Blogbeitrag Übersetzen von OData-Abfragen in HQL)

Abfrageüberprüfung

Das [Queryable] -Attribut überprüft die Abfrage, bevor sie ausgeführt wird. Der Validierungsschritt wird in der QueryableAttribute.ValidateQuery-Methode ausgeführt. Sie können auch den Überprüfungsprozess anpassen.

Siehe auch OData-Sicherheitsleitfaden.

Überschreiben Sie zunächst eine der Validierungsklassen, die im Web.Http.OData.Query.Validators-Namespace definiert ist. Beispielsweise deaktiviert die folgende Validierungsklasse die Option "desc" für die 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);
    }
}

Unterklasse des [Queryable] -Attributs, um die ValidateQuery-Methode außer Kraft zu setzen.

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

Legen Sie dann Ihr benutzerdefiniertes Attribut entweder global oder pro Controller fest:

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

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

Wenn Sie ODataQueryOptions direkt verwenden, legen Sie die Validierung für die Folgenden Optionen fest:

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