Injection de dépendances dans API Web ASP.NET 2

Télécharger le projet terminé

Ce tutoriel montre comment injecter des dépendances dans votre contrôleur API Web ASP.NET.

Versions logicielles utilisées dans le tutoriel

Qu’est-ce que l’injection de dépendances ?

Une dépendance est un objet qui nécessite un autre objet. Par exemple, il est courant de définir un référentiel qui gère l’accès aux données. Nous allons illustrer avec un exemple. Tout d’abord, nous allons définir un modèle de domaine :

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Voici une classe de dépôt simple qui stocke des éléments dans une base de données, à l’aide d’Entity Framework.

public class ProductsContext : DbContext
{
    public ProductsContext()
        : base("name=ProductsContext")
    {
    }
    public DbSet<Product> Products { get; set; }
}

public class ProductRepository : IDisposable
{
    private ProductsContext db = new ProductsContext();

    public IEnumerable<Product> GetAll()
    {
        return db.Products;
    }
    public Product GetByID(int id)
    {
        return db.Products.FirstOrDefault(p => p.Id == id);
    }
    public void Add(Product product)
    {
        db.Products.Add(product);
        db.SaveChanges();
    }

    protected void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (db != null)
            {
                db.Dispose();
                db = null;
            }
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

À présent, nous allons définir un contrôleur d’API web qui prend en charge les requêtes GET pour les Product entités. (J’exclus POST et d’autres méthodes par souci de simplicité.) Voici une première tentative :

public class ProductsController : ApiController
{
    // This line of code is a problem!
    ProductRepository _repository = new ProductRepository();

    public IEnumerable<Product> Get()
    {
        return _repository.GetAll();
    }

