Febrero de 2016

Volumen 31, número 2

Puntos de datos: refactorización de un proyecto de ASP.NET 5/EF6 e inserción de dependencias

Por Julie Lerman

Justo antes del cierre de la edición, Microsoft anunció el cambio de nombre de ASP.NET 5 y las pilas relacionadas. ASP.NET 5 es ahora ASP.NET Core 1.0. Entity Framework (EF) 7 es ahora Entity Framework (EF) Core 1.0. Los paquetes y espacios de nombres de ASP.NET 5 y EF7 cambiarán, pero la nueva nomenclatura no afectará de ningún otro modo a las lecciones de este artículo.

Julie LermanLa inserción de dependencias (DI) gira en torno al acoplamiento débil (bit.ly/1TZWVtW). En lugar de codificar de forma rígida clases de las que depende en otras clases, las puede solicitar desde cualquier otro sitio, de forma ideal desde el constructor de la clase. Eso sigue el principio de dependencias explícitas, que informa de forma más clara a los usuarios de la clase sobre los colaboradores que requiere. También permite que se cree más flexibilidad en el software para escenarios como las configuraciones alternativas de la instancia del objeto de una clase y es realmente beneficioso para la escritura de pruebas automáticas para dichas clases. En mi mundo, que está lleno de código de Entity Framework, un ejemplo típico de codificación sin acoplamiento débil es la creación de un repositorio o un controlador que crea una instancia de un elemento DbContext directamente. Lo he hecho miles de veces. De hecho, mi objetivo con este artículo es aplicar lo que he aprendido sobre DI al código que escribí en mi columna "EF6, EF7 y ASP.NET 5 Soup" (msdn.com/magazine/dn973011). Por ejemplo, a continuación se muestra un método donde creé una instancia de un elemento DbContext directamente:

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

Como usé esto dentro de una solución de ASP.NET 5 y ASP.NET 5 tiene una gran compatibilidad con DI integrada, Rowan Miller del equipo de EF sugirió que podía mejorar el ejemplo si aprovechaba dicha compatibilidad con DI. Me había centrado tanto en otros aspectos del problema que ni siquiera había considerado esa posibilidad. De forma que me dediqué a refactorizar ese ejemplo poco a poco, hasta que conseguí que el flujo funcionara como debía. Miller me había señalado un buen ejemplo escrito por Paweł Grudzień en una publicación de su blog, "Entity Framework 6 with ASP.NET 5" (bit.ly/1k4Tt4Y), pero decidí explícitamente tomar otro camino y no simplemente copiar y pegar de ese blog. En su lugar, trabajé las ideas por mi cuenta para comprender mejor el flujo. Al final, me sentí feliz de ver que mi solución estaba en sintonía con la de la publicación del blog.

La inversión de control (IoC) y los contenedores de IoC son patrones que siempre me han parecido abrumadores. Tenga en cuenta que he estado codificando durante casi treinta años, así que supongo que no soy el único desarrollador con experiencia que nunca ha hecho la transición mental a este patrón. Martin Fowler, un reconocido experto en este campo, señala que IoC tiene varios significados, pero que el que está en sintonía con DI (un término que creó para aclarar este tipo de IoC) trata sobre qué parte de la aplicación está a cargo de la creación de objetos particulares. Sin IoC, esto siempre ha sido un reto.

Cuando escribí como coautor el curso de Pluralsight "Domain-Driven Design Fundamentals" (bit.ly/PS-DDD) con Steve Smith (deviq.com), finalmente tuve que usar la biblioteca de StructureMap, que se ha convertido en uno de los contenedores de IoC más populares entre los desarrolladores de .NET desde su comienzo en 2005. La conclusión es que llegué un poco tarde. Con la orientación de Smith, fui capaz de entender cómo funciona y sus ventajas, pero seguía sin sentirme completamente a gusto. Después del consejo de Miller, decidí refactorizar mi ejemplo anterior para aprovechar un contenedor que permitiera insertar con facilidad instancias de objetos en lógica que necesita usarlos.

Pero primero, hablemos de DRY

