Freigeben über


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

von Mike Wasson

Abgeschlossenes Projekt herunterladen

In OData stellen Aktionen eine Möglichkeit dar, serverseitige Verhaltensweisen hinzuzufügen, die nicht einfach als CRUD-Vorgänge für Entitäten definiert werden. Einige Verwendungsmöglichkeiten für Aktionen umfassen:

  • Implementieren komplexer Transaktionen.
  • Gleichzeitiges Manipulieren mehrerer Entitäten.
  • Aktualisierungen nur für bestimmte Eigenschaften einer Entität zulassen.
  • Senden von Informationen an den Server, der nicht in einer Entität definiert ist.

Im Lernprogramm verwendete Softwareversionen

  • Web-API 2
  • OData Version 3
  • Entity Framework 6

Beispiel: Bewertung eines Produkts

In diesem Beispiel möchten wir Benutzern das Bewerten von Produkten ermöglichen und dann die durchschnittlichen Bewertungen für jedes Produkt verfügbar machen. In der Datenbank speichern wir eine Liste der Bewertungen, die auf Produkte zugeschnitten sind.

Nachfolgend sehen Sie das Modell, das wir verwenden können, um die Bewertungen in Entity Framework darzustellen:

public class ProductRating
{
    public int ID { get; set; }

    [ForeignKey("Product")]
    public int ProductID { get; set; }
    public virtual Product Product { get; set; }  // Navigation property

    public int Rating { get; set; }
}

Wir möchten jedoch nicht, dass Clients ein ProductRating Objekt in eine "Ratings"-Auflistung posten. Die Bewertung ist intuitiv mit der Produktesammlung verknüpft, und der Kunde sollte nur den Bewertungswert posten müssen.

Daher definieren wir anstelle der normalen CRUD-Vorgänge eine Aktion, die ein Client für ein Produkt aufrufen kann. In der OData-Terminologie ist die Aktion an Produktentitäten gebunden .

Aktionen haben Nebenwirkungen auf dem Server. Aus diesem Grund werden sie mithilfe von HTTP POST-Anforderungen aufgerufen. Aktionen können Parameter und Rückgabetypen aufweisen, die in den Dienstmetadaten beschrieben werden. Der Client sendet die Parameter im Anforderungstext, und der Server sendet den Rückgabewert im Antworttext. Um die Aktion "Produkt bewerten" aufzurufen, sendet der Client einen POST an einen URI wie den folgenden:

http://localhost/odata/Products(1)/RateProduct

Die Daten in der POST-Anforderung sind einfach die Produktbewertung:

{"Rating":2}

Deklarieren der Aktion im Entitätsdatenmodell

Fügen Sie in Ihrer Web-API-Konfiguration die Aktion zum Entitätsdatenmodell (EDM) hinzu:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        builder.EntitySet<Supplier>("Suppliers");
        builder.EntitySet<ProductRating>("Ratings");

        // New code: Add an action to the EDM, and define the parameter and return type.
        ActionConfiguration rateProduct = builder.Entity<Product>().Action("RateProduct");
        rateProduct.Parameter<int>("Rating");
        rateProduct.Returns<double>();

        config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
    }
}

Dieser Code definiert "RateProduct" als Aktion, die für Produktentitäten ausgeführt werden kann. Außerdem wird deklariert, dass die Aktion einen Int-Parameter namens "Rating" ausführt und einen Int-Wert zurückgibt.

Hinzufügen der Aktion zum Controller

Die Aktion "RateProduct" ist an Produktentitäten gebunden. Um die Aktion zu implementieren, fügen Sie dem Products-Controller eine Methode mit dem Namen RateProduct hinzu.

[HttpPost]
public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    int rating = (int)parameters["Rating"];

    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }

    product.Ratings.Add(new ProductRating() { Rating = rating });
    db.SaveChanges();

    double average = product.Ratings.Average(x => x.Rating);

    return Ok(average);
}

