martes, 24 de febrero de 2015
Nota [05/10/2015] - Hay una versión actualizada de este post.
La inyección de dependencias ha sido un tema recurrente en Variable not found desde hace bastante tiempo, y dadas las grandes novedades incluidas en ASP.NET 5 al respecto estaba deseando hincarle el diente y publicar algo sobre el tema.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 ;)
Pero permitidme una nota antes de empezar: 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.
Ah, y recordad: todo lo que vamos a ver podría cambiar conforme vaya avanzando el desarrollo del producto, pero más o menos son cosas que parecen relativamente estables desde hace algún tiempo.
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:
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.Framework.DependencyInjection
, establece un contrato bastante simple donde sólo encontramos operaciones que permiten registrar servicios partiendo de descriptores (objetos IServiceDescriptor
):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:
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.Framework.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 instancia concreta:
Lo que le estamos diciendo al contenedor, más o menos es lo siguiente: “hey, cuando algún componente requiera una instancia del tipoIMyService
, suminístrale directamente este objetomyService
que te estoy proporcionando”. Obviamente, en memoria sólo existirá una copia del objeto, la que hemos instanciado para configurar el servicio.
- Registro de interfaz a objeto volátil:
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:
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”:
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, pero 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.
ConfigureServices()
podría tener una pinta como la siguiente: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.
- Inyección de propiedades, es otro mecanismo frecuente para indicar al framework qué dependencias presenta un componente. En este caso, lo que hacemos es crear propiedades del tipo deseado y decorarlas con el atributo
[Activate]
. ASP.NET 5 las detectará y les cargará automáticamente instancias atendiendo al registro de servicios.
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, aunque sólo funcionarán en contadas ocasiones. De hecho, hasta el momento sólo he visto que se pueda hacer en un caso concreto, que es el método
Invoke()
de los middlewares personalizados, como en el siguiente ejemplo, donde indicamos en este método que vamos a usar una instancia deINotificationServices
:
No sé si en el futuro se ampliará su ámbito de utilización, pero de momento es así de cortito. En cualquier caso, a priori no me parecería excesivamente complicado hacerlo por ejemplo con acciones de controlador, como ya vimos para versiones anteriores del framework en el post “Inyección de parámetros en acciones ASP.NET MVC (I)” de hace algún tiempo.
- 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:
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.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario