Obsługa działań OData w programie ASP.NET Web API 2

według Jan Wasson

Pobierz ukończony projekt

W protokole OData Akcje są sposobem dodawania zachowań po stronie serwera, które nie są łatwo zdefiniowane jako operacje CRUD na jednostkach. Niektóre zastosowania do akcji obejmują:

  • Implementowanie złożonych transakcji.
  • Jednoczesne manipulowanie kilkoma jednostkami.
  • Zezwalanie na aktualizacje tylko do określonych właściwości jednostki.
  • Wysyłanie informacji do serwera, który nie jest zdefiniowany w jednostce.

Wersje oprogramowania używane w samouczku

  • Internetowy interfejs API 2
  • Usługa OData w wersji 3
  • Entity Framework 6

Przykład: Ocena produktu

W tym przykładzie chcemy umożliwić użytkownikom ocenianie produktów, a następnie uwidocznienie średniej klasyfikacji dla każdego produktu. W bazie danych zostanie przechowana lista ocen, które zostały dostosowane do produktów.

Oto model, którego możemy użyć do reprezentowania klasyfikacji w Entity Framework:

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

Ale nie chcemy, aby klienci OGŁASZAli obiekt ProductRating w kolekcji "ratings". Intuicyjnie, ocena jest skojarzona z kolekcją produkty, a klient powinien potrzebować tylko do opublikowania wartości klasyfikacji.

W związku z tym zamiast używania normalnych operacji CRUD definiujemy akcję, którą klient może wywołać w produkcie. W terminologii OData akcja jest powiązana z jednostkami produktu.

Akcje mają efekty uboczne na serwerze. Z tego powodu są wywoływane przy użyciu żądań HTTP POST. Akcje mogą zawierać parametry i typy zwracane, które są opisane w metadanych usługi. Klient wysyła parametry w treści żądania, a serwer wysyła wartość zwracaną w treści odpowiedzi. Aby wywołać akcję "Oceń produkt", klient wysyła wpis do identyfikatora URI w następujący sposób:

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

Dane w żądaniu POST są po prostu klasyfikacją produktu:

{"Rating":2}

Zadeklaruj akcję w Entity Data Model

W konfiguracji internetowego interfejsu API Dodaj akcję do modelu Entity Data Model (EDM):

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

Ten kod definiuje "RateProduct" jako akcję, którą można wykonać w jednostkach produktu. Deklaruje także, że akcja przyjmuje parametr int o nazwie "Rating" i zwraca wartość int .

Dodaj akcję do kontrolera

Akcja "RateProduct" jest powiązana z jednostkami produktu. Aby zaimplementować akcję, Dodaj metodę o nazwie RateProduct do kontrolera produktów:

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

Zwróć uwagę, że nazwa metody jest zgodna z nazwą akcji w modelu EDM. Metoda ma dwa parametry:

  • klucz: klucz produktu do oceniania.
  • Parametry: słownik wartości parametrów akcji.

Jeśli używasz domyślnych Konwencji routingu, parametr klucza musi mieć nazwę "klucz". Ważne jest również uwzględnienie atrybutu [FromOdataUri] , jak pokazano. Ten atrybut wskazuje, że interfejs API sieci Web ma używać reguł składni OData podczas analizowania klucza z żądania URI.

Użyj słownika parametrów , aby uzyskać parametry akcji:

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

Jeśli klient wysyła parametry akcji w prawidłowym formacie, wartość ModelState. IsValid ma wartość true. W takim przypadku można pobrać wartości parametrów przy użyciu słownika ODataActionParameters . W tym przykładzie akcja RateProduct przyjmuje jeden parametr o nazwie "Rating".

Metadane akcji

Aby wyświetlić metadane usługi, Wyślij żądanie GET do/OData/$metadata. Poniżej znajduje się część metadanych, która deklaruje RateProduct akcję:

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

