Freigeben über


Unterstützen 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. Um beispielsweise die Ergebnisse zu sortieren, verwendet ein Client 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. Dadurch erhalten Sie eine bequeme Möglichkeit, Features wie Filtern und Sortieren zu einer beliebigen Webanwendung hinzuzufügen.

Lesen Sie vor dem Aktivieren von Abfrageoptionen den OData-Sicherheitsleitfaden.

Aktivieren von OData-Abfrageoptionen

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

Auswahl Beschreibung
$expand Erweitert verknüpfte 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 die ersten n ergebnisse zurück.

Um OData-Abfrageoptionen zu verwenden, müssen Sie sie explizit aktivieren. Sie können sie global für die gesamte Anwendung aktivieren 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 Abfrageoptionen für die gesamte Anwendung nicht aktiviert werden sollen, können Sie sie für bestimmte Controlleraktionen aktivieren, indem Sie das [Queryable] -Attribut der Aktionsmethode hinzufügen.

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

Beispielabfragen

In diesem Abschnitt werden die Typen 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.

Client-Driven Paging

Bei großen Entitätssätzen möchte der Client möglicherweise die Anzahl der Ergebnisse einschränken. Ein Client kann z. B. jeweils 10 Einträge mit "next"-Links anzeigen, um die nächste Ergebnisseite abzurufen. Dazu verwendet der Client die Optionen $top und $skip.

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

Die option $top gibt die maximale Anzahl der zurückzugebenden Einträge 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 können Clients die Ergebnisse filtern, indem ein boolescher Ausdruck angewendet wird. Die Filterausdrücke sind ziemlich leistungsfähig; sie umfassen logische und arithmetische Operatoren, Zeichenfolgenfunktionen und Datumsfunktionen.

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

Sortieren

Verwenden Sie zum Sortieren der Ergebnisse den $orderby Filter.

Sortieren Nach Preis. http://localhost/Products?$orderby=Price
Sortieren Sie nach Preis in absteigender Reihenfolge (am höchsten zum niedrigsten Wert). 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

Servergesteuertes 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 einschränken, die er in einer einzigen Antwort sendet. Um die Server paging zu aktivieren, legen Sie die PageSize-Eigenschaft im Queryable-Attribut fest. Der Wert ist die maximale Anzahl der zurückzugebenden Einträge.

[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 Datenseite:

{
  "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 zur nächsten Seite und die Inline-Zählung erfordern beide das OData-Format. Der Grund dafür ist, dass OData spezielle Felder im Antworttext definiert, um den Link und die Anzahl zu enthalten.

Bei Nicht-OData-Formaten ist es weiterhin möglich, Links zur nächsten Seite und inline-Anzahl zu unterstützen, indem die Abfrageergebnisse in ein PageResult<T-Objekt> umbrochen werden. Es ist jedoch etwas mehr Code erforderlich. Hier ist ein 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 ist eine Beispiel-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 menge Kontrolle über die Abfrage, die auf dem Server ausgeführt wird. In einigen Fällen sollten Sie die verfügbaren Optionen aus Sicherheits- oder Leistungsgründen einschränken. Das Attribut [Queryable] verfügt über einige integrierte Eigenschaften dafür. Nachfolgend finden Sie einige Beispiele.

Nur $skip und $top zulassen, um paging und nichts anderes zu unterstützen:

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

Die Sortierung nur nach bestimmten Eigenschaften zulassen, um das Sortieren nach Eigenschaften zu verhindern, die nicht in der Datenbank indiziert sind:

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

Erlaube die logische Funktion "eq", aber keine anderen logischen Funktionen.

[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]

Es dürfen keine arithmetischen Operatoren verwendet werden.

[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]

Sie können Optionen global einschränken, indem Sie eine QueryableAttribute-Instanz 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 Attribut [Queryable] 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 eine IQueryable an die ApplyTo-Methode. Die Methode gibt eine weitere IQueryable zurück.

Für fortgeschrittene Szenarien, wenn Sie nicht über einen IQueryable-Abfrageanbieter verfügen, können Sie die ODataQueryOptions untersuchen und die Abfrageoptionen in eine andere Form übersetzen. (Beispiel: Siehe 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 Überprüfungsschritt wird in der QueryableAttribute.ValidateQuery-Methode ausgeführt. Sie können den Überprüfungsprozess auch anpassen.

Siehe auch OData-Sicherheitsleitfaden.

Überschreiben Sie zunächst eine der validator-Klassen, die im Web.Http.OData.Query.Validators-Namespace definiert sind. Die folgende Validatorklasse deaktiviert z. B. 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 zum Überschreiben der ValidateQuery-Methode .

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 den Validator für die 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>;
}