Un problema inicial de la clase que alberga la clase GetAllNinjas que se mostró anteriormente es que repito el código using:

using(var context=new NinjaContext)

En otros métodos de esa clase, como por ejemplo:

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

El principio Don’t Repeat Yourself (DRY, no te repitas) me ayuda a identificar ese peligro potencial. Moveré la creación de la instancia de NinjaContext a un constructor y compartiré una variable como por ejemplo _context con los distintos métodos:

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

Sin embargo, esta clase, que solo debería centrarse en la recuperación de datos, sigue siendo responsable de determinar cómo y cuándo se debe crear el contexto. Quiero mover las decisiones sobre cómo y cuándo crear el contexto más arriba en el flujo y simplemente dejar que mi repositorio use el contexto insertado. Y por ello refactorizaré de nuevo para pasar un contexto creado en otra parte:

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

Ahora el repositorio está de forma independiente. No tengo que pelearme con él para crear el contexto. El repositorio no se preocupa de cómo se configure el contexto, cuándo se cree o cuándo se descarte. Esto también ayuda a que la clase siga otro principio orientado a objetos, el principio de responsabilidad única (Single Responsibility Principle), ya que deja de ser responsable de administrar contextos de EF además de realizar solicitudes de base de datos. Cuando trabajo en la clase del repositorio, me puedo centrar en las consultas. También puedo realizar pruebas con más facilidad, ya que mis pruebas pueden guiar esas decisiones y no me confundiré con un repositorio que esté diseñado para utilizarse de una forma que no esté en sintonía con la forma en que puedo querer usarlo en pruebas automatizadas.

Existe otro problema en mi ejemplo original, ya que codifiqué de forma rígida la cadena de conexión con el elemento DbContext. Mi justificación en aquel momento fue que se trataba "tan solo de una demo"; obtener la cadena de conexión de la aplicación en ejecución (la aplicación de ASP.NET 5) para el proyecto de EF6 era complicado y estaba centrado en otros aspectos. Sin embargo, a medida que refactorice este proyecto, podré aprovechar IoC para pasar la cadena de conexión desde la aplicación en ejecución. Vea cómo lo hago más adelante en este artículo.

Permita que ASP.NET 5 inserte el elemento NinjaContext

Pero, ¿dónde muevo la creación de NinjaContext? El controlador usa el repositorio. Sin lugar a dudas, no quiero introducir EF en el controlador para pasarlo a una nueva instancia del repositorio. Eso provocaría un caos (algo parecido a esto):

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

De la misma manera, obligo al controlador a que tenga presente EF, este código ignora los problemas de crear instancias de objetos dependientes que acabo de resolver en el repositorio. El controlador está creando instancias de la clase del repositorio directamente. Sencillamente quiero que use el repositorio, no que se preocupe sobre cómo ni cuándo debe crearlo o cuándo debe descartarlo. De la misma forma que inserté la instancia de NinjaContext en el repositorio, quiero insertar una instancia de repositorio lista para usar en el controlador.

Una versión más limpia del código de la clase del controlador tendría un aspecto similar a este:

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

Diseño de la creación de objetos con contenedores de IoC

Como estoy trabajando con ASP.NET 5, en lugar de extraer en StructureMap, sacaré partido de la compatibilidad integrada de ASP.NET 5 con DI. No solo se han creado muchas de las nuevas clases de ASP.NET para que acepten objetos que se inserten, sino que ASP.NET 5 tiene una infraestructura de servicio que puede coordinar qué objetos van a qué lugar: un contenedor de IoC. También permite especificar el ámbito de los objetos (cuándo se deben crear y descartar) que se crearán e insertarán. Una forma más sencilla de iniciarse es trabajar con la compatibilidad integrada.

Antes de usar la compatibilidad con DI de ASP.NET 5 para ayudarme a insertar mis elementos NinjaContext y NinjaRepository según sea necesario, veamos el aspecto que tiene cuando se insertan clases de EF7, ya que EF7 tiene métodos integrados para conectarlo con la compatibilidad con DI de ASP.NET 5. La clase startup.cs que forma parte de un proyecto de ASP.NET 5 estándar tiene un método llamado ConfigureServices. Es ahí donde se indica a la aplicación cómo se quieren conectar las dependencias para que pueda crear y después insertar los objetos adecuados en los objetos que los necesitan. A continuación se muestra ese método, con todo eliminado excepto una configuración para EF7:

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

