Podpora akcí OData ve webovém rozhraní API ASP.NET 2

Mike Wasson

Stažení dokončeného projektu

V OData představují akce způsob, jak přidat chování na straně serveru, které není snadné definovat jako operace CRUD u entit. Mezi použití akcí patří:

  • Implementace složitých transakcí
  • Manipulace s několika entitami najednou
  • Povolení aktualizací pouze určitých vlastností entity
  • Odesílání informací na server, které nejsou definovány v entitě.

Verze softwaru použité v kurzu

  • Webové rozhraní API 2
  • OData verze 3
  • Entity Framework 6

Příklad: Hodnocení produktu

V tomto příkladu chceme uživatelům umožnit hodnocení produktů a zveřejnění průměrného hodnocení jednotlivých produktů. V databázi uložíme seznam hodnocení s klíči na základě produktů.

Tady je model, který můžeme použít k reprezentaci hodnocení v Entity Frameworku:

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

Nechceme ale, aby klienti post objektu ProductRating do kolekce Hodnocení. Intuitivně se hodnocení přidružuje ke kolekci Products a klientovi by mělo vyžadovat pouze odeslání hodnoty hodnocení.

Proto místo použití normálních operací CRUD definujeme akci, kterou klient může vyvolat u produktu. V terminologii OData je akce svázaná s entitami Product.

Akce mají vedlejší účinky na server. Z tohoto důvodu se vyvolávají pomocí požadavků HTTP POST. Akce můžou mít parametry a návratové typy, které jsou popsané v metadatech služby. Klient odešle parametry v textu požadavku a server odešle návratovou hodnotu v těle odpovědi. K vyvolání akce "Ohodnotit produkt" klient odešle POST na identifikátor URI, který vypadá takto:

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

Data v požadavku POST jsou jednoduše hodnocení produktu:

{"Rating":2}

Deklarace akce v modelu Entity Data Model

V konfiguraci webového rozhraní API přidejte akci do modelu EDM (Entity Data Model):

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

Tento kód definuje "RateProduct" jako akci, kterou lze provést s entitami Produktu. Také deklaruje, že akce přijme parametr int s názvem "Rating" a vrátí hodnotu int .

Přidání akce do kontroleru

Akce RateProduct je svázaná s entitami Produktu. Pokud chcete akci implementovat, přidejte do kontroleru Products metodu s názvem RateProduct :

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

Všimněte si, že název metody odpovídá názvu akce v EDM. Metoda má dva parametry:

  • key: Klíč produktu, který má být ohodnocený.
  • parameters: Slovník hodnot parametrů akce.

Pokud používáte výchozí konvence směrování, musí mít parametr klíče název "key". Je také důležité zahrnout atribut [FromOdataUri], jak je znázorněno níže. Tento atribut říká webovému rozhraní API, aby při analýze klíče z identifikátoru URI požadavku použilo pravidla syntaxe OData.

K získání parametrů akce použijte slovník parametrů :

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

Pokud klient odešle parametry akce ve správném formátu, hodnota ModelState.IsValid je true. V takovém případě můžete k získání hodnot parametrů použít slovník ODataActionParameters . V tomto příkladu RateProduct akce přijme jeden parametr s názvem "Rating".

Metadata akcí

Pokud chcete zobrazit metadata služby, odešlete požadavek GET na adresu /odata/$metadata. Tady je část metadat, která deklaruje RateProduct akci:

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

FunctionImport Element deklaruje akci. Většina polí je srozumitelná, ale stojí za zmínku dvě:

  • IsBindable znamená, že akci je možné vyvolat na cílové entitě, aspoň někdy.
  • IsAlwaysBindable znamená, že akci lze vždy vyvolat u cílové entity.

Rozdíl spočívá v tom, že některé akce jsou klientům vždy dostupné, ale jiné akce můžou záviset na stavu entity. Předpokládejme například, že definujete akci Nákup. Můžete koupit pouze položku, která je na skladě. Pokud je položka na skladě, klient nemůže tuto akci vyvolat.

Když definujete EDM, metoda Action vytvoří akci s možností trvalé vazby:

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

O akcích, které nelze vždy svázat (označované také jako přechodné akce) probereme později v tomto tématu.

Vyvolání akce

Teď se podíváme, jak klient vyvolá tuto akci. Předpokládejme, že klient chce produktu udělit hodnocení 2 s ID = 4. Tady je příklad zprávy požadavku, která pro text požadavku používá formát JSON:

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

{"Rating":2}

Tady je zpráva s odpovědí:

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
}

Svázání akce se sadou entit

V předchozím příkladu je akce vázaná na jednu entitu: Klient ocejuje jeden produkt. Akci můžete také svázat s kolekcí entit. Stačí provést následující změny:

V EDM přidejte akci do vlastnosti Kolekce entity.

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

V metodě kontroleru vyněžte parametr klíče .

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

Klient teď vyvolá akci pro sadu entit Products:

http://localhost/odata/Products/RateAllProducts

Akce s parametry kolekce

Akce můžou mít parametry, které přebírají kolekci hodnot. V EDM pomocí CollectionParameter<T> deklarujte parametr .

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

Tím se deklaruje parametr s názvem Ratings, který přijímá kolekci hodnot int . V metodě kontroleru stále získáte hodnotu parametru z objektu ODataActionParameters, ale teď je hodnota int> ICollection<:

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

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

    // ...
}

Přechodné akce

V příkladu RateProduct můžou uživatelé vždy ohodnotit produkt, takže akce je vždy dostupná. Některé akce ale závisejí na stavu entity. Například ve službě videopůjčoven není akce "Rezervovat" vždy dostupná. (Záleží na tom, jestli je k dispozici kopie tohoto videa.) Tento typ akce se nazývá přechodná akce.

V metadatech služby má přechodná akce hodnotu IsAlwaysBindable rovno false. Ve skutečnosti je to výchozí hodnota, takže metadata budou vypadat takto:

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

Tady je důvod, proč na tom záleží: Pokud je akce přechodná, musí server klientovi sdělit, kdy je tato akce dostupná. Provede to tak, že do entity zahrne odkaz na akci. Tady je příklad entity Film:

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

Vlastnost "#CheckOut" obsahuje odkaz na akci Rezervovat. Pokud akce není k dispozici, server odkaz vynechá.

Pokud chcete deklarovat přechodnou akci v EDM, zavolejte metodu TransientAction :

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

Musíte také poskytnout funkci, která vrátí odkaz akce pro danou entitu. Nastavte tuto funkci voláním HasActionLink. Funkci můžete napsat jako výraz lambda:

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

Pokud je akce dostupná, výraz lambda vrátí odkaz na akci. Serializátor OData obsahuje tento odkaz, když serializuje entitu. Pokud akce není k dispozici, funkce vrátí null.

Další materiály