Udostępnij za pośrednictwem


Obsługa akcji OData w interfejsie WEB API 2 ASP.NET

Autor: Mike Wasson

Pobieranie ukończonego projektu

W usłudze OData akcje to sposób dodawania zachowań po stronie serwera, które nie są łatwo zdefiniowane jako operacje CRUD na jednostkach. Niektóre zastosowania akcji to:

  • Implementowanie złożonych transakcji.
  • Manipulowanie kilkoma jednostkami jednocześnie.
  • Zezwalanie na aktualizacje tylko do niektórych 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
  • 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 uwidaczniać średnie oceny dla każdego produktu. W bazie danych będziemy przechowywać listę klasyfikacji z kluczem do produktów.

Oto model, który możemy użyć do reprezentowania ocen w programie 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; }
}

Nie chcemy jednak, aby klienci publikowali ProductRating obiekt w kolekcji "Ratings". Intuicyjnie ocena jest skojarzona z kolekcją Products, a klient powinien tylko opublikować wartość klasyfikacji.

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

Akcje mają skutki uboczne na serwerze. Z tego powodu są one wywoływane przy użyciu żądań HTTP POST. Akcje mogą mieć 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 element POST do identyfikatora URI w następujący sposób:

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

Dane w żądaniu POST to po prostu ocena produktu:

{"Rating":2}

Deklarowanie akcji w modelu danych jednostki

W konfiguracji internetowego interfejsu API dodaj akcję do modelu danych jednostki (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 Product. Deklaruje również, że akcja przyjmuje parametr int o nazwie "Rating" i zwraca wartość int .

Dodawanie akcji do kontrolera

Akcja "RateProduct" jest powiązana z jednostkami Product. Aby zaimplementować akcję, dodaj metodę o nazwie RateProduct do kontrolera Products:

[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 narzędziu EDM. Metoda ma dwa parametry:

  • key: klucz, który ma być oceniany przez produkt.
  • parameters: słownik wartości parametrów akcji.

Jeśli używasz domyślnych konwencji routingu, parametr klucza musi mieć nazwę "key". Należy również uwzględnić atrybut [FromOdataUri], jak pokazano poniżej. Ten atrybut informuje internetowy interfejs API o użyciu reguł składni OData podczas analizowania klucza z identyfikatora URI żądania.

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 poprawnym formacie, wartość ModelState.IsValid ma wartość true. W takim przypadku możesz użyć słownika ODataActionParameters , aby uzyskać wartości parametrów. W tym przykładzie RateProduct akcja przyjmuje pojedynczy parametr o nazwie "Ocena".

Metadane akcji

Aby wyświetlić metadane usługi, wyślij żądanie GET do /odata/$metadata. Oto 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 wymaga wyjaśnienia, ale warto zauważyć dwa:

  • IsBindable oznacza, że akcję można wywołać w jednostce docelowej, co najmniej w pewnym czasie.
  • IsAlwaysBindable oznacza, że akcję można zawsze wywołać 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 definiujesz akcję "Kup". Możesz kupić tylko element, który jest w magazynie. Jeśli element jest niedostępny, klient nie może wywołać tej akcji.

Podczas definiowania EDM metoda Action tworzy akcję zawsze powiązaną:

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

W dalszej części tego tematu omówię nie zawsze powiązane akcje (nazywane również akcjami przejściowymi ).

Wywoływanie akcji

Teraz zobaczmy, jak klient wywoła tę akcję. Załóżmy, że klient chce nadać produktowi ocenę 2 z identyfikatorem = 4. Oto przykładowy komunikat żądania korzystający z 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 ocenia pojedynczy produkt. Można również powiązać akcję z kolekcją jednostek. Po prostu wprowadź następujące zmiany:

W narzędziu EDM dodaj akcję do właściwości Collection 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ę w zestawie jednostek Products:

http://localhost/odata/Products/RateAllProducts

Akcje z parametrami kolekcji

Akcje mogą mieć parametry, które przyjmują kolekcję wartości. W EDM użyj parametru CollectionParameter<T> , aby zadeklarować parametr .

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

Spowoduje to zadeklarowanie parametru o nazwie "Ratings", który pobiera kolekcję wartości int . W metodzie kontrolera nadal uzyskujesz wartość parametru z obiektu ODataActionParameters, ale teraz wartość jest wartością int> ICollection<:

[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 mogą zawsze oceniać produkt, więc akcja jest zawsze dostępna. Jednak niektóre akcje zależą od stanu jednostki. Na przykład w usłudze wypożyczania wideo akcja "CheckOut" nie zawsze jest dostępna. (Zależy to od tego, czy jest dostępna kopia tego filmu wideo). Ten typ akcji jest nazywany akcją przejściową .

W metadanych usługi akcja przejściowa ma wartość IsAlwaysBindable równą false. Jest to 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 ma to znaczenie: jeśli akcja jest przejściowa, serwer musi poinformować klienta o dostępności akcji. Robi to przez dołączenie linku do akcji w jednostce. Oto przykład jednostki Movie:

{
  "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 CheckOut. Jeśli akcja jest niedostępna, serwer pomija link.

Aby zadeklarować akcję przejściową w module 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 funkcję 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 podczas serializacji jednostki. Gdy akcja jest niedostępna, funkcja zwraca wartość null.

Dodatkowe zasoby