Unterstützung von OData-Aktionen in ASP.NET-Web-API 2
von Mike Wasson
Abgeschlossenes Projekt herunterladen
In OData sind Aktionen eine Möglichkeit, serverseitige Verhaltensweisen hinzuzufügen, die nicht einfach als CRUD-Vorgänge für Entitäten definiert werden können. Einige Verwendungsmöglichkeiten für Aktionen sind:
- Implementieren komplexer Transaktionen.
- Bearbeiten mehrerer Entitäten gleichzeitig.
- Zulassen von Updates nur für bestimmte Eigenschaften einer Entität.
- Senden von Informationen an den Server, die nicht in einer Entität definiert sind.
Im Tutorial verwendete Softwareversionen
- Web-API 2
- OData Version 3
- Entity Framework 6
Beispiel: Bewertung eines Produkts
In diesem Beispiel möchten wir Benutzern die Möglichkeit geben, Produkte zu bewerten und dann die durchschnittlichen Bewertungen für jedes Produkt verfügbar zu machen. In der Datenbank speichern wir eine Liste von Bewertungen, die für Produkte bestimmt sind.
Hier 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 einer "Ratings"-Auflistung postieren. Intuitiv ist die Bewertung der Products-Auflistung zugeordnet, und der Client sollte nur den Bewertungswert veröffentlichen 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 Nebeneffekte 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-Wert wie folgt an einen URI:
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 die Aktion in Ihrer Web-API-Konfiguration dem Entitätsdatenmodell (Entity Data Model, 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 Product-Entitäten ausgeführt werden kann. Außerdem wird deklariert, dass die Aktion einen int-Parameter namens "Rating" annimmt 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 mit dem Namen der Aktion im EDM übereinstimmt. Die Methode verfügt über zwei Parameter:
- key: Der Schlüssel für das zu bewertende Produkt.
- parameters: 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 gezeigt einzuschließen. Dieses Attribut weist die Web-API an, OData-Syntaxregeln zu verwenden, wenn sie 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 verwendet die RateProduct
Aktion einen einzelnen Parameter namens "Rating".
Aktionsmetadaten
Um die Dienstmetadaten anzuzeigen, senden Sie eine GET-Anforderung an /odata/$metadata. Hier sehen Sie den 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 für die Zielentität aufgerufen werden kann, mindestens zu einem bestimmten Zeitpunkt.
- IsAlwaysBindable bedeutet, dass die Aktion immer für die Zielentität aufgerufen werden kann.
Der Unterschied besteht darin, dass einige Aktionen für Clients immer verfügbar sind, aber andere Aktionen können vom Zustand der Entität abhängen. Angenommen, Sie definieren eine Aktion "Kauf". Sie können nur einen Artikel kaufen, der auf Lager ist. Wenn das Element nicht vorrätig ist, kann ein Client diese Aktion nicht aufrufen.
Wenn Sie den EDM definieren, erstellt die Action-Methode eine immer bindungsfähige Aktion:
builder.Entity<Product>().Action("RateProduct"); // Always bindable
Ich werde weiter unten in diesem Thema über nicht immer bindungsfähige Aktionen (auch als vorübergehende Aktionen bezeichnet) sprechen.
Aufrufen der Aktion
Nun sehen wir uns an, wie ein Client diese Aktion aufrufen würde. Angenommen, der Client möchte dem Produkt eine Bewertung von 2 mit der ID = 4 geben. Hier sehen Sie 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 Antwortmeldung:
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 die Aktion im EDM der Collection-Eigenschaft der Entität hinzu.
var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");
Lassen Sie in der Controllermethode den Schlüsselparameter aus.
[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
// ....
}
Nun ruft der Client die Aktion für den Entitätssatz Products auf:
http://localhost/odata/Products/RateAllProducts
Aktionen mit Sammlungsparametern
Aktionen können Parameter enthalten, die eine Auflistung von Werten annehmen. Verwenden Sie im EDM CollectionParameter<T> , um den Parameter zu deklarieren.
rateAllProducts.CollectionParameter<int>("Ratings");
Dadurch wird ein Parameter mit dem Namen "Ratings" deklariert, der eine Auflistung von int-Werten übernimmt. 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 Zustand der Entität ab. In einem Videoverleihdienst ist die Aktion "CheckOut" beispielsweise 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 weist eine vorübergehende Aktion IsAlwaysBindable gleich false auf. Dies ist tatsächlich der Standardwert, sodass die Metadaten wie folgt aussehen:
<FunctionImport Name="CheckOut" IsBindable="true">
<Parameter Name="bindingParameter" Type="ProductsService.Models.Product" />
</FunctionImport>
Deshalb ist folgendes wichtig: Wenn eine Aktion vorübergehend ist, muss der Server dem Client mitteilen, wann die Aktion verfügbar ist. Dazu wird ein Link zur Aktion in die Entität eingeschlossen. Hier sehen Sie ein Beispiel für eine Movie-Entitä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, lässt der Server den Link aus.
Um eine vorübergehende Aktion im EDM zu deklarieren, rufen Sie die TransientAction-Methode auf :
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 Lambdaausdruck 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 Lambdaausdruck einen Link zur Aktion zurück. Der OData-Serialisierer enthält diesen Link, wenn die Entität serialisiert wird. Wenn die Aktion nicht verfügbar ist, gibt die Funktion zurück null
.