Beachten Sie, dass der Methodenname dem Namen der Aktion im EDM entspricht. Die Methode hat zwei Parameter:

  • key: Der Schlüssel für das zu bewertende Produkt.
  • Parameter: Ein Wörterbuch mit Aktionsparameterwerten.

Wenn Sie die Standardroutingkonventionen verwenden, muss der Schlüsselparameter den Namen "key" haben. Es ist auch wichtig, das [FromOdataUri] -Attribut wie dargestellt einzuschließen. Dieses Attribut weist die Web-API an, OData-Syntaxregeln zu verwenden, wenn er den Schlüssel aus dem Anforderungs-URI analysiert.

Verwenden Sie das Parameterwörterbuch , um die Aktionsparameter abzurufen:

if (!ModelState.IsValid)
{
    return BadRequest();
}
int rating = (int)parameters["Rating"];

Wenn der Client die Aktionsparameter im richtigen Format sendet, ist der Wert von ModelState.IsValid true. In diesem Fall können Sie das ODataActionParameters-Wörterbuch verwenden, um die Parameterwerte abzurufen. In diesem Beispiel übernimmt die RateProduct Aktion einen einzelnen Parameter mit dem Namen "Rating".

Aktionsmetadaten

Um die Dienstmetadaten anzuzeigen, senden Sie eine GET-Anforderung an /odata/$metadata. Dies ist der Teil der Metadaten, der die RateProduct Aktion deklariert:

<FunctionImport Name="RateProduct" m:IsAlwaysBindable="true" IsBindable="true" ReturnType="Edm.Double">
  <Parameter Name="bindingParameter" Type="ProductService.Models.Product"/>
  <Parameter Name="Rating" Nullable="false" Type="Edm.Int32"/>
</FunctionImport>

Das FunctionImport-Element deklariert die Aktion. Die meisten Felder sind selbsterklärend, aber zwei sind erwähnenswert:

  • IsBindable bedeutet, dass die Aktion auf die Zielentität zumindest teilweise angewendet werden kann.
  • IsAlwaysBindable bedeutet, dass die Aktion immer für die Zielentität aufgerufen werden kann.

Der Unterschied besteht darin, dass einige Aktionen immer für Clients verfügbar sind, andere Aktionen können jedoch vom Status der Entität abhängen. Angenommen, Sie definieren eine "Kauf"-Aktion. Sie können nur einen Artikel kaufen, der auf Lager ist. Wenn das Element nicht mehr vorrätig ist, kann ein Client diese Aktion nicht aufrufen.

Wenn Sie EDM definieren, erstellt die Action-Methode eine immer bindungsfähige Aktion:

builder.Entity<Product>().Action("RateProduct"); // Always bindable

Ich werde über nicht immer bindungsfähige Aktionen (auch als vorübergehende Aktionen bezeichnet) weiter unten in diesem Thema sprechen.

Aufrufen der Aktion

Sehen wir uns nun an, wie ein Client diese Aktion aufrufen würde. Angenommen, der Kunde möchte dem Produkt mit der ID = 4 eine Bewertung von 2 geben. Hier ist eine Beispielanforderungsnachricht, die das JSON-Format für den Anforderungstext verwendet:

POST http://localhost/odata/Products(4)/RateProduct HTTP/1.1
Content-Type: application/json
Content-Length: 12

{"Rating":2}

Hier ist die Antwortnachricht:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DataServiceVersion: 3.0
Date: Tue, 22 Oct 2013 19:04:00 GMT
Content-Length: 89

{
  "odata.metadata":"http://localhost:21900/odata/$metadata#Edm.Double","value":2.75
}

Binden einer Aktion an einen Entitätssatz

Im vorherigen Beispiel ist die Aktion an eine einzelne Entität gebunden: Der Client bewertet ein einzelnes Produkt. Sie können eine Aktion auch an eine Sammlung von Entitäten binden. Nehmen Sie einfach die folgenden Änderungen vor:

