Resolución de instancias con ASP.NET Core DI

302

¿Cómo resuelvo manualmente un tipo usando el marco de inyección de dependencia incorporado ASP.NET Core MVC?

Configurar el contenedor es bastante fácil:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddTransient<ISomeService, SomeConcreteService>();
}

Pero, ¿cómo puedo resolver ISomeServicesin realizar la inyección? Por ejemplo, quiero hacer esto:

ISomeService service = services.Resolve<ISomeService>();

No hay tales métodos en IServiceCollection.

Dave New
fuente
1
posible duplicado de Cómo resolver instancias dentro de ConfigureServices en ASP.NET 5
Muhammad Rehan Saeed
3
¿Desea resolverlos en el ConfigureServices()método (con IServiceCollection) o en cualquier lugar de la aplicación?
Henk Mollema
2
@HenkMollema: en cualquier lugar dentro del inicio en realidad.
Dave New

Respuestas:

486

La IServiceCollectioninterfaz se usa para construir un contenedor de inyección de dependencia. Una vez que está completamente construido, se compone de una IServiceProviderinstancia que puede usar para resolver servicios. Puede inyectar un IServiceProvideren cualquier clase. Los IApplicationBuildery HttpContextlas clases pueden proporcionar el proveedor de servicio, así, a través de sus ApplicationServiceso RequestServicespropiedades respectivamente.

IServiceProviderdefine un GetService(Type type)método para resolver un servicio:

var service = (IFooService)serviceProvider.GetService(typeof(IFooService));

También hay varios métodos de extensión de conveniencia disponibles, como serviceProvider.GetService<IFooService>()(agregar un usingpara Microsoft.Extensions.DependencyInjection).

Resolviendo servicios dentro de la clase de inicio

Inyectando dependencias

Proveedor de alojamiento de servicio del tiempo de ejecución puede inyectar ciertos servicios en el constructor de la Startupclase, tales como IConfiguration, IWebHostEnvironment( IHostingEnvironmenten pre-3.0 versiones), ILoggerFactoryy IServiceProvider. Tenga en cuenta que esta última es una instancia creada por la capa de alojamiento y contiene solo los servicios esenciales para iniciar una aplicación .

El ConfigureServices()método no permite inyectar servicios, solo acepta un IServiceCollectionargumento. Esto tiene sentido porque ConfigureServices()es donde registra los servicios requeridos por su aplicación. Sin embargo, puede utilizar los servicios inyectados en el constructor de la startup aquí, por ejemplo:

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
    // Use Configuration here
}

Cualquier servicio registrado en ConfigureServices()puede ser inyectado en el Configure()método; Puede agregar un número arbitrario de servicios después del IApplicationBuilderparámetro:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IFooService>();
}

public void Configure(IApplicationBuilder app, IFooService fooService)
{
    fooService.Bar();
}

Resolver dependencias manualmente

Si necesita resolver los servicios de forma manual, debe utilizar preferiblemente el ApplicationServicesproporcionado por IApplicationBuilderen el Configure()método:

public void Configure(IApplicationBuilder app)
{
    var serviceProvider = app.ApplicationServices;
    var hostingEnv = serviceProvider.GetService<IHostingEnvironment>();
}

Es posible pasar y usar directamente un IServiceProvideren el constructor de su Startupclase, pero como se indica arriba , contendrá un subconjunto limitado de servicios y, por lo tanto, tiene una utilidad limitada:

public Startup(IServiceProvider serviceProvider)
{
    var hostingEnv = serviceProvider.GetService<IWebHostEnvironment>();
}

Si debe resolver los servicios en el ConfigureServices()método, se requiere un enfoque diferente. Puede crear un intermediario IServiceProviderdesde la IServiceCollectioninstancia que contiene los servicios que se han registrado hasta ese momento :

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IFooService, FooService>();

    // Build the intermediate service provider
    var sp = services.BuildServiceProvider();

    // This will succeed.
    var fooService = sp.GetService<IFooService>();
    // This will fail (return null), as IBarService hasn't been registered yet.
    var barService = sp.GetService<IBarService>();
}

Tenga en cuenta: en general, debe evitar resolver servicios dentro del ConfigureServices()método, ya que este es realmente el lugar donde está configurando los servicios de la aplicación. A veces solo necesitas acceder a una IOptions<MyOptions>instancia. Puede lograr esto vinculando los valores de la IConfigurationinstancia a una instancia de MyOptions(que es esencialmente lo que hace el marco de opciones):

public void ConfigureServices(IServiceCollection services)
{
    var myOptions = new MyOptions();
    Configuration.GetSection("SomeSection").Bind(myOptions);
}

La resolución manual de servicios (también conocido como Localizador de servicios) generalmente se considera un antipatrón . Si bien tiene sus casos de uso (para marcos y / o capas de infraestructura), debe evitarlo tanto como sea posible.