Al contrario que mi proyecto, que usa mi modelo basado en EF6, el proyecto donde esta configuración se ejecuta depende de EF7. Estos siguientes párrafos describen lo que sucede en el código.

Como se especificó .MicrosoftSqlServer de EntityFramework en su archivo project.json, el proyecto hace referencia a todos los ensamblados de EF7 relevantes. Uno de ellos, el ensamblado EntityFramework.Core, proporciona el método de extensión AddEntityFramework a IServiceCollection, lo que me permite agregar el servicio de Entity Framework. El archivo dll .MicrosoftSqlServer de EntityFramework ofrece el método de extensión AddSqlServer que se anexa a AddEntityFramework. Esto introduce el servicio de SqlServer en el contenedor de IoC, de forma que EF sabrá que debe usarlo cuando busque un proveedor de base de datos.

AddDbContext procede del núcleo de EF. Este código agrega la instancia de DbContext especificada (con las opciones especificadas) al contenedor integrado de ASP.NET 5. A cualquier clase que solicite un elemento DbContext en su constructor (y que ASP.NET 5 esté construyendo) se le proporcionará el elemento DbContext cuando se cree. Por tanto, este código agrega el elemento NinjaContext como un tipo conocido del que el servicio creará instancias según sea necesario. Además, el código especifica que cuando se construya un elemento NinjaContext, debe usar la cadena encontrada en el código de configuración (que en este caso procede de un archivo appsettings.json de ASP.NET 5, que ha creado la plantilla del proyecto) como una opción de configuración de SqlServer. Como ConfigureService se ejecuta en el código de inicio, cuando un código de la aplicación espera un elemento NinjaContext, pero no se proporciona ninguna instancia específica, ASP.NET 5 creará una instancia de un nuevo objeto NinjaContext con la cadena de conexión especificada y la pasará.

Y todo eso está muy bien integrado en EF7. Lamentablemente, nada de esto existe para EF6. Pero ahora que tiene una idea sobre cómo funcionan los servicios, el patrón para agregar el elemento NinjaContext de EF6 a los servicios de la aplicación debería tener sentido.

Adición de servicios no creados para ASP.NET 5

Además de los servicios que están creados para funcionar con ASP.NET 5, que tienen interesantes extensiones como AddEntityFramework y AddMvc, es posible agregar otras dependencias. La interfaz IServicesCollection ofrece un método Add estándar, junto con un conjunto de métodos para especificar la duración del servicio que se agregue: AddScoped, AddSingleton y AddTransient. Me centraré en AddScoped para mi solución porque en su ámbito está la duración de la instancia solicitada para cada solicitud de la aplicación de MVC en la que quiero usar mi proyecto EF6Model. La aplicación no intentará compartir una instancia entre solicitudes. Esto emulará lo que conseguía originalmente con la creación y el descarte de mi elemento NinjaContext dentro de cada acción de controlador, ya que cada acción de controlador respondía a una única solicitud.

Recuerde que dispongo de dos clases que necesitan que se inserten objetos. La clase NinjaRepository necesita NinjaContext, y NinjaController necesita un objeto NinjaRepository.

En el método de ConfigureServices startup.cs comienzo agregando:

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

Ahora mi aplicación tiene en cuenta estos tipos y creará una instancia de ellos cuando lo solicite el constructor de otro tipo.

Cuando el constructor del controlador espera un elemento NinjaRepository que se pase como un parámetro:

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

pero no se ha pasado ninguno, el servicio creará un elemento NinjaRepository sobre la marcha. Esto se conoce como "inserción de constructores". Cuando el elemento NinjaRepository espera una instancia de NinjaContext y no se ha pasado ninguna, el servicio sabrá que también debe crear una instancia de eso.