Fügen Sie im EDM die Aktion zur Sammlungseigenschaft der Entität hinzu.

var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");

Lassen Sie in der Controllermethode den Schlüsselparameter weg.

[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
    // ....
}

Jetzt ruft der Client die Aktion für den Entitätssatz "Products" auf:

http://localhost/odata/Products/RateAllProducts

Aktionen mit Sammlungsparametern

Aktionen können Parameter haben, die eine Sammlung von Werten annehmen. Verwenden Sie im EDM CollectionParameter<T> , um den Parameter zu deklarieren.

rateAllProducts.CollectionParameter<int>("Ratings");

Dadurch wird ein Parameter namens "Ratings" deklariert, der eine Sammlung von Int-Werten verwendet. In der Controllermethode erhalten Sie weiterhin den Parameterwert aus dem ODataActionParameters-Objekt, aber jetzt ist der Wert ein ICollection-Int-Wert<>:

[HttpPost]
public void RateAllProducts(ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }

    var ratings = parameters["Ratings"] as ICollection<int>; 

    // ...
}

Vorübergehende Aktionen

Im Beispiel "RateProduct" können Benutzer ein Produkt immer bewerten, sodass die Aktion immer verfügbar ist. Einige Aktionen hängen jedoch vom Status der Entität ab. Beispielsweise ist in einem Videoverleihdienst die Aktion "Auschecken" nicht immer verfügbar. (Es hängt davon ab, ob eine Kopie dieses Videos verfügbar ist.) Diese Art von Aktion wird als vorübergehende Aktion bezeichnet.

In den Dienstmetadaten hat eine vorübergehende Aktion IsAlwaysBindable gleich "false". Das ist tatsächlich der Standardwert, sodass die Metadaten wie folgt aussehen:

<FunctionImport Name="CheckOut" IsBindable="true">
    <Parameter Name="bindingParameter" Type="ProductsService.Models.Product" />
</FunctionImport>

Dies ist wichtig: Wenn eine Aktion vorübergehend ist, muss der Server dem Client mitteilen, wann die Aktion verfügbar ist. Dazu wird ein Link auf die Aktion in die Entität eingefügt. Hier ist ein Beispiel für eine Filmentität:

{
  "odata.metadata":"http://localhost:17916/odata/$metadata#Movies/@Element",
  "#CheckOut":{ "target":"http://localhost:17916/odata/Movies(1)/CheckOut" },
  "ID":1,"Title":"Sudden Danger 3","Year":2012,"Genre":"Action"
}

Die Eigenschaft "#CheckOut" enthält einen Link zur CheckOut-Aktion. Wenn die Aktion nicht verfügbar ist, wird der Link vom Server weggelassen.

Rufen Sie die TransientAction-Methode auf, um eine vorübergehende Aktion im EDM zu deklarieren:

var checkoutAction = builder.Entity<Movie>().TransientAction("CheckOut");

Außerdem müssen Sie eine Funktion bereitstellen, die einen Aktionslink für eine bestimmte Entität zurückgibt. Legen Sie diese Funktion fest, indem Sie HasActionLink aufrufen. Sie können die Funktion als Lambda-Ausdruck schreiben:

checkoutAction.HasActionLink(ctx =>
{
    var movie = ctx.EntityInstance as Movie;
    if (movie.IsAvailable) {
        return new Uri(ctx.Url.ODataLink(
            new EntitySetPathSegment(ctx.EntitySet), 
            new KeyValuePathSegment(movie.ID.ToString()),
            new ActionPathSegment(checkoutAction.Name)));
    }
    else
    {
        return null;
    }
}, followsConventions: true);

Wenn die Aktion verfügbar ist, gibt der Lambda-Ausdruck einen Link zur Aktion zurück. Der OData-Serializer enthält diesen Link, wenn er die Entität serialisiert. Wenn die Aktion nicht verfügbar ist, gibt die Funktion zurück null.

Zusätzliche Ressourcen