Henk Mollema
fuente
14
@HenkMollema, pero qué pasa si no puedo inyectarme nada, quiero decir que no puedo haber IServiceCollectioninyectado, alguna clase que se está creando manualmente ( fuera del alcance del software intermedio ), un planificador en mi caso, que periódicamente necesita algunos servicios para generar y enviar un correo electrónico
Merdan Gochmuradov
52
advertencia si necesita resolver servicios ConfigureServicesy ese servicio es un singleton, ¡será un singleton diferente al que Controllerusa! Supongo que esto se debe a que utiliza una diferente IServiceProvider- para evitar esto no se resuelve a través de BuildServiceProvidery en lugar de mover su operaciones de búsqueda del singleton de ConfigureServicesque Configure(..other params, IServiceProvider serviceProvider)enStartup.cs
Wal
3
@wal buen punto. Debido a que es una IServiceProviderinstancia diferente , creará una nueva instancia de singleton. Puede evitar esto devolviendo la instancia del proveedor de servicios del ConfigureServicesmétodo, de modo que también será el contenedor que usa su aplicación.
Henk Mollema
1
Invocar collection.BuildServiceProvider();era lo que necesitaba, ¡gracias!
Chris Marisic
2
@HenkMollema, ¿cómo hace que funcione con una sola instancia de proveedor de servicios? Por lo general, debería 1) Registrar algunas de sus dependencias 2) crear una instancia de proveedor de servicios provisional 3) Usar ese proveedor de servicios para resolver algo que necesita para registrar algunas otras dependencias. Después, no puede devolver la instancia provisional, ya que le faltan algunas de sus dependencias (registradas en 3). ¿Me estoy perdiendo de algo?
Filip
109

La resolución manual de instancias implica el uso de la IServiceProviderinterfaz:

Resolviendo dependencia en Startup.ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyService, MyService>();

    var serviceProvider = services.BuildServiceProvider();
    var service = serviceProvider.GetService<IMyService>();
}

Resolver dependencias en el inicio.

public void Configure(
    IApplicationBuilder application,
    IServiceProvider serviceProvider)
{
    // By type.
    var service1 = (MyService)serviceProvider.GetService(typeof(MyService));

    // Using extension method.
    var service2 = serviceProvider.GetService<MyService>();

    // ...
}

Resolver dependencias en el inicio.Configurar en ASP.NET Core 3

public void Configure(
    IApplicationBuilder application,
    IWebHostEnvironment webHostEnvironment)
{
    app.ApplicationServices.GetService<MyService>();
}

Uso de servicios inyectados en tiempo de ejecución

Algunos tipos se pueden inyectar como parámetros de método:

public class Startup
{
    public Startup(
        IHostingEnvironment hostingEnvironment,
        ILoggerFactory loggerFactory)
    {
    }

    public void ConfigureServices(
        IServiceCollection services)
    {
    }

    public void Configure(
        IApplicationBuilder application,
        IHostingEnvironment hostingEnvironment,
        IServiceProvider serviceProvider,
        ILoggerFactory loggerfactory,
        IApplicationLifetime applicationLifetime)
    {
    }
}

Resolver dependencias en acciones del controlador

[HttpGet("/some-action")]
public string SomeAction([FromServices] IMyService myService) => "Hello";
Muhammad Rehan Saeed
fuente
1
@AfsharMohebbi, GetServiceque es genérico, es un método de extensión en el Microsoft.Extensions.DependencyInjectionespacio de nombres.
ahmadali shafiee
Acerca de los métodos de extensión: Un método de extensión es un método estático que agrega funcionalidad a una clase, puede declarar público TheReturnType TheMethodName (este TheTypeYouExtend theTypeYouExtend {// BODY} y luego puede usarlo como: TheTypeYouExtend.TheMethodName (); Eso tiene convertirse en un enfoque muy común con .NET Core, para que los desarrolladores puedan ampliar la funcionalidad básica ... buenos ejemplos aquí: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
Juan
17

Si genera una aplicación con una plantilla, tendrá algo como esto en la Startupclase:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);

    services.AddMvc();
}

Luego puede agregar dependencias allí, por ejemplo:

services.AddTransient<ITestService, TestService>();

Si desea acceder ITestServiceen su controlador, puede agregar IServiceProviderel constructor y se inyectará:

public HomeController(IServiceProvider serviceProvider)

Luego puede resolver el servicio que agregó:

var service = serviceProvider.GetService<ITestService>();

Tenga en cuenta que para usar la versión genérica debe incluir el espacio de nombres con las extensiones:

using Microsoft.Extensions.DependencyInjection;

ITestService.cs

public interface ITestService
{
    int GenerateRandom();
}

TestService.cs

public class TestService : ITestService
{
    public int GenerateRandom()
    {
        return 4;
    }
}

Startup.cs (ConfigureServices)

public void ConfigureServices(IServiceCollection services)
{
    services.AddApplicationInsightsTelemetry(Configuration);
    services.AddMvc();

    services.AddTransient<ITestService, TestService>();
}

HomeController.cs

using Microsoft.Extensions.DependencyInjection;

