Fevereiro de 2016

Volume 31 – Número 2

Pontos de Dados – refatorando um projeto ASP.NET 5/EF6 e injeção de dependência

Por Julie Lerman

Antes da publicação na imprensa, a Microsoft anunciou alterações de nome relativas ao ASP.NET 5 e pilhas relacionadas. O ASP.NET 5 é agora o ASP.NET Core 1.0. O Entity Framework (EF) 7 é agora o Entity Framework (EF) Core 1.0. Os pacotes do EF7 e ASP.NET 5 e os namespaces serão alterados, mas, de qualquer forma, a nova nomenclatura não terá impacto nas lições deste artigo.

Julie LermanID (Injeção de dependência) é sinônimo de acoplamento fraco (bit.ly/1TZWVtW). Em vez de codificar as classes das quais você depende em outras classes, você as solicita de outro lugar, preferencialmente de seu construtor de classe. Isso segue o princípio de dependências explícitas, informando claramente os usuários de sua classe sobre os respectivos colaboradores necessários. Isso também permite que você desenvolva mais flexibilidade em seu software para cenários como configurações alternadas da instância de objeto de uma classe, o que é realmente vantajoso para escrever testes automatizados para tais classes. No meu universo repleto de código de Entity Framework, um exemplo típico de código sem acoplamento fraco é criar um repositório ou controlador que instancia um DbContex diretamente. Já fiz isso milhares de vezes. Na verdade, meu objetivo com este artigo é aplicar o que eu aprendi sobre ID para o código que escrevi em minha coluna, “A mistura de EF6, EF7 e ASP.NET 5” (https://msdn.microsoft.com/pt-br/magazine/dn973011). Por exemplo, aqui está um método no qual eu instanciei um DbContext diretamente:

public List<Ninja> GetAllNinjas() {
  using (var context=new NinjaContext())
  {
    return context.Ninjas.ToList();
  }
}

O fato de eu ter usado isso dentro de uma solução ASP.NET 5, que tem tanto suporte a ID interno, Rowan Miller da equipe EF sugeriu que eu poderia melhorar o exemplo tirando vantagem desse suporte a ID. Eu estava tão focada em outros aspectos do problema que nem tinha considerado isso. Por isso, comecei a refatorar esse exemplo parte por parte até conseguir ter o fluxo funcionando conforme indicado. Na verdade, Miller já me tinha indicado um bom exemplo escrito por Paweł Grudzień nesta postagem no blog, “Entity Framework 6 with ASP.NET 5” (bit.ly/1k4Tt4Y), mas eu escolhi explicitamente não me basear nele e não simplesmente copiar/colar do blog. Em vez disso, eu trabalhei ideias próprias para que pudesse compreender melhor o fluxo. No final, fiquei feliz de ver que minha solução estava bem alinhada com a postagem no blog.

IoC (Inversão de Controle) e contêineres de IoC são padrões que sempre me pareceram um pouco complicados. Lembre-se de que venho escrevendo código há quase 30 anos e, por isso, imagino que eu não seja a única desenvolvedora experiente que nunca fez a transição mental para esse padrão. Martin Fowler, um famoso especialista nesta área, aponta que a IoC tem vários significados, mas a que se alinha com a ID (termo criado para esclarecer este tipo de IoC) é sobre que parte de seu aplicativo tem controle para criar objetos específicos. Sem IoC, isso foi sempre um desafio.

Quando escrevi como coautora o curso Pluralsight “Domain-Driven Design Fundamentals” (bit.ly/PS-DDD) com Steve Smith (deviq.com), fui finalmente levada a usar a biblioteca StructureMap, que se tornou um dos contêineres mais populares de IoC entre os desenvolvedores de .NET desde seu início em 2005. A verdade é que eu estava um pouco desatualizada. Com a orientação do Smith, eu consegui entender como isso funciona e seus benefícios, mas ainda não me sentia muito firme com a tecnologia. Por isso, depois da dica de Miller, decidi refatorar minha amostra anterior para aproveitar um contêiner, o que facilita injetar instâncias de objeto em uma lógica que precisa dessas instâncias.

Mas, primeiro, vamos usar o DRY

Um problema inicial em minha classe que abriga a classe GetAllNinjas mostrada anteriormente é que eu repito o código using:

using(var context=new NinjaContext)

em outros métodos dessa classe, como:

public Ninja GetOneNinja(int id) {
  using (var context=new NinjaContext())
  {
    return context.Ninjas.Find(id);
  }
}

O princípio “Não se repita” (Don’t Repeat Yourself – DRY) me ajuda a reconhecer essa possível armadilha. Eu moverei a criação da instância NinjaContext em um construtor e compartilharei uma variável como _context com os vários métodos:

NinjaContext _context;
public NinjaRepository() {
  _context = new NinjaContext();
}

No entanto, essa classe, que deveria apenas focar na recuperação dos dados, ainda é responsável por determinar como e quando criar o contexto. Eu quero mover decisões sobre como e quando criar o contexto mais para cima no fluxo e apenas deixar meu repositório usar o contexto injetado. Por isso, vou refatorar novamente passando para um contexto criado em outro local:

NinjaContext _context;
public NinjaRepository(NinjaContext context) {
  _context = context;
}

Agora, o repositório está por conta própria. Eu não tenho que continuar mexendo com ele para criar o contexto. O repositório são se preocupa com a maneira como o texto é configurado, quando é criado ou quando é descartado. Isso também ajuda a classe a seguir outro princípio orientado a objeto, o princípio de responsabilidade única, pois ele não é mais responsável por gerenciar contextos EF além de fazer solicitações de banco de dados. Quando estou trabalhando na classe de repositório, posso focar nas consultas. Eu também posso testá-la facilmente, pois meus testes podem orientar essas decisões e não serão atropelados por um repositório criado para ser usado de uma forma que não se alinha com a maneira como eu possa querer usá-lo em testes automatizados.

Existe outro problema em meu exemplo original, que é o fato de eu ter codificado a cadeia de conexão no DbContext. Justifiquei que, na altura, ele era “só uma demo”, e obter a cadeia de conexão do aplicativo de execução (o aplicativo ASP.NET 5) para o projeto EF6 era complicado e eu estava focada em outras coisas. No entanto, conforme eu vou refatorando esse projeto, vou conseguindo aproveitar o IoC para passar à cadeia de conexão do aplicativo em execução. Veja a continuação disso mais à frente no artigo.

Deixe o ASP.NET 5 injetar o NinjaContext

Mas para onde eu movo a criação do NinjaContext? O controlador usa o repositório. Eu realmente não quero introduzir o EF no controlador para passá-lo para uma nova instância do repositório. Isso viraria uma bagunça (algo assim):

public class NinjaController : Controller {
  NinjaRepository _repo;
  public NinjaController() {
    var context = new NinjaContext();
    _repo = new NinjaRepository(context);
  }
  public IActionResult Index() {
    return View(_repo.GetAllNinjas());
  }
}

Ao mesmo tempo, estou forçando o controlador para estar ciente do EF; esse código ignora os problemas de instanciação dos objetos dependentes que eu acabei de resolver no repositório. O controlador instancia diretamente a classe de repositório. Eu só quero que ele use o repositório e não se preocupe sobre como e quando criá-lo ou quando descartá-lo. Assim como eu injetei a instância NinjaContext no repositório, quero injetar uma instância de repositório pronta para uso no controlador.

Uma versão mais limpa do código na classe do controlador se parece mais assim:

public class NinjaController : Controller {
  NinjaRepository _repo;
  public NinjaController(NinjaRepository repo) {
    _repo = repo;
  }
  public IActionResult Index() {
    return View(_repo.GetAllNinjas());
  }
}

Orquestrando uma criação de objeto com contêineres IoC

Como eu estou trabalhando com o ASP.NET 5 em vez de usar um StructureMap, vou aproveitar o suporte interno à ID do ASP.NET 5. Não só muitas as classes ASP.NET são criadas para aceitar objetos sendo injetados, como o ASP.NET 5 tem uma infraestrutura de serviço que pode coordenar quais objetos vão para que locais – um contêiner IoC. Isso também permite que você especifique o escopo dos objetos – quando eles devem ser criados e descartados – que serão criados e injetados. Trabalhar com o suporte interno é uma maneira mais fácil de começar.

Antes de usar o suporte à ID do ASP.NET 5 para me ajudar a injetar meu NinjaContext e NinjaRepository, se necessário, vamos ver como isso fica injetando classes EF7, pois o EF7 tem métodos internos para ligá-lo ao suporte à ID do ASP.NET 5. A classe startup.cs, que é parte de um projeto ASP.NET 5, tem um método chamado ConfigureServices. É aqui que você diz ao seu aplicativo como deseja ligar as dependências para que ele possa criar e depois injetar os objetos certos nos objetos que precisam deles. Aqui está esse método, com tudo eliminado exceto uma configuração para EF7:

public void ConfigureServices(IServiceCollection services)
{
  services.AddEntityFramework()
          .AddSqlServer()
          .AddDbContext<NinjaContext>(options =>
            options.UseSqlServer(
            Configuration["Data:DefaultConnection:ConnectionString"]));
}

Diferentemente do meu projeto, que usa meu modelo baseado em EF6, o projeto no qual esta configuração está sendo executada depende do EF7. Esses próximos parágrafos descrevem o que está acontecendo neste código.

Como o EntityFramework .MicrosoftSqlServer foi especificado em seu arquivo project.json, o projeto referencia todos os assemblies EF7 relevantes. Um deles, o assembly EntityFramework.Core, fornece o método de extensão AddEntityFramework para IServiceCollection, permitindo que eu adicione o serviço Entity Framework. O dll EntityFramework .MicrosoftSqlServer fornece o método de extensão AddSqlServer acrescentado ao AddEntityFramework. Isso coloca o serviço SqlServer no contêiner IoC para que o EF saiba como usá-lo quando está procurando um provedor de dados.

O AddDbContext vem do núcleo EF. Este código adiciona a instância DbContext especificada (com as opções especificadas) ao contêiner interno ASP.NET 5. Qualquer classe que solicite um DbContext em seu construtor (e que esse ASP.NET 5 esteja construindo) terá o DbContext configurado que foi fornecido quando este for criado. Por isso, este código adiciona o NinjaContext como um tipo conhecido que o serviço vai instanciar conforme necessário. Além disso, o código especifica que ao construir um NinjaContext, ele deverá usar a cadeia de caracteres encontrada no código de configuração (que neste caso está vindo de um arquivo appsettings.json do ASP.NET 5, criado pelo modelo de projeto) como uma opção de configuração SqlServer. Como o ConfigureService executa no código de inicialização, quando qualquer código no aplicativo espera um NinjaContext, mas nenhuma instância é fornecida, o ASP.NET 5 vai instanciar um novo objeto NinjaContext usando a cadeia de conexão especificada e passá-lo para dentro.

Então está tudo muito bem integrado com o EF7. Infelizmente, nada disso existe para o EF6. Mas agora que você tem uma ideia de como os serviços funcionam, o padrão para adicionar o NinjaContext do EF6 aos serviços do aplicativo deverão fazer sentido.

Adicionando serviços não criados para o ASP.NET 5

Além dos serviços que são criados para trabalhar com o ASP.NET 5, que têm boas extensões como o AddEntityFramework e o AddMvc, é possível adicionar outras dependências. A interface IServicesCollection fornece um método Add simples, junto com um conjunto de métodos para especificar o tempo de vida do serviço sendo adicionado: AddScoped, AddSingleton e AddTransient. Focarei no AddScoped para a minha solução, pois ele vasculha o tempo de vida da instância solicitada para cada solicitação HTTP no aplicativo MVC no qual eu quero usar meu projeto EF6Model. O aplicativo não tentará compartilhar uma instância pelas solicitações. Isso vai emular o que eu estava conseguindo originalmente com a criação e o descarte do meu NinjaContext dentro de cada ação do controlador, pois cada ação do controlador estava respondendo a uma única solicitação.

Lembre-se de que eu tenho duas classes que precisam de objetos injetados. A classe NinjaRepository precisa do NinjaContext, e o NinjaController precisa de um objeto NinjaRepository.

No método ConfigureServices do startup.cs eu começo adicionando:

services.AddScoped<NinjaRepository>();
services.AddScoped<NinjaContext>();

Agora que o meu aplicativo tem consciência desses tipos e vai instanciá-los quando solicitado por outro construtor do tipo.

Quando o construtor do controlado está procurando um NinjaRepository para ser passado como um parâmetro:

public NinjaController(NinjaRepository repo) {
    _repo = repo;
  }

mas nenhum foi passado, por isso, o serviço criará um NinjaRepository no momento. Isso é referido como “injeção do construtor”. Quando o NinjaRepository espera uma instância NinjaRepository e nenhuma foi passada, o serviço também saberá instanciar isso.

Lembra-se do código de cadeia de conexão em meu DbContext que eu mostrei anteriormente? Agora eu posso instruir o método AddScoped que constrói o NinjaContext sobre a cadeia de conexão. Colocarei novamente a cadeia de caracteres no arquivo appsetting.json. Aqui está a seção apropriada desse arquivo:

"Data": {
    "DefaultConnection": {
      "NinjaConnectionString":
      "Server=(localdb)\\mssqllocaldb;Database=NinjaContext;
      Trusted_Connection=True;MultipleActiveResultSets=true"
    }
  }

Observe que o JSON não suporta quebra automática de linha, por isso, a cadeia começa com Server= não pode ser quebrada em seu arquivo JSON. Ela está quebrada aqui só para facilitar a leitura.

Eu modifiquei o construtor NinjaContext para usar uma cadeia de conexão na sobrecarga do DbContext, que também usa uma cadeia de conexão:

public NinjaContext(string connectionString):
    base(connectionString) { }

Agora, eu posso dizer ao AddScoped que quando ele vê um NinjaContext, deverá construí-lo usando essa sobrecarga, passando no Ninja­ConnectionString encontrado em appsettings.json:

services.AddScoped<NinjaContext>
(serviceProvider=>new NinjaContext
  (Configuration["Data:DefaultConnection:NinjaConnectionString"]));

Com esta última modificação, a solução que eu encontrei enquanto refatorava agora funciona de ponta a ponta. A lógica de inicialização configura o aplicativo para injetar o repositório e o contexto. Quando o aplicativo é encaminhado para o controlador padrão (que usa o repositório que usa o contexto), os objetos necessários são criados imediatamente e os dados são recuperados do banco de dados. Meu aplicativo ASP.NET 5 aproveita seu DI interno para interagir com um assembly mais antigo no qual eu usei EF6 para criar o modelo.

Interfaces para flexibilidade

Existe uma última melhoria possível, que consiste em aproveitar as interfaces. Se existir a possibilidade de eu querer usar uma versão diferente da minha classe NinjaRepository ou NinjaContext, eu posso implementar as interfaces em tudo. Eu não consigo prever a necessidade de ter uma variação sobre o NinjaContext, por isso, eu só criarei uma interface para a classe de repositório.

Como mostrado na Figura 1, o NinjaRepository agora implementa um contrato INinjaRepository.

Figura 1 NinjaRepository usando uma interface

public interface INinjaRepository
{
  List<Ninja> GetAllNinjas();
}
public class NinjaRepository : INinjaRepository
{
  NinjaContext _context;
  public NinjaRepository(NinjaContext context) {
    _context = context;
  }
  public List<Ninja> GetAllNinjas() {
    return _context.Ninjas.ToList();
  }
}

O controlador no aplicativo MVC do ASP.NET 5 agora usa a interface INinjaRepository em vez da implementação concreta, NinjaRepository:

public class NinjaController : Controller {
  INinjaRepository _repo;
  public NinjaController(INinjaRepository repo) {
    _repo = repo;
  }
  public IActionResult Index() {
    return View(_repo.GetAllNinjas());
  }
}

Eu modifiquei o método AddScoped para o NinjaRepository dizer ao ASP.NET 5 usar a implementação apropriada (atualmente NinjaRepository) sempre que a interface for solicitada:

services.AddScoped<INinjaRepository, NinjaRepository>();

Quando chegar o momento de uma nova versão, ou se eu estiver usando outra implementação diferente da interface em um aplicativo diferente, poderei modificar o método AddScoped para usar a implementação correta.

Aprenda fazendo, não copie e cole

Eu agradeço ao Miller, que gentilmente me desafiou a refatorar minha solução. Naturalmente, minha refatoração não correu tão tranquilamente como pode parecer baseado no que eu escrevi. Como eu não apenas copiei a solução de outra pessoa, fiz algumas coisas erradas no início. Descobrir o que estava errado e encontrar o código certo fez com que eu finalmente acertasse, o que foi muito vantajoso para o meu aprendizado de ID e IoC. Espero que as minha explicações deem a você essa vantagem sem que tenha que quebrar muito a cabeça como eu.


Julie Lerman* é MVP da Microsoft, mentora e consultora do .NET, e reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos .NET em grupos de usuários e conferências em todo o mundo. Ela escreve no blog thedatafarm.com/blog e é autora do "Programming Entity Framework", bem como de uma edição do Code First e do DbContext, todos da O'Reilly Media. Siga-a no Twitter em @julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.*

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Steve Smith
Steve Smith (@ardalis) é empresário e desenvolvedor de software apaixonado por criar software de qualidade. Steve publicou vários cursos no Pluralsight, abordando DDD, SOLID, padrões de design e arquitetura de software. Ele é MVP da Microsoft, palestrante frequente em conferências para desenvolvedores, autor, mentor e instrutor. Descubra como Steve pode ajudar sua equipe ou projeto em ardalis.com.