Condividi tramite


Supporto delle azioni OData in API Web ASP.NET 2

di Mike Wasson

Scaricare il progetto completato

In OData le azioni sono un modo per aggiungere comportamenti lato server che non sono facilmente definiti come operazioni CRUD sulle entità. Alcuni usi per le azioni includono:

  • Implementazione di transazioni complesse.
  • Modifica di più entità contemporaneamente.
  • Consentire gli aggiornamenti solo a determinate proprietà di un'entità.
  • Invio di informazioni al server non definito in un'entità.

Versioni software usate nell'esercitazione

  • API Web 2
  • OData versione 3
  • Entity Framework 6

Esempio: Classificazione di un prodotto

In questo esempio si vuole consentire agli utenti di valutare i prodotti e quindi esporre le valutazioni medie per ogni prodotto. Nel database verrà archiviato un elenco di classificazioni, con chiave per i prodotti.

Ecco il modello che è possibile usare per rappresentare le classificazioni in 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; }
}

Ma non si vuole che i client postino un ProductRating oggetto a una raccolta "Ratings". Intuitivamente, la classificazione è associata alla raccolta Products e il client deve solo pubblicare il valore di classificazione.

Pertanto, invece di usare le normali operazioni CRUD, viene definita un'azione che un client può richiamare su un prodotto. Nella terminologia OData l'azione è associata alle entità Product.

Le azioni hanno effetti collaterali sul server. Per questo motivo, vengono richiamati usando le richieste HTTP POST. Le azioni possono avere parametri e tipi restituiti, descritti nei metadati del servizio. Il client invia i parametri nel corpo della richiesta e il server invia il valore restituito nel corpo della risposta. Per richiamare l'azione "Rate Product", il client invia un POST a un URI simile al seguente:

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

I dati nella richiesta POST sono semplicemente la classificazione del prodotto:

{"Rating":2}

Dichiarare l'azione nel modello di dati dell'entità

Nella configurazione dell'API Web aggiungere l'azione al modello di dati dell'entità (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());
    }
}

Questo codice definisce "RateProduct" come azione che può essere eseguita sulle entità Product. Dichiara inoltre che l'azione accetta un parametro int denominato "Rating" e restituisce un valore int .

Aggiungere l'azione al controller

L'azione "RateProduct" è associata alle entità Product. Per implementare l'azione, aggiungere un metodo denominato RateProduct al controller 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);
}

Si noti che il nome del metodo corrisponde al nome dell'azione in EDM. Il metodo ha due parametri:

  • key: chiave per il prodotto da valutare.
  • parameters: dizionario dei valori dei parametri azione.

Se si usano le convenzioni di routing predefinite, il parametro chiave deve essere denominato "key". È anche importante includere l'attributo [FromOdataUri] , come illustrato. Questo attributo indica all'API Web di usare le regole di sintassi OData quando analizza la chiave dall'URI della richiesta.

Usare il dizionario dei parametri per ottenere i parametri di azione:

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

Se il client invia i parametri azione nel formato corretto, il valore di ModelState.IsValid è true. In tal caso, è possibile usare il dizionario ODataActionParameters per ottenere i valori dei parametri. In questo esempio, l'azione RateProduct accetta un singolo parametro denominato "Rating".

Metadati azione

Per visualizzare i metadati del servizio, inviare una richiesta GET a /odata/$metadata. Ecco la parte dei metadati che dichiara l'azione RateProduct :

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

L'elemento FunctionImport dichiara l'azione. La maggior parte dei campi è autoesplicativa, ma ne vale la pena notare due:

  • IsBindable indica che l'azione può essere richiamata sull'entità di destinazione, almeno una parte del tempo.
  • IsAlwaysBindable indica che l'azione può sempre essere richiamata sull'entità di destinazione.

La differenza è che alcune azioni sono sempre disponibili per i client, ma altre azioni possono dipendere dallo stato dell'entità. Si supponga, ad esempio, di definire un'azione "Acquista". È possibile acquistare solo un articolo in magazzino. Se l'articolo non è disponibile, un client non può richiamare tale azione.

Quando si definisce EDM, il metodo Action crea un'azione associabile sempre:

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

Più avanti in questo argomento si parlerà di azioni non sempre associabili (denominate anche azioni temporanee ).

Richiamo dell'azione

Si vedrà ora come un client richiamerà questa azione. Si supponga che il client voglia assegnare una valutazione pari a 2 al prodotto con ID = 4. Ecco un messaggio di richiesta di esempio, usando il formato JSON per il corpo della richiesta:

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

{"Rating":2}

Ecco il messaggio di risposta:

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
}

Associazione di un'azione a un set di entità

Nell'esempio precedente, l'azione è associata a una singola entità: il client esegue la tariffa di un singolo prodotto. È anche possibile associare un'azione a una raccolta di entità. È sufficiente apportare le modifiche seguenti:

In EDM aggiungere l'azione alla proprietà Collection dell'entità.

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

Nel metodo controller omettere il parametro key .

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

Il client richiama ora l'azione sul set di entità Products:

http://localhost/odata/Products/RateAllProducts

Azioni con parametri di raccolta

Le azioni possono avere parametri che accettano una raccolta di valori. In EDM usare CollectionParameter<T> per dichiarare il parametro .

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

In questo modo viene dichiarato un parametro denominato "Ratings" che accetta una raccolta di valori int . Nel metodo controller si ottiene comunque il valore del parametro dall'oggetto ODataActionParameters, ma ora il valore è un valore int> ICollection<:

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

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

    // ...
}

Azioni temporanee

Nell'esempio "RateProduct" gli utenti possono sempre valutare un prodotto, quindi l'azione è sempre disponibile. Alcune azioni dipendono tuttavia dallo stato dell'entità. Ad esempio, in un servizio di noleggio video, l'azione "CheckOut" non è sempre disponibile. Dipende dal fatto che sia disponibile una copia del video. Questo tipo di azione viene chiamato azione temporanea .

Nei metadati del servizio un'azione temporanea ha IsAlwaysBindable uguale a false. Questo è in realtà il valore predefinito, quindi i metadati avranno un aspetto simile al seguente:

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

Ecco perché questo è importante: se un'azione è temporanea, il server deve indicare al client quando l'azione è disponibile. Questa operazione viene eseguita includendo un collegamento all'azione nell'entità . Ecco un esempio per un'entità 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"
}

La proprietà "#CheckOut" contiene un collegamento all'azione CheckOut. Se l'azione non è disponibile, il server omette il collegamento.

Per dichiarare un'azione temporanea in EDM, chiamare il metodo TransientAction :

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

È inoltre necessario fornire una funzione che restituisce un collegamento all'azione per una determinata entità. Impostare questa funzione chiamando HasActionLink. È possibile scrivere la funzione come espressione 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);

Se l'azione è disponibile, l'espressione lambda restituisce un collegamento all'azione. Il serializzatore OData include questo collegamento quando serializza l'entità. Quando l'azione non è disponibile, la funzione restituisce null.

Risorse aggiuntive