namespace Core.Controllers
{
    public class HomeController : Controller
    {
        public HomeController(IServiceProvider serviceProvider)
        {
            var service = serviceProvider.GetService<ITestService>();
            int rnd = service.GenerateRandom();
        }
BrunoLM
fuente
10

Si solo necesita resolver una dependencia con el fin de pasarla al constructor de otra dependencia que está registrando, puede hacerlo.

Digamos que tenía un servicio que incluía una cadena y un ISomeService.

public class AnotherService : IAnotherService
{
    public AnotherService(ISomeService someService, string serviceUrl)
    {
        ...
    }
}

Cuando vaya a registrar esto dentro de Startup.cs, deberá hacer esto:

services.AddScoped<IAnotherService>(ctx => 
      new AnotherService(ctx.GetService<ISomeService>(), "https://someservice.com/")
);
raterus
fuente
El OP no indicó la razón por la que necesita resolver un servicio en el método ConfigureService, pero esta es probablemente la razón por la que alguien pensaría hacer eso
kilkfoe
1
De hecho, esta debería ser la respuesta aceptada ... Aunque la respuesta de Henk Mollema es muy ilustrativa, hoy en día su respuesta es más limpia y no introduce problemas relacionados con la construcción de un IServiceProvider intermedio (diferentes instancias de singletons ...). Probablemente, esta solución no estaba disponible en 2015 cuando Henk respondió, pero ahora es el camino a seguir.
Vi100
Intenté esto pero ISomeServicetodavía era nulo para mí.
ajbeaven
2 preguntas: 1) Si el constructor de parámetros de la clase de servicio AnotherService cambia (servicios eliminados o agregados), entonces necesito modificar el segmento de registro del servicio IAnotherService y sigue cambiando. 2) En cambio, puedo agregar solo un constructor para AnotherService con 1 parámetro como public AnotherService (IServiceProvider serviceProvider) y obtener los servicios que necesito del constructor. Y solo necesito registrar la clase de servicio AnotherService en la clase de inicio como services.AddTransient <IAnotherService, AnotherService> (sp => {var service = new AnotherService (sp); return service;});
Thomas.Benz
2

Puede inyectar dependencias en atributos como AuthorizeAttribute de esta manera

var someservice = (ISomeService)context.HttpContext.RequestServices.GetService(typeof(ISomeService));
Bora Aydın
fuente
Esto es lo que estaba buscando ... Gracias
Reyan Chougle
0

Sé que esta es una vieja pregunta, pero me sorprende que no haya un truco bastante obvio y desagradable.

Puede aprovechar la capacidad de definir su propia función de ctor para tomar los valores necesarios de sus servicios a medida que los define ... obviamente, esto se ejecutará cada vez que se solicite el servicio a menos que elimine / borre explícitamente y vuelva a agregar la definición de Este servicio dentro de la primera construcción del ctor explotador .

Este método tiene la ventaja de no requerir que construya el árbol de servicios o que lo use durante la configuración del servicio. Todavía está definiendo cómo se configurarán los servicios.

public void ConfigureServices(IServiceCollection services)
{
    //Prey this doesn't get GC'd or promote to a static class var
    string? somevalue = null;

    services.AddSingleton<IServiceINeedToUse, ServiceINeedToUse>(scope => {
         //create service you need
         var service = new ServiceINeedToUse(scope.GetService<IDependantService>())
         //get the values you need
         somevalue = somevalue ?? service.MyDirtyHack();
         //return the instance
         return service;
    });
    services.AddTransient<IOtherService, OtherService>(scope => {
         //Explicitly ensuring the ctor function above is called, and also showcasing why this is an anti-pattern.
         scope.GetService<IServiceINeedToUse>();
         //TODO: Clean up both the IServiceINeedToUse and IOtherService configuration here, then somehow rebuild the service tree.
         //Wow!
         return new OtherService(somevalue);
    });
}

La forma de arreglar este patrón sería dar OtherServiceuna dependencia explícita IServiceINeedToUse, en lugar de depender implícitamente de él o del valor de retorno de su método ... o resolver esa dependencia explícitamente de alguna otra manera.

Izzy
fuente
-4
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddDbContext<ConfigurationRepository>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("SqlConnectionString")));

    services.AddScoped<IConfigurationBL, ConfigurationBL>();
    services.AddScoped<IConfigurationRepository, ConfigurationRepository>();
}
Nathan Alard
fuente
55
Es más probable que sus respuestas sean aceptadas y votadas si proporciona una breve explicación de por qué es una buena respuesta, no solo un fragmento de código. También ayuda al autor de la pregunta a asegurarse de que realmente está respondiendo la pregunta que hicieron.
Jim L
Alguien marcó incorrectamente su respuesta como de baja calidad. Debería agregar un texto que lo acompañe para explicar cómo funciona su respuesta para evitar que se marquen más y / o se voten negativamente. Una respuesta de solo código no es de baja calidad . ¿Intenta responder la pregunta? De lo contrario, marque como 'no es una respuesta' o recomiende la eliminación (si está en la cola de revisión). b) ¿Es técnicamente incorrecto? Voto negativo o comentario. De la revisión .
Wai Ha Lee