Prise en charge des actions OData dans API Web ASP.NET 2

par Mike Wasson

Télécharger le projet terminé

Dans OData, les actions permettent d’ajouter des comportements côté serveur qui ne sont pas facilement définis en tant qu’opérations CRUD sur des entités. Voici quelques utilisations des actions :

  • Implémentation de transactions complexes.
  • Manipulation de plusieurs entités à la fois.
  • Autorisation des mises à jour uniquement pour certaines propriétés d’une entité.
  • Envoi d’informations au serveur qui ne sont pas définies dans une entité.

Versions logicielles utilisées dans le tutoriel

  • API web 2
  • OData version 3
  • Entity Framework 6

Exemple : Évaluation d’un produit

Dans cet exemple, nous voulons permettre aux utilisateurs d’évaluer les produits, puis d’exposer les évaluations moyennes pour chaque produit. Dans la base de données, nous allons stocker une liste d’évaluations, avec clé sur les produits.

Voici le modèle que nous pouvons utiliser pour représenter les évaluations dans 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; }
}

Mais nous ne voulons pas que les clients publient un ProductRating objet dans une collection « Ratings ». Intuitivement, l’évaluation est associée à la collection Products, et le client doit uniquement publier la valeur d’évaluation.

Par conséquent, au lieu d’utiliser les opérations CRUD normales, nous définissons une action qu’un client peut appeler sur un produit. Dans la terminologie OData, l’action est liée aux entités Product.

Les actions ont des effets secondaires sur le serveur. Pour cette raison, ils sont appelés à l’aide de requêtes HTTP POST. Les actions peuvent avoir des paramètres et des types de retour, qui sont décrits dans les métadonnées du service. Le client envoie les paramètres dans le corps de la demande, et le serveur envoie la valeur de retour dans le corps de la réponse. Pour appeler l’action « Évaluer le produit », le client envoie un message POST à un URI comme suit :

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

Les données de la requête POST sont simplement l’évaluation du produit :

{"Rating":2}

Déclarer l’action dans le modèle de données d’entité

Dans votre configuration d’API web, ajoutez l’action au modèle de données d’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());
    }
}

Ce code définit « RateProduct » comme une action qui peut être effectuée sur les entités Product. Il déclare également que l’action prend un paramètre int nommé « Rating » et retourne une valeur int .

Ajouter l’action au contrôleur

L’action « RateProduct » est liée aux entités Product. Pour implémenter l’action, ajoutez une méthode nommée RateProduct au contrôleur 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);
}

Notez que le nom de la méthode correspond au nom de l’action dans l’EDM. La méthode a deux paramètres :

  • key : clé pour le produit à évaluer.
  • parameters : dictionnaire de valeurs de paramètre d’action.

Si vous utilisez les conventions de routage par défaut, le paramètre de clé doit être nommé « key ». Il est également important d’inclure l’attribut [FromOdataUri], comme indiqué. Cet attribut indique à l’API web d’utiliser des règles de syntaxe OData lorsqu’elle analyse la clé à partir de l’URI de la requête.

Utilisez le dictionnaire de paramètres pour obtenir les paramètres d’action :

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

Si le client envoie les paramètres d’action dans le format approprié, la valeur de ModelState.IsValid est true. Dans ce cas, vous pouvez utiliser le dictionnaire ODataActionParameters pour obtenir les valeurs des paramètres. Dans cet exemple, l’action RateProduct prend un seul paramètre nommé « Rating ».

Métadonnées d’action

Pour afficher les métadonnées du service, envoyez une requête GET à /odata/$metadata. Voici la partie des métadonnées qui déclare l’action 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’élément FunctionImport déclare l’action. La plupart des champs sont explicites, mais deux sont à noter :

  • IsBindable signifie que l’action peut être appelée sur l’entité cible, au moins une partie du temps.
  • IsAlwaysBindable signifie que l’action peut toujours être appelée sur l’entité cible.

La différence est que certaines actions sont toujours disponibles pour les clients, mais d’autres peuvent dépendre de l’état de l’entité. Par exemple, supposons que vous définissiez une action « Achat ». Vous ne pouvez acheter qu’un article en stock. Si l’article est en rupture de stock, un client ne peut pas appeler cette action.

Lorsque vous définissez l’EDM, la méthode Action crée une action toujours lié :

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

Je parlerai des actions qui ne sont pas toujours liées (également appelées actions temporaires ) plus loin dans cette rubrique.

Appel de l’action

Voyons maintenant comment un client appelle cette action. Supposons que le client souhaite attribuer une note de 2 au produit avec l’ID = 4. Voici un exemple de message de demande, utilisant le format JSON pour le corps de la demande :

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

{"Rating":2}

Voici le message de réponse :

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
}

Liaison d’une action à un jeu d’entités

Dans l’exemple précédent, l’action est liée à une seule entité : le client évalue un seul produit. Vous pouvez également lier une action à une collection d’entités. Apportez simplement les modifications suivantes :

Dans l’EDM, ajoutez l’action à la propriété Collection de l’entité.

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

Dans la méthode du contrôleur, omettez le paramètre key .

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

À présent, le client appelle l’action sur l’ensemble d’entités Products :

http://localhost/odata/Products/RateAllProducts

Actions avec des paramètres de collection

Les actions peuvent avoir des paramètres qui prennent une collection de valeurs. Dans l’EDM, utilisez CollectionParameter<T> pour déclarer le paramètre.

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

Cela déclare un paramètre nommé « Ratings » qui prend une collection de valeurs int . Dans la méthode du contrôleur, vous obtenez toujours la valeur du paramètre à partir de l’objet ODataActionParameters, mais la valeur est maintenant une valeur int> ICollection< :

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

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

    // ...
}

Actions temporaires

Dans l’exemple « RateProduct », les utilisateurs peuvent toujours évaluer un produit, de sorte que l’action est toujours disponible. Toutefois, certaines actions dépendent de l’état de l’entité. Par exemple, dans un service de location de vidéos, l’action « CheckOut » n’est pas toujours disponible. (Cela dépend si une copie de cette vidéo est disponible.) Ce type d’action est appelé action temporaire .

Dans les métadonnées de service, une action temporaire a IsAlwaysBindable égal à false. Il s’agit en fait de la valeur par défaut, de sorte que les métadonnées ressemblent à ceci :

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

Voici pourquoi cela est important : si une action est temporaire, le serveur doit indiquer au client quand l’action est disponible. Pour ce faire, il inclut un lien vers l’action dans l’entité. Voici un exemple pour une 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 propriété « #CheckOut » contient un lien vers l’action CheckOut. Si l’action n’est pas disponible, le serveur omet le lien.

Pour déclarer une action temporaire dans l’EDM, appelez la méthode TransientAction :

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

En outre, vous devez fournir une fonction qui retourne un lien d’action pour une entité donnée. Définissez cette fonction en appelant HasActionLink. Vous pouvez écrire la fonction en tant qu’expression 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);

Si l’action est disponible, l’expression lambda retourne un lien vers l’action. Le sérialiseur OData inclut ce lien lorsqu’il sérialise l’entité. Lorsque l’action n’est pas disponible, la fonction retourne null.

Ressources supplémentaires