    public IHttpActionResult Get(int id)
    {
        var product = _repository.GetByID(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
}

Notez que la classe du contrôleur dépend ProductRepositoryde et que nous laissons le contrôleur créer le ProductRepository instance. Toutefois, il n’est pas judicieux de coder en dur la dépendance de cette façon, pour plusieurs raisons.

  • Si vous souhaitez remplacer ProductRepository par une autre implémentation, vous devez également modifier la classe de contrôleur.
  • Si a des ProductRepository dépendances, vous devez les configurer à l’intérieur du contrôleur. Pour un projet volumineux avec plusieurs contrôleurs, votre code de configuration est dispersé dans votre projet.
  • Il est difficile de procéder à un test unitaire, car le contrôleur est codé en dur pour interroger la base de données. Pour un test unitaire, vous devez utiliser un référentiel fictif ou stub, ce qui n’est pas possible avec la conception actuelle.

Nous pouvons résoudre ces problèmes en injectant le dépôt dans le contrôleur. Tout d’abord, refactorisez la ProductRepository classe dans une interface :

public interface IProductRepository
{
    IEnumerable<Product> GetAll();
    Product GetById(int id);
    void Add(Product product);
}

public class ProductRepository : IProductRepository
{
    // Implementation not shown.
}

Fournissez ensuite le IProductRepository en tant que paramètre de constructeur :

public class ProductsController : ApiController
{
    private IProductRepository _repository;

    public ProductsController(IProductRepository repository)  
    {
        _repository = repository;
    }

    // Other controller methods not shown.
}

Cet exemple utilise l’injection de constructeur. Vous pouvez également utiliser l’injection de setter, où vous définissez la dépendance par le biais d’une méthode ou d’une propriété setter.

Mais maintenant, il y a un problème, car votre application ne crée pas directement le contrôleur. L’API web crée le contrôleur lorsqu’elle achemine la requête, et l’API web ne sait rien sur IProductRepository. C’est là qu’intervient le programme de résolution des dépendances de l’API web.

Programme de résolution des dépendances de l’API web

L’API web définit l’interface IDependencyResolver pour la résolution des dépendances. Voici la définition de l’interface :

public interface IDependencyResolver : IDependencyScope, IDisposable
{
    IDependencyScope BeginScope();
}

public interface IDependencyScope : IDisposable
{
    object GetService(Type serviceType);
    IEnumerable<object> GetServices(Type serviceType);
}

L’interface IDependencyScope a deux méthodes :

  • GetService crée une instance d’un type.
  • GetServices crée une collection d’objets d’un type spécifié.

La méthode IDependencyResolver hérite de IDependencyScope et ajoute la méthode BeginScope . Je parlerai des étendues plus loin dans ce tutoriel.

Lorsque l’API web crée un contrôleur instance, elle appelle d’abord IDependencyResolver.GetService, en passant le type de contrôleur. Vous pouvez utiliser ce hook d’extensibilité pour créer le contrôleur, en résolvant les dépendances. Si GetService retourne null, l’API web recherche un constructeur sans paramètre sur la classe de contrôleur.

Résolution des dépendances avec le conteneur Unity

Bien que vous puissiez écrire une implémentation IDependencyResolver complète à partir de zéro, l’interface est vraiment conçue pour servir de pont entre l’API web et les conteneurs IoC existants.

Un conteneur IoC est un composant logiciel chargé de gérer les dépendances. Vous inscrivez des types avec le conteneur, puis utilisez le conteneur pour créer des objets. Le conteneur détermine automatiquement les relations de dépendance. De nombreux conteneurs IoC vous permettent également de contrôler des éléments tels que la durée de vie et l’étendue des objets.

Notes

« IoC » signifie « inversion de contrôle », qui est un modèle général où une infrastructure appelle le code d’application. Un conteneur IoC construit vos objets pour vous, ce qui « inverse » le flux de contrôle habituel.

Pour ce tutoriel, nous allons utiliser Unity à partir de Modèles & Pratiques Microsoft. (D’autres bibliothèques populaires incluent Castle Windsor, Spring.Net, Autofac, Ninject et StructureMap.) Vous pouvez utiliser le Gestionnaire de package NuGet pour installer Unity. Dans le menu Outils de Visual Studio, sélectionnez Gestionnaire de package NuGet, puis Console du Gestionnaire de package. Dans la fenêtre Console du Gestionnaire de package, tapez la commande suivante :

Install-Package Unity

Voici une implémentation de IDependencyResolver qui encapsule un conteneur Unity.

using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

public class UnityResolver : IDependencyResolver
{
    protected IUnityContainer container;

    public UnityResolver(IUnityContainer container)
    {
        if (container == null)
        {
            throw new ArgumentNullException(nameof(container));
        }
        this.container = container;
    }

    public object GetService(Type serviceType)
    {
        try
        {
            return container.Resolve(serviceType);
        }
        catch (ResolutionFailedException exception)
        {
            throw new InvalidOperationException(
                $"Unable to resolve service for type {serviceType}.",
                exception)
        }
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        try
        {
            return container.ResolveAll(serviceType);
        }
        catch (ResolutionFailedException exception)
        {
            throw new InvalidOperationException(
                $"Unable to resolve service for type {serviceType}.",
                exception)
        }
    }

    public IDependencyScope BeginScope()
    {
        var child = container.CreateChildContainer();
        return new UnityResolver(child);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        container.Dispose();
    }
}

Configuration du programme de résolution des dépendances

Définissez le programme de résolution de dépendances sur la propriété DependencyResolver de l’objet HttpConfiguration global.

Le code suivant inscrit l’interface IProductRepository auprès d’Unity, puis crée un UnityResolver.

public static void Register(HttpConfiguration config)
{
    var container = new UnityContainer();
    container.RegisterType<IProductRepository, ProductRepository>(new HierarchicalLifetimeManager());
    config.DependencyResolver = new UnityResolver(container);

    // Other Web API configuration not shown.
}

Étendue de dépendance et durée de vie du contrôleur

Les contrôleurs sont créés par demande. Pour gérer les durées de vie des objets, IDependencyResolver utilise le concept d’étendue.

Le programme de résolution de dépendances attaché à l’objet HttpConfiguration a une étendue globale. Lorsque l’API web crée un contrôleur, elle appelle BeginScope. Cette méthode retourne un IDependencyScope qui représente une étendue enfant.

L’API web appelle ensuite GetService sur l’étendue enfant pour créer le contrôleur. Lorsque la demande est terminée, l’API web appelle Dispose sur l’étendue enfant. Utilisez la méthode Dispose pour supprimer les dépendances du contrôleur.

La façon dont vous implémentez BeginScope dépend du conteneur IoC. Pour Unity, l’étendue correspond à un conteneur enfant :

public IDependencyScope BeginScope()
{
    var child = container.CreateChildContainer();
    return new UnityResolver(child);
}

La plupart des conteneurs IoC ont des équivalents similaires.