Hace bastantes meses, allá por febrero, publiqué el post “Inyección de dependencias en ASP.NET 5”, donde describía el sistema integrado de inyección de dependencias que se estaba construyendo en ASP.NET 5.
Sin embargo, las cosas han cambiado un poco desde entonces, por lo que he pensado que sería buena idea reeditar el artículo y actualizarlo al momento actual, con la beta 8 de ASP.NET 5 a punto de publicarse.
<disclaimer>Aunque el producto está cada vez más estable y completo, aún podrían cambiar cosas antes de la versión definitiva, incluso antes de tener la release candidate en la calle.</disclaimer>
Si no sabes aún lo que es inyección de dependencias, el resto del post te sonará a arameo antiguo. Quizás podrías echar previamente un vistazo a este otro, Desacoplando controladores ASP.NET MVC, paso a paso, que aunque tiene ya algún tiempo, creo que todavía es válido para que podáis ver las ventajas que nos ofrece esta forma de diseñar componentes.
Para los perezosos, podríamos resumir el resto del post con una frase: ASP.NET 5 viene construido con inyección de dependencias desde su base, e incluye todas las herramientas necesarias para que los desarrolladores podamos usarla en nuestras aplicaciones de forma directa, en muchos casos sin necesidad de componentes de terceros (principalmente motores de IoC que usábamos hasta ahora). Simplemente, lo que veremos a continuación es cómo utilizar esta característica ;)
1. Cómo registrar dependencias
Como ya hemos visto por aquí en el post “La clase Startup en ASPNET 5”, completado por el gran Unai en este otro, en esta clase existe un método llamadoConfigureServices()
que es invocado por el framework durante el arranque para darnos oportunidad de configurar los servicios o dependencias utilizadas por nuestra aplicación. Esta llamada se producirá antes que Configure()
, lo que da la oportunidad de registrar las dependencias antes de comenzar a configurar el pipeline.Bueno, en realidad, como se comenta en los posts citados, el framework intentará llamar primero al método
Configure[EnvironmentName]Services()
, y sólo si no existe usará ConfigureServices()
. Por ejemplo, si ejecutamos nuestra aplicación en un entorno denominado “Development”, el framework intentará primero ejecutar el método ConfigureDevelopmentServices()
de la clase Startup
, y, si no existe, ejecutará ConfigureServices()
.La firma del método de configuración es la siguiente:
public void ConfigureServices(IServiceCollection services) { // Register services here }El parámetro de tipo
IServiceCollection
que recibimos representa a la colección que almacenará los servicios o dependencias a usar por nuestro sistema. Este interfaz, definido en el espacio de nombres Microsoft.Extensions.DependencyInjection
, establece un contrato bastante simple donde sólo encontramos operaciones que permiten registrar servicios partiendo de descriptores (objetos IServiceDescriptor
):public interface IServiceCollection : IList<ServiceDescriptor> { }Normalmente no usaremos directamente los métodos definidos por este interfaz, sino métodos de extensión de
IServiceCollection
bastante más cómodos de utilizar, y que normalmente pertenecerán a uno de los siguientes tipos:- Extensores proporcionados por frameworks y middlewares que registran los servicios usados internamente por estos componentes.
- Extensores para añadir servicios personalizados manualmente.
1.1. Extensores proporcionados por frameworks/middlewares
Dentro del primer grupo están los extensores que acompañan de serie a los frameworks o middlewares. Por ejemplo, ASP.NET MVC 6 está construido internamente haciendo uso extensivo de inyección de dependencias, pero obviamente, para que funcione, es necesario configurarlo y registrar sus dependencias antes de que el framework comience a funcionar. Lo mismo ocurre con Entity framework, SignalR, Identity, y muchos otros middlewares, frameworks o componentes.Por esta razón es por lo que en la plantilla de proyectos ASP.NET 5 encontramos el siguiente código por defecto:
public void ConfigureServices(IServiceCollection services) { // Add entity Framework to the services container. services.AddEntityFramework() .AddSqlServer() .AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:DefaultConnection:ConnectionString"]) ); // Add Identity services to the services container. services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // Add MVC services to the services container. services.AddMvc(); }Los métodos
AddEntityFramework()
, AddIdentity()
y AddMvc()
son proporcionados respectivamente por Entity Framework, ASP.NET Identity y ASP.NET MVC para facilitarnos el registro de sus dependencias internas. De hecho, si pensamos utilizar alguno de estos frameworks es absolutamente imprescindible realizar estas llamadas antes de que éstos se utilicen desde nuestra aplicación.Así, por ejemplo, si intentamos ejecutar una aplicación ASP.NET MVC sin haber añadido sus servicios mediante el extensor
AddMvc()
, el sistema reventará directamente al arrancar, mostrando una pantalla como la que vemos en el latera. Es lógico, porque sin los servicios internos de MVC correctamente registrados no funcionaría absolutamente nada.Entity framework o Identity son menos exigentes en un principio, pero se producirán errores y excepciones en cuanto intentemos utilizarlos en algún punto de la aplicación.
Por tanto, tened siempre en cuenta que cuando añadamos nuevos frameworks, middlewares o componentes a nuestras aplicaciones es probable que necesitemos registrar también sus servicios para que funcionen correctamente.
Estos extensores siguen una convención en cuanto a su nombrado, por lo que serán fácilmente descubribles de forma intuitiva. Por ejemplo, ¿qué podríamos esperar si vamos a añadir SignalR a nuestra aplicación? Está claro, el registro de servicios se realizará mediante una llamada al extensor
AddSignalR()
.También, a pesar de que cada extensor está implementado en el ensamblado del framework o middleware que lo proporciona, se definen siempre en el espacio de nombres
Microsoft.Extensions.DependencyInjection
para que intellisense pueda ayudarnos a descubrirlos con más facilidad.Estos métodos suelen presentar además un interfaz fluido que facilita la configuración del servicio cuando es necesario. Ejemplos de ello son las cadenas de llamadas
AddEntityFramework().AddSqlServer().AddDbContext()
o AddIdentity().AddEntityFrameworkStores()
que hemos visto anteriormente.En definitiva, debemos tener en cuenta estas convenciones si pensamos a crear nuestros propios componentes y queremos proporcionar a los desarrolladores una experiencia de codificación similar a la que encuentran de serie.
1.2. Extensores para añadir servicios personalizados
El registro de servicios personalizados lo realizaremos de forma muy similar a como hemos hecho hasta ahora utilizando contenedores de inversión de control como Unity, Autofac, Ninject, StructureMap u otros, normalmente asignando interfaces a clases concretas. La única diferencia, en lugar de utilizar los contenedores proporcionados por estos componentes utilizaremos el propioIServiceCollection
que nos llega como parámetro.Aunque hay algunas opciones adicionales, normalmente utilizaremos una de las siguientes fórmulas para registrar servicios:
- Registro de interfaz a objeto volátil:
services.AddTransient<IMyService, MyService>();
Aquí, cada vez que un componente solicite una instancia deIMyService
, el contenedor instanciará un nuevo objeto de tipoMyService
. Por tanto, en memoria podrán existir múltiples instancias de objetos de tipoMyService
, una por cada componente que lo haya requerido. - Registro de interfaz a objeto Singleton:
services.AddSingleton<IMyService, MyService>();
En este caso, cuando un componente solicita una instancia deIMyService
, el contenedor instanciará un objeto de tipoMyService
, pero a diferencia del caso anterior, si otro componente requiere una instancia del mismo tipo se le suministrará la creada anteriormente, por lo que en memoria sólo existirá una copia de ella. Vaya, un Singleton de los de toda la vida. - Registro de interfaz a objeto Singleton con vida “per-request”:
services.AddScoped<IMyService, MyService>();
Es similar al caso anterior, es decir, un objeto Singleton creado cuando lo solicite el primer componente y compartido con todos los componentes que lo requieran a continuación en el contexto de la misma petición. Pero, además, con la particularidad de que será liberado explícitamente por el framework al terminar el ámbito de ejecución, o scope, en el que se enmarca el proceso actual. En la práctica, esto significa que el framework recordará los objetos que ha ido creando según este modelo de funcionamiento y cuando finalice el proceso de la petición invocará a sus respectivos métodosDispose()
para liberar recursos.
Este escenario es muy frecuente en el mundo web, donde queremos que al finalizar la petición se liberen de forma automática los recursos que hayamos podido utilizar, como conexiones a bases de datos.
- Registro de interfaz a factoría de objetos:
services.AddTransient<IMyService>(provider => new MyService());
Lo que le estamos diciendo al contenedor, poco más o menos es lo siguiente: “hey, cuando algún componente requiera una instancia del tipoIMyService
, usa este delegado (la función lambda especificada) como factoría para obtener el objeto”.
ConfigureServices()
podría tener una pinta como la siguiente:public void ConfigureServices(IServiceCollection services) { // Add entity Framework to the services container. services.AddEntityFramework() .AddSqlServer() .AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:DefaultConnection:ConnectionString"]) ); // Add Identity services to the services container. services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // Add MVC services to the services container. services.AddMvc(); // Add app services services.AddScoped<IInvoiceServices, InvoiceServices>(); services.AddScoped<ICustomerServices, CustomerServices>(); services.AddScoped<IPaymentServices, PaymentServices>(); services.AddScoped<INotificationServices, NotificationServices>(); services.AddScoped<IPortalServices, PortalServices>(); // ... }Obviamente, en aplicaciones de cierto tamaño este método podría extenderse demasiado y probablemente sería más conveniente el estructurar este código de una forma apropiada, por ejemplo moviendo el registro a archivos y clases independientes, creando extensores, o de la forma que veamos más conveniente.
Por último, comentar que además de los extensores comentados anteriormente hay otros mecanismos que nos permiten un mayor grado de control sobre los procesos y gestión del ciclo de vida de las dependencias, pero con lo visto hasta el momento tenemos lo suficiente como para poder desarrollar aplicaciones con ciertas garantías.
2. Cómo usar dependencias
Vamos a ver ahora el proceso desde el otro lado, es decir, desde el punto de vista de los componentes que requieren para poder funcionar de los servicios prestados por otros componentes.ASP.NET 5 ofrece de momento las siguientes fórmulas para inyectar dependencias en componentes:
- Inyección de dependencias en parámetros de constructor, que para mi gusto es la forma más limpia y recomendable de utilizar inyección de dependencias. Consiste simplemente en indicar en el constructor de una clase qué servicios necesita ésta para funcionar, realizar una copia local de éstos, y ponerlos a disposición de los miembros internos para que éstos puedan realizar sus tareas.
La siguiente porción de código muestra un controlador MVC cuyo constructor muestra que esta clase depende de servicios de pago y notificaciones para realizar su cometido, con un ejemplo de uso en el métodoCancel()
. ASP.NET 5 se encargará de suministrarle las instancias indicadas a partir de los registros realizados en elIServiceCollection
descrito anteriormente.
public class PaymentController: Controller { private readonly IPaymentServices _paymentServices; private readonly INotificationServices _notificationServices; public PaymentController(IPaymentServices paymentServices, INotificationServices notificationServices) { _paymentServices = paymentServices; _notificationServices = notificationServices; } public async Task<IActionResult> Cancel(string paymentId) { await _paymentServices.CancelPayment(paymentId); await _notificationServices.NotifyPaymentCancellation(paymentId); return View(); } // ... }
- Inyección de propiedades, es otro mecanismo frecuente para indicar al framework qué dependencias presenta un componente. En este caso, lo que haremos será crear propiedades públicas del tipo deseado y decorarlas con el atributo
[FromServices]
. ASP.NET 5 las detectará y les cargará automáticamente instancias atendiendo al registro de servicios.
public class PaymentController : Controller { [FromServices] public IPaymentServices PaymentServices { get; set; } [FromServices] public INotificationServices NotificationServices { get; set; } public async Task<IActionResult> Cancel(string paymentId) { await PaymentServices.CancelPayment(paymentId); await NotificationServices.NotifyPaymentCancellation(paymentId); return View(); } // ... }
Obviamente la carga de dependencias la realizará el framework después de haber creado la instancia del objeto, por lo que en el constructor no podremos utilizar estos miembros.
- Inyección de dependencias en parámetros de métodos, que permite un control más granular sobre el uso de las dependencias. En el siguiente ejemplo, vemos cómo podemos introducir en una acción MVC parámetros que serán cargados desde el contenedor de servicios:
public class PaymentController : Controller { public async Task<IActionResult> Cancel( string paymentId, [FromServices] IPaymentServices paymentServices, [FromServices] INotificationServices notificationServices) { await paymentServices.CancelPayment(paymentId); await notificationServices.NotifyPaymentCancellation(paymentId); return View(); } // ... }
- Y, aunque muchos lo consideran un antipatrón, para los escenarios en los que otros mecanismos no son posibles podríamos utilizar el patrón Service Locator con el contenedor de servicios. En este caso, lo único que tendríamos que hacer es obtener una instancia de
IServiceProvider
, el proveedor de servicios interno de ASP.NET, e invocar a su métodoGetService()
para obtener los servicios que necesitemos:
public class PaymentController : Controller { private readonly IServiceProvider _provider; public PaymentController(IServiceProvider serviceProvider) { _provider = serviceProvider; } public async Task<IActionResult> Cancel(string paymentId) { var payment = _provider.GetService(typeof(IPaymentServices)) as IPaymentServices; var notification = _provider.GetService(typeof (INotificationServices)) as INotificationServices; await paymentServices.CancelPayment(paymentId); await notificationServices.NotifyPaymentCancellation(paymentId); return View(); } // ... }
Pero antes de cerrar, una última reflexión. Visto este nuevo panorama, en el que el propio framework ofrece muchas de las funcionalidades que hasta ahora se delegaban a contenedores de inversión de control, ¿tiene sentido utilizar contenedores IoC “tradicionales” como Unity, Ninject o Autofac en aplicaciones ASP.NET 5? Pues como dijo mi amigo gallego, sí… o igual no ;)
Desde mi punto de vista, probablemente la mayoría de aplicaciones convencionales no tendrán necesidad real de usarlos, puesto que ASP.NET ofrece todo lo que necesitaremos para cubrir los escenarios más habituales. A falta de lo que la experiencia vaya enseñándonos por el camino, creo que la tendencia natural será comenzar los proyectos usando las herramientas proporcionadas por el framework, y migrar a contenedores más versátiles cuando realmente sean necesarios, por ejemplo si queremos usar convenciones, mapeos basados en archivos de configuración, parametrización o selección dinámica de constructores, ciclos de vida no incluidos (por ejemplo, el scope por thread), u otras características más avanzadas.
Publicado en Variable not found.
from Variable not found http://www.variablenotfound.com/2015/10/inyeccion-de-dependencias-en-aspnet-5.html
via IFTTT