Element FunctionImport deklaruje akcję. Większość pól nie ma wyjaśnień, ale dwie są następujące:

  • Isbindd oznacza, że akcja może być wywoływana w jednostce docelowej, co najmniej jakiś czas.
  • IsAlwaysBindable oznacza, że akcja zawsze może być wywoływana w jednostce docelowej.

Różnica polega na tym, że niektóre akcje są zawsze dostępne dla klientów, ale inne akcje mogą zależeć od stanu jednostki. Załóżmy na przykład, że zdefiniujesz akcję "Kup". Można kupić tylko element, który znajduje się w magazynie. Jeśli element jest poza magazynem, klient nie może wywołać tej akcji.

Podczas definiowania modelu EDM, Metoda akcji tworzy zawsze powiązane akcje:

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

Porozmawiam w dalszej części tego tematu o akcjach, które nie są zawsze wiązane (nazywane również akcjami przejściowymi ).

Wywoływanie akcji

Teraz zobaczmy, jak klient wywoła tę akcję. Załóżmy, że klient chce przekazać klasyfikację 2 do produktu o IDENTYFIKATORze = 4. Oto przykładowy komunikat żądania, który używa formatu JSON dla treści żądania:

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

{"Rating":2}

Oto komunikat odpowiedzi:

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
}

Wiązanie akcji z zestawem jednostek

W poprzednim przykładzie akcja jest powiązana z pojedynczą jednostką: klient klasyfikuje pojedynczy produkt. Można również powiązać akcję z kolekcją jednostek. Po prostu wprowadź następujące zmiany:

W modelu EDM Dodaj akcję do właściwości kolekcji jednostki.

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

W metodzie kontrolera Pomiń parametr klucza .

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

Teraz klient wywołuje akcję dla zestawu jednostek Products:

http://localhost/odata/Products/RateAllProducts

Akcje z parametrami kolekcji

Akcje mogą mieć parametry, które pobierają kolekcję wartości. W modelu EDM Użyj CollectionParameter<t> , aby zadeklarować parametr.

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

Deklaruje on parametr o nazwie "ratings", który pobiera kolekcję wartości int . W metodzie kontrolera nadal otrzymujesz wartość parametru z obiektu ODataActionParameters , ale teraz wartość jest typu ICollection<int> wartość:

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

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

    // ...
}

Akcje przejściowe

W przykładzie "RateProduct" użytkownicy zawsze mogą ocenić produkt, więc akcja jest zawsze dostępna. Jednak niektóre akcje zależą od stanu jednostki. Na przykład w usłudze najmu wideo akcja "wyewidencjonowywanie" nie zawsze jest dostępna. (Jest to zależne od tego, czy kopia tego wideo jest dostępna). Akcja tego typu jest nazywana akcją przejściową .

W metadanych usługi przejściowa akcja ma IsAlwaysBindable równa false. Jest to rzeczywista wartość domyślna, więc metadane będą wyglądać następująco:

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

Oto dlaczego te kwestie: Jeśli akcja jest przejściowa, serwer musi poinformować klienta, gdy akcja jest dostępna. Robi to poprzez dołączenie linku do akcji w jednostce. Oto przykład dla jednostki filmu:

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

Właściwość "#CheckOut" zawiera link do akcji wyewidencjonowywania. Jeśli akcja jest niedostępna, serwer pomija link.

Aby zadeklarować przejściową akcję w modelu EDM, wywołaj metodę TransientAction :

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

Ponadto należy podać funkcję, która zwraca link akcji dla danej jednostki. Ustaw tę funkcję, wywołując HasActionLink. Funkcję można napisać jako wyrażenie 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);

Jeśli akcja jest dostępna, wyrażenie lambda zwraca link do akcji. Serializator OData zawiera ten link, gdy serializować jednostkę. Gdy akcja jest niedostępna, funkcja zwraca null.

Dodatkowe materiały

Przykład akcji OData