¿Recuerda la modificación de la cadena de conexión en mi elemento DbContext, que indiqué anteriormente? Ahora puedo indicar al método AddScoped que construya el elemento NinjaContext en relación con la cadena de conexión. Pondré la cadena de nuevo en el archivo appsetting.json. A continuación se muestra la sección adecuada de ese archivo:

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

Tenga en cuenta que JSON no admite el ajuste de línea, por lo que la cadena que comience con Server= no se puede partir en el archivo JSON. Aquí se separa exclusivamente para que se pueda leer mejor.

He modificado el constructor de NinjaContext para que admita una cadena de conexión y la use en la sobrecarga de DbContext, que también acepta una cadena de conexión:

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

Ahora puedo indicar a AddScoped que cuando vea un elemento NinjaContext, debe construirlo con esa sobrecarga y pasar el elemento Ninja­ConnectionString que se encuentra en appsettings.json:

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

Con este último cambio, la solución que estropeé mientras la refactorizaba ahora funciona de un extremo a otro. La lógica de inicio configura la aplicación para insertar el repositorio y el contexto. Cuando la aplicación se redirige al controlador predeterminado (que usa el repositorio que utiliza el contexto), los objetos necesarios se crean sobre la marcha y los datos se recuperan de la base de datos. Mi aplicación de ASP.NET 5 saca provecho de la DI integrada para interactuar con un ensamblado antiguo donde usé EF6 para crear mi modelo.

Interfaces para la flexibilidad

Existe una última mejora posible, que es aprovechar las interfaces. Si hay una posibilidad de que pudiera querer usar una versión distinta de mi clase NinjaContext o NinjaRepository, puedo implementar interfaces en todas partes. No puedo predecir la necesidad de disponer de una variación de NinjaContext, por lo que solo crearé una interfaz para la clase del repositorio.

Como se muestra en la Figura 1, el elemento NinjaRepository ahora implementa un contrato INinjaRepository.

Figura 1. NinjaRepository con una interfaz

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

El controlador de la aplicación de ASP.NET 5 MVC ahora utiliza la interfaz de INinjaRepository en lugar de la implementación concreta, NinjaRepository:

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

He modificado el método AddScoped para que el elemento NinjaRepository indique a ASP.NET 5 que use la implementación adecuada (actualmente NinjaRepository) cuando la interfaz sea necesaria:

services.AddScoped<INinjaRepository, NinjaRepository>();

Cuando sea el momento de una nueva versión o si estoy usando una implementación diferente de la interfaz en una aplicación distinta, puedo modificar el método AddScoped para usar la implementación correcta.

Aprenda haciéndolo, no se limite a copiar y pegar

Tengo que darle las gracias a Miller por retarme a refactorizar mi solución. Naturalmente, mi refactorización no fue tan fluida como podría parecer por lo que he escrito. Como no me limité a copiar la solución de otra persona, al principio hice algunas cosas mal. Gracias a aprender lo que estaba mal y descubrir el código correcto, conseguí mi objetivo en un proceso que fue muy beneficioso para mis conocimientos sobre DI e IoC. Espero que mis explicaciones les hayan sido igual de beneficiosas sin tener que quebrarse la cabeza tanto como yo lo hice.


Julie Lerman es una Microsoft MVP, mentora y consultora de .NET que vive en las colinas de Vermont. Puede encontrarla haciendo presentaciones sobre el acceso a datos y otros temas de .NET a grupos de usuarios y en conferencias en todo el mundo. Su blog es thedatafarm.com/blog y es la autora de "Programming Entity Framework", así como de una edición de Code First y una edición de DbContext, de O’Reilly Media. Sígala en Twitter en @julielerman y vea sus cursos de Pluralsight en juliel.me/PS-Videos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Steve Smith
Steve Smith (@ardalis) es un empresario y desarrollador de software con una gran pasión por la creación de software de calidad. Steve ha publicado varios cursos en Pluralsight, que tratan sobre DDD, SOLID, patrones de diseño y arquitectura de software. Es MVP de Microsoft, ponente habitual en las conferencias de desarrolladores, autor, mentor y formador. Descubra la manera en que Steve puede ayudar a su equipo o proyecto en ardalis.com.