Relations d’entités dans OData v4 à l’aide de API Web ASP.NET 2.2

par Mike Wasson

La plupart des jeux de données définissent les relations entre les entités : les clients ont des commandes ; les livres ont des auteurs; les produits ont des fournisseurs. À l’aide d’OData, les clients peuvent naviguer sur les relations d’entité. En fonction d’un produit, vous pouvez trouver le fournisseur. Vous pouvez également créer ou supprimer des relations. Par exemple, vous pouvez définir le fournisseur d’un produit.

Ce tutoriel montre comment prendre en charge ces opérations dans OData v4 à l’aide de API Web ASP.NET. Le tutoriel s’appuie sur le didacticiel Créer un point de terminaison OData v4 à l’aide de API Web ASP.NET 2.

Versions logicielles utilisées dans le tutoriel

  • API web 2.1
  • OData v4
  • Visual Studio 2017 (téléchargez Visual Studio 2017 ici)
  • Entity Framework 6
  • .NET 4.5

Versions du didacticiel

Pour la version 3 d’OData, consultez Prise en charge des relations d’entité dans OData v3.

Ajouter une entité fournisseur

Notes

Le tutoriel s’appuie sur le didacticiel Créer un point de terminaison OData v4 à l’aide de API Web ASP.NET 2.

Tout d’abord, nous avons besoin d’une entité associée. Ajoutez une classe nommée Supplier dans le dossier Models.

using System.Collections.Generic;

namespace ProductService.Models
{
    public class Supplier
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public ICollection<Product> Products { get; set; }
    }
}

Ajoutez une propriété de navigation à la Product classe :

using System.ComponentModel.DataAnnotations.Schema;

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }

        // New code:    
        [ForeignKey("Supplier")]
        public int? SupplierId { get; set; }
        public virtual Supplier Supplier { get; set; }
    }
}

Ajoutez un nouveau DbSet à la ProductsContext classe, afin qu’Entity Framework inclue la table Supplier dans la base de données.

public class ProductsContext : DbContext
{
    static ProductsContext()
    {
        Database.SetInitializer(new ProductInitializer());
    }

    public DbSet<Product> Products { get; set; }
    // New code:
    public DbSet<Supplier> Suppliers { get; set; }
}

Dans WebApiConfig.cs, ajoutez un ensemble d’entités « Suppliers » au modèle de données d’entité :

public static void Register(HttpConfiguration config)
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Product>("Products");
    // New code:
    builder.EntitySet<Supplier>("Suppliers");
    config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
}

Ajouter un contrôleur de fournisseurs

Ajoutez une SuppliersController classe au dossier Controllers.

using ProductService.Models;
using System.Linq;
using System.Web.OData;

namespace ProductService.Controllers
{
    public class SuppliersController : ODataController
    {
        ProductsContext db = new ProductsContext();

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

Je ne montre pas comment ajouter des opérations CRUD pour ce contrôleur. Les étapes sont les mêmes que pour le contrôleur Products (consultez Créer un point de terminaison OData v4).

Pour obtenir le fournisseur d’un produit, le client envoie une demande GET :

GET /Products(1)/Supplier

Pour prendre en charge cette demande, ajoutez la méthode suivante à la ProductsController classe :

public class ProductsController : ODataController
{
    // GET /Products(1)/Supplier
    [EnableQuery]
    public SingleResult<Supplier> GetSupplier([FromODataUri] int key)
    {
        var result = db.Products.Where(m => m.Id == key).Select(m => m.Supplier);
        return SingleResult.Create(result);
    }
 
   // Other controller methods not shown.
}

Cette méthode utilise une convention de nommage par défaut

  • Nom de la méthode : GetX, où X est la propriété de navigation.
  • Nom du paramètre : clé

Si vous suivez cette convention de nommage, l’API web mappe automatiquement la requête HTTP à la méthode du contrôleur.

Exemple de requête HTTP :

GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

Exemple de réponse HTTP :

HTTP/1.1 200 OK
Content-Length: 125
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 00:44:27 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Suppliers/$entity","Id":2,"Name":"Wingtip Toys"
}

Dans l’exemple précédent, un produit a un fournisseur. Une propriété de navigation peut également retourner une collection. Le code suivant obtient les produits d’un fournisseur :

public class SuppliersController : ODataController
{
    // GET /Suppliers(1)/Products
    [EnableQuery]
    public IQueryable<Product> GetProducts([FromODataUri] int key)
    {
        return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products);
    }

    // Other controller methods not shown.
}

Dans ce cas, la méthode retourne un IQueryable au lieu d’un SingleResult<T>

Exemple de requête HTTP :

GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

Exemple de réponse HTTP :

HTTP/1.1 200 OK
Content-Length: 372
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 01:06:54 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Products","value":[
    {
      "Id":1,"Name":"Hat","Price":14.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":2,"Name":"Socks","Price":6.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":4,"Name":"Pogo Stick","Price":29.99,"Category":"Toys","SupplierId":2
    }
  ]
}

Création d’une relation entre des entités

OData prend en charge la création ou la suppression de relations entre deux entités existantes. Dans la terminologie OData v4, la relation est une « référence ». (Dans OData v3, la relation a été appelée lien. Les différences de protocole n’ont pas d’importance pour ce tutoriel.)

