martes, 6 de octubre de 2015
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 el que todavía se denominaba 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 Core 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 Core 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 Core”, 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 Core MVC 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 Core 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 Core, Identity y 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 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 Core 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 Core 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 [Actualizado Nov-2015] Hasta la Release Candidate 1 podíamos utilizar inyección de propiedades en el controlador utilizando el atributo
[FromServices]
, pero esta funcionalidad ha sido descartada en la versión final. Más información aquí: https://github.com/aspnet/Mvc/issues/3507.
- 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 Core, 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 Core? 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 Core 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.
12 Comentarios:
Buen post m eha gustado mucho. He llegado a él investigando sobre las DI y también sobre el uso de la session state de asp vnext.
Querría si me permites , preguntarte si yo tengo un classe por ejemplo llamada SessionTools en la que le inyecto el IHttpContextAccessor para obtener una instancia de HttpContext.Session en un constructor i luego la registro con services.AddTransient en configurationServices del startup.cs , entonces como podria recuperar la instancia de de la clase para usarla desde otra clase que no fuera un controlador ?
Gracias
Hola!
No sé si te he entendido bien, pero intento responder ;)
Si la clase desde la que quieres usarla está registrada también en el contenedor de inyección de dependencias, simplemente deberías añadir un parámetro hacia tu clase Sessiontools (o el interfaz que implemente) en su constructor. O también puedes hacerlo con el "antipatrón" service locator que se comenta en el post, pero está más feo ;)
Saludos!
Disculpa quizás no me haya explicado muy bién. Lo intento otra vez.
TEngo la clase SessionTools:
public class SessionTools
{
private readonly IHttpContextAccessor _httpContextAccessor;
private ISession _session => _httpContextAccessor.HttpContext.Session;
public SomeOtherClass(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
.......
}
Com opuedes ver recibe por inyeccion el httpContextAccessor del tipo IHttpContextAccessor .
Ok, bien ahora ebn el startuo en el metodo configurationServices registro la clase mediante el addtransient.
services.addTransients();
fijate que no tengo interfaz ni nada del estilo tal cual.
Ahora viene mi duda:
tengo la clase Por ejemplo persona, como accederia a SessionTools a su instancia desde la classe pesona en el metodo MiMetodo que no es un controlador. Sin usar el servicelocator?
public class persona
{
public void MiMetodo()
{
//// Como accedo aqui sin usar el service locator a la instancia de SessionTools:
->
}
Tomo nota de que service locator podria o funciona pero como bien dices es feo, por no decirte que no lo veo lògico.
Marc.
Disculpa en el último comentario que te envíe al poner el código del services.AddTransient con los <> se comió el nombre de la clase por lo que ahí entre los signos < > iria SessionTools()
Hola,
una vez has registrado SessionTools en los servicios del sistema, si por ejemplo quieres usarlo desde una clase controlador bastaría con añadir un parámetro de este tipo, por ejemplo:
public HomeController(SessionTools sessionTools)
{
_sessionTools = sessionTools;
}
Esto es así porque el framework es el que se encarga de instanciar el controlador y, para hacerlo, busca en el contenedor de DI instancias que satisfagan los parámetros de su constructor.
Si quieres que la clase Persona tenga acceso a SessionTools, bastaría con hacer lo mismo, siempre que sea el framework el que instancie la clase Persona y ésta esté registrada en el DI.
Por ejemplo, si tienes la siguiente estructura todo funcionaría porque cuando el framework va a crear HomeController crearía una instancia de Persona, pero para ello a su vez obtendría también una instancia de SessionTools.
public class HomeController: Controller
{
public HomeController(Persona persona)
{
_persona = persona;
}
...
}
public class Persona
{
public Persona(SessionTools sessionTools)
{
...
}
}
Espero haberte aclarado algo ;)
Saludos!
Ok perfecto comprendo todo lo que dices.
pero permiteme que lo complique... ;)
Supón ahora que tienes la classe static Tools_Persona donde tienes un metodo public static que se llama crear_persona(string nombre) que devuelve una instancia de Persona, por lo que persona tendría un constructor que por ejemplo pasándole el nombre de tipo string instanciaria la clase persona, podría en ese constructor de persona acceder a la SessionUtility sin usar el services locator ? y si es que sí como se harñia (a eso me referia sin estar en un controlador, ni a tener que pasar el IhttpAccessor inyectado de forma manual en la clase persona usando su constructor)
Por cierto gracias por tu ayuda. :)
Hola,
en cuanto introduces una clase o método estático por medio, como sea factoría, complicas la inyección de dependencias y tienes que ingeniártelas de otra forma para conseguirlo.
Persona sólo puede recibir en el constructor la instancia de SessionUtility si alguien se la envía, ya sea el sistema de DI, o que lo hagas tú desde cualquier otro punto de la aplicación.
Una forma, por ejemplo, sería hacer que la factoría "Tools_Persona" sea de instancia y esté registrado en el DI, y que por tanto sea esta clase la que recibe la instancia de SessionTools que hemos comentado antes en su constructor. Después, cuando la factoría crea la instancia de Persona desde el método CrearPersona(), le suministraría también las dependencias.
public class ToolsPersona
{
...
public ToolsPersona(SessionTools sessionTools)
{
_sessionTools = sessionTools;
}
public Persona CrearPersona(string nombre)
{
var persona = new Persona(nombre, _sessionTools);
}
}
En resumen: si no hay ningún componente que suministre a tu clase Persona una instancia de SessionTools, tendrás que recurrir al service locator.
Saludos!
Hola muy buen día.
Excelente articulo, quizás me puedas ayudar con el siguiente escenario:
Trabajo en VS2015 y en mi solución existen 3 proyectos Net Core 1.1.0:
- un proyecto MVC como webservice usando restful.
- un proyecto de tipo librería para el uso de entidades o modelos.
- un proyecto de tipo librería para la comunicación con la base de datos.
A continuación un ejemplo de un método desarrollado en la librería de acceso a datos:
---
namespace creaworlds.Framework.WebApi.DataAccess
public static partial class Customer {
public static bool Exists(string id) {
var exists = false;
if(id == null) {
throw new Exception("The id can't be null");
} else {
//leemos el archivo appsettings.json y determinamos
//la cadena de conexión a la base de datos
}
return exists;
}
}
---
A continuación mi archivo Startup.cs en el proyecto MVC:
---
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace creaworlds.Framework.WebApi.WebService
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
}
}
}
---
La pregunta es: ¿Cómo puedo inyectar la configuración leída en el Startup.cs del proyecto MVC directamente en la case static de mi proyecto de acceso a datos?
Hola, Anthony!
La fórmula "normal" de conseguir acceso a la configuración es utilizando el sistema de inyección de dependencias, pero esto no te será posible desde una clase estática.
Mi recomendación sería convertir esta clase en una de instancia, registrarla en el inyector y hacer que reciba una referencia hacia tu configuración, por ejemplo como se indica en este post: http://www.variablenotfound.com/2017/02/refresco-automatico-de-setting-tipados.html.
Si aún así quieres continuar usando el enfoque estático, siempre podrías construir de nuevo el objeto de configuración desde esta clase y usarlo para acceder a las propiedades:
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
var config = builder.Build();
var algo = config["Algo"]; // Acceso a tus propiedades de settings.
Espero que te sea de ayuda.
Un saludo!
Lo que hice finalmente es inyectar en mis controladores la configuración y enviarla a cada método estático.
Te agradezco infinitamente tu apoyo.
Hola amigo, lo primero decirte que me encanta tu blog y que lo sigo desde hace mucho tiempo.
Tengo una duda sobre la inyección de dependencias en asp.net core porque no tengo claro como solucionar un caso que me he encontrado. Tengo dos interfaces IRepository (sus miembros son los distintos dbset) y IUnitOfWork (que solo tiene el método SaveChangesAsync) y ambos deben estar asociados a la misma instancia de un contexto de datos de entity framewok. El problema es que si los registro de forma independiente la instancia que se inyecta es distinta y no funciona bien. Es decir, esto no funciona:
services.AddScoped();
services.AddScoped();
public class Ejemplo
{
public Ejemplo(IRepositories repos, IUnitOfWork uow) // inyectados
{
// quiero que repos = uow
}
}
No se si me he explicado bien, si te parece te voy a enviar un mensaje por el formulario de contacto.
Quedo muy agradecido de antemano.
Recibido y respondido :) De todas formas, me parece una pregunta interesante y escribiré un post al respecto.
Muchas gracias!
Enviar un nuevo comentario