Injeção de dependência na API Web ASP.NET 2
Este tutorial mostra como injetar dependências no controlador de API Web do ASP.NET.
Versões de software usadas no tutorial
- API Web 2
- Bloco de aplicativos do Unity
- Entity Framework 6 (a versão 5 também funciona)
O que é injeção de dependência?
Uma dependência é qualquer objeto exigido por outro objeto. Por exemplo, é comum definir um repositório que lida com o acesso a dados. Vamos ilustrar com um exemplo. Primeiro, definiremos um modelo de domínio:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Aqui está uma classe de repositório simples que armazena itens em um banco de dados, usando o 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);
}
}
Agora vamos definir um controlador de API Web que dê suporte a solicitações GET para Product
entidades. (Estou deixando de fora o POST e outros métodos para simplificar.) Aqui está uma primeira tentativa:
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);
}
}
Observe que a classe do controlador depende de ProductRepository
, e estamos permitindo que o controlador crie a ProductRepository
instância. No entanto, é uma má ideia codificar a dependência dessa maneira, por vários motivos.
- Se você quiser substituir
ProductRepository
por uma implementação diferente, também precisará modificar a classe do controlador. - Se o
ProductRepository
tiver dependências, você deverá configurá-las dentro do controlador. Para um projeto grande com vários controladores, seu código de configuração fica espalhado pelo projeto. - É difícil testar a unidade, porque o controlador é codificado para consultar o banco de dados. Para um teste de unidade, você deve usar um repositório fictício ou stub, o que não é possível com o design atual.
Podemos resolver esses problemas injetando o repositório no controlador. Primeiro, refatore a ProductRepository
classe em uma interface:
public interface IProductRepository
{
IEnumerable<Product> GetAll();
Product GetById(int id);
void Add(Product product);
}
public class ProductRepository : IProductRepository
{
// Implementation not shown.
}
Em seguida, forneça o IProductRepository
como um parâmetro de construtor:
public class ProductsController : ApiController
{
private IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
// Other controller methods not shown.
}
Este exemplo usa injeção de construtor. Você também pode usar a injeção de setter, em que você define a dependência por meio de um método ou propriedade setter.
Mas agora há um problema, porque seu aplicativo não cria o controlador diretamente. A API Web cria o controlador quando roteia a solicitação, e a API Web não sabe nada sobre IProductRepository
o . É aqui que entra o resolvedor de dependência da API Web.
O Resolvedor de Dependência da API Web
A API Web define a interface IDependencyResolver para resolver dependências. Aqui está a definição da interface:
public interface IDependencyResolver : IDependencyScope, IDisposable
{
IDependencyScope BeginScope();
}
public interface IDependencyScope : IDisposable
{
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}
A interface IDependencyScope tem dois métodos:
- GetService cria uma instância de um tipo.
- GetServices cria uma coleção de objetos de um tipo especificado.
O método IDependencyResolver herda IDependencyScope e adiciona o método BeginScope. Falarei sobre escopos mais adiante neste tutorial.
Quando a API Web cria uma instância do controlador, ela primeiro chama IDependencyResolver.GetService, passando o tipo de controlador. Você pode usar esse gancho de extensibilidade para criar o controlador, resolvendo todas as dependências. Se GetService retornar nulo, a API Web procurará um construtor sem parâmetros na classe do controlador.
Resolução de dependência com o contêiner do Unity
Embora você possa escrever uma implementação IDependencyResolver completa do zero, a interface foi realmente projetada para atuar como ponte entre a API Web e os contêineres de IoC existentes.
Um contêiner de IoC é um componente de software responsável pelo gerenciamento de dependências. Você registra tipos com o contêiner e, em seguida, usa o contêiner para criar objetos. O contêiner descobre automaticamente as relações de dependência. Muitos contêineres de IoC também permitem que você controle coisas como tempo de vida e escopo do objeto.
Observação
"IoC" significa "inversão de controle", que é um padrão geral em que uma estrutura chama o código do aplicativo. Um contêiner de IoC constrói seus objetos para você, o que "inverte" o fluxo usual de controle.
Para este tutorial, usaremos o Unity de Padrões e Práticas da Microsoft. (Outras bibliotecas populares incluem Castelo Windsor, Spring.Net, Autofac, Ninject, Injetor Simples e StructureMap.) Você pode usar o Gerenciador de Pacotes NuGet para instalar o Unity. No menu Ferramentas no Visual Studio, selecione Gerenciador de Pacotes NuGet e, em seguida, selecione Console do Gerenciador de Pacotes. Na janela Console do Gerenciador de Pacotes, digite o seguinte comando:
Install-Package Unity
Aqui está uma implementação de IDependencyResolver que encapsula um contêiner do 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();
}
}
Configurando o resolvedor de dependência
Defina o resolvedor de dependência na propriedade DependencyResolver do objeto HttpConfiguration global.
O código a seguir registra a interface com o IProductRepository
Unity e cria um UnityResolver
arquivo .
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.
}
Escopo de dependência e tempo de vida do controlador
Os controladores são criados por solicitação. Para gerenciar o tempo de vida do objeto, IDependencyResolver usa o conceito de um escopo.
O resolvedor de dependência anexado ao objeto HttpConfiguration tem escopo global. Quando a API Web cria um controlador, ela chama BeginScope. Esse método retorna um IDependencyScope que representa um escopo filho.
Em seguida, a API Web chama GetService no escopo filho para criar o controlador. Quando a solicitação é concluída, a API Web chama Dispose no escopo filho. Use o método Dispose para descartar as dependências do controlador.
A forma como você implementa o BeginScope depende do contêiner de IoC. Para o Unity, o escopo corresponde a um contêiner filho:
public IDependencyScope BeginScope()
{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}
A maioria dos contêineres de IoC tem equivalentes semelhantes.