Une référence a son propre URI, sous la forme /Entity/NavigationProperty/$ref. Par exemple, voici l’URI pour traiter la référence entre un produit et son fournisseur :

http:/host/Products(1)/Supplier/$ref

Pour ajouter une relation, le client envoie une requête POST ou PUT à cette adresse.

  • PUT si la propriété de navigation est une entité unique, telle que Product.Supplier.
  • POST si la propriété de navigation est une collection, telle que Supplier.Products.

Le corps de la requête contient l’URI de l’autre entité dans la relation. Voici un exemple de requête :

PUT http://myproductservice.example.com/Products(6)/Supplier/$ref HTTP/1.1
OData-Version: 4.0;NetFx
OData-MaxVersion: 4.0;NetFx
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Content-Type: application/json;odata.metadata=minimal
User-Agent: Microsoft ADO.NET Data Services
Host: myproductservice.example.com
Content-Length: 70
Expect: 100-continue

{"@odata.id":"http://myproductservice.example.com/Suppliers(4)"}

Dans cet exemple, le client envoie une demande PUT à /Products(6)/Supplier/$ref, qui est l’URI $ref du Supplier produit avec l’ID = 6. Si la demande réussit, le serveur envoie une réponse 204 (Aucun contenu) :

HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT

Voici la méthode de contrôleur pour ajouter une relation à un Product:

public class ProductsController : ODataController
{
    [AcceptVerbs("POST", "PUT")]
    public async Task<IHttpActionResult> CreateRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = await db.Products.SingleOrDefaultAsync(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }
        switch (navigationProperty)
        {
            case "Supplier":
                // Note: The code for GetKeyFromUri is shown later in this topic.
                var relatedKey = Helpers.GetKeyFromUri<int>(Request, link);
                var supplier = await db.Suppliers.SingleOrDefaultAsync(f => f.Id == relatedKey);
                if (supplier == null)
                {
                    return NotFound();
                }

                product.Supplier = supplier;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();
        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

Le paramètre navigationProperty spécifie la relation à définir. (S’il existe plusieurs propriétés de navigation sur l’entité, vous pouvez ajouter d’autres case instructions.)

Le paramètre link contient l’URI du fournisseur. L’API web analyse automatiquement le corps de la requête pour obtenir la valeur de ce paramètre.

Pour rechercher le fournisseur, nous avons besoin de l’ID (ou de la clé), qui fait partie du paramètre de lien . Pour ce faire, utilisez la méthode d’assistance suivante :

using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;

namespace ProductService
{
    public static class Helpers
    {
        public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

            string serviceRoot = urlHelper.CreateODataLink(
                request.ODataProperties().RouteName, 
                request.ODataProperties().PathHandler, new List<ODataPathSegment>());
            var odataPath = request.ODataProperties().PathHandler.Parse(
                request.ODataProperties().Model, 
                serviceRoot, uri.LocalPath);

            var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
            if (keySegment == null)
            {
                throw new InvalidOperationException("The link does not contain a key.");
            }

            var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
            return (TKey)value;
        }

    }
}

Fondamentalement, cette méthode utilise la bibliothèque OData pour fractionner le chemin d’accès de l’URI en segments, rechercher le segment qui contient la clé et convertir la clé en le type correct.

Suppression d’une relation entre des entités

Pour supprimer une relation, le client envoie une requête HTTP DELETE à l’URI $ref :

DELETE http://host/Products(1)/Supplier/$ref

Voici la méthode de contrôleur pour supprimer la relation entre un produit et un fournisseur :

public class ProductsController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = db.Products.SingleOrDefault(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }

        switch (navigationProperty)
        {
            case "Supplier":
                product.Supplier = null;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }        

    // Other controller methods not shown.
}

Dans ce cas, Product.Supplier est la fin « 1 » d’une relation 1 à plusieurs, vous pouvez donc supprimer la relation simplement en définissant Product.Suppliernullsur .

Dans la fin « plusieurs » d’une relation, le client doit spécifier l’entité associée à supprimer. Pour ce faire, le client envoie l’URI de l’entité associée dans la chaîne de requête de la demande. Par exemple, pour supprimer « Product 1 » de « Fournisseur 1 » :

DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)

Pour prendre en charge cela dans l’API web, nous devons inclure un paramètre supplémentaire dans la DeleteRef méthode. Voici la méthode de contrôleur pour supprimer un produit de la Supplier.Products relation.

public class SuppliersController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        [FromODataUri] string relatedKey, string navigationProperty)
    {
        var supplier = await db.Suppliers.SingleOrDefaultAsync(p => p.Id == key);
        if (supplier == null)
        {
            return StatusCode(HttpStatusCode.NotFound);
        }

        switch (navigationProperty)
        {
            case "Products":
                var productId = Convert.ToInt32(relatedKey);
                var product = await db.Products.SingleOrDefaultAsync(p => p.Id == productId);

                if (product == null)
                {
                    return NotFound();
                }
                product.Supplier = null;
                break;
            default:
                return StatusCode(HttpStatusCode.NotImplemented);

        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

Le paramètre key est la clé du fournisseur, et le paramètre relatedKey est la clé que le produit doit supprimer de la Products relation. Notez que l’API web obtient automatiquement la clé à partir de la chaîne de requête.