Inserción de dependencias en ASP.NET API web 2

Descargar el proyecto completado

En este tutorial se muestra cómo insertar dependencias en el controlador de API web de ASP.NET.

Versiones de software usadas en el tutorial

¿Qué es la inserción de dependencias?

Una dependencia es cualquier objeto requerido por otro objeto. Por ejemplo, es habitual definir un repositorio que controla el acceso a los datos. Vamos a ilustrar con un ejemplo. En primer lugar, definiremos un modelo de dominio:

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

Esta es una clase de repositorio simple que almacena elementos en una base de datos mediante 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);
    }
}

Ahora vamos a definir un controlador de API web que admita solicitudes GET para Product entidades. (Estoy dejando fuera POST y otros métodos para simplificar.) Este es un primer intento:

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);
    }
}

Tenga en cuenta que la clase de controlador depende de ProductRepository, y se permite que el controlador cree la ProductRepository instancia. Sin embargo, es una mala idea codificar la dependencia de esta manera, por varias razones.

  • Si desea reemplazar ProductRepository por una implementación diferente, también debe modificar la clase de controlador.
  • Si tiene ProductRepository dependencias, debe configurarlas dentro del controlador. Para un proyecto grande con varios controladores, el código de configuración se dispersa en el proyecto.
  • Es difícil realizar pruebas unitarias, ya que el controlador está codificado de forma rígida para consultar la base de datos. Para una prueba unitaria, debe usar un repositorio ficticio o de código auxiliar, que no es posible con el diseño actual.

Para solucionar estos problemas, insertar el repositorio en el controlador. En primer lugar, refactorice la ProductRepository clase en una interfaz:

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

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

A continuación, proporcione IProductRepository como parámetro de constructor:

public class ProductsController : ApiController
{
    private IProductRepository _repository;

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

    // Other controller methods not shown.
}

En este ejemplo se usa la inserción de constructores. También puede usar inserción de establecedores, donde se establece la dependencia a través de un método o propiedad de establecedor.

Pero ahora hay un problema, ya que la aplicación no crea el controlador directamente. La API web crea el controlador cuando enruta la solicitud y la API web no sabe nada sobre IProductRepository. Aquí es donde entra la resolución de dependencias de la API web.

Resolución de dependencias de API web

La API web define la interfaz de IDependencyResolver para resolver las dependencias. Esta es la definición de la interfaz:

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

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

La interfaz IDependencyScope tiene dos métodos:

  • GetService crea una instancia de un tipo.
  • GetServices crea una colección de objetos de un tipo especificado.

El método IDependencyResolver hereda IDependencyScope y agrega el método BeginScope. Hablaré sobre ámbitos más adelante en este tutorial.

Cuando la API web crea una instancia de controlador, primero llama a IDependencyResolver.GetService, pasando el tipo de controlador. Puede usar este enlace de extensibilidad para crear el controlador y resolver las dependencias. Si GetService devuelve null, Web API busca un constructor sin parámetros en la clase de controlador.

Resolución de dependencias con el contenedor de Unity

Aunque podría escribir una implementación completa IDependencyResolver desde cero, la interfaz está realmente diseñada para actuar como puente entre la API web y los contenedores de IoC existentes.

Un contenedor de IoC es un componente de software responsable de administrar las dependencias. Los tipos se registran en el contenedor y, a continuación, se usa el contenedor para crear objetos. El contenedor calcula automáticamente las relaciones de dependencia. Muchos contenedores de IoC también le permiten controlar cosas como la duración y el ámbito de los objetos.

Nota:

"IoC" significa "inversión de control", que es un patrón general en el que un marco llama al código de la aplicación. Un contenedor de IoC construye los objetos automáticamente, lo que "invierte" el flujo de control habitual.

En este tutorial, usaremos Unity de Microsoft Patterns & Practices. (Otras bibliotecas populares incluyen Castle Windsor, Spring.Net, Autofac, Ninjecty StructureMap.) Puede usar el Administrador de paquetes NuGet para instalar Unity. En Visual Studio, en el menú Herramientas, seleccione Administrador de paquetes NuGet, después seleccione Consola del administrador de paquetes. En la ventana de la consola del administrador de paquetes, escriba el comando siguiente:

Install-Package Unity

Esta es una implementación de IDependencyResolver que encapsula un contenedor de 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();
    }
}

Configuración del solucionador de dependencias

Establezca la resolución de dependencias en la propiedad DependencyResolver del objeto HttpConfiguration global.

El código siguiente registra la IProductRepository interfaz con Unity y, a continuación, crea 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.
}

Duración del ámbito de dependencia y del controlador

Los controladores se crean por solicitud. Para administrar las duraciones de objetos, IDependencyResolver usa el concepto de un ámbito.

El solucionador de dependencias adjunto al objeto HttpConfiguration tiene ámbito global. Cuando la API web crea un controlador, llama a BeginScope. Este método devuelve un IDependencyScope que representa un ámbito secundario.

A continuación, la API web llama a GetService en el ámbito secundario para crear el controlador. Una vez completada la solicitud, la API web llama a Eliminar en el ámbito secundario. Use el método Eliminar para eliminar las dependencias del controlador.

La forma de implementar BeginScope depende del contenedor de IoC. Para Unity, el ámbito corresponde a un contenedor secundario:

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

La mayoría de los contenedores de IoC tienen equivalentes similares.