.NET Core DI, formas de pasar parámetros al constructor

102

Tener el siguiente constructor de servicios

public class Service : IService
{
     public Service(IOtherService service1, IAnotherOne service2, string arg)
     {

     }
}

¿Cuáles son las opciones para pasar los parámetros mediante el mecanismo de IOC de .NET Core?

_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService>(x=>new Service( _serviceCollection.BuildServiceProvider().GetService<IOtherService>(), _serviceCollection.BuildServiceProvider().GetService<IAnotherOne >(), "" ));

Hay alguna otra manera ?

boris
fuente
3
Cambia tu diseño. Extraiga el argumento en un objeto de parámetro e inyéctelo.
Steven

Respuestas:

121

El parámetro de expresión ( x en este caso), del delegado de fábrica es a IServiceProvider.

Úselo para resolver las dependencias,

_serviceCollection.AddSingleton<IService>(x => 
    new Service(x.GetRequiredService<IOtherService>(),
                x.GetRequiredService<IAnotherOne>(), 
                ""));

El delegado de fábrica es una invocación retrasada. Siempre que se deba resolver el tipo, pasará el proveedor completo como parámetro de delegado.

Nkosi
fuente
1
sí, así es como lo estoy haciendo ahora mismo, pero ¿hay alguna otra forma? más elegante tal vez? Quiero decir, se vería un poco extraño tener otros parámetros que son servicios registrados. Estoy buscando algo más como registrar los servicios normalmente y solo pasar los argumentos que no son de servicio, en este caso el arg. Algo como lo hace Autofac .WithParameter("argument", "");
boris
1
No, está construyendo el proveedor manualmente, lo cual es malo. El delegado es una invocación retrasada. Siempre que se deba resolver el tipo, pasará el proveedor completo como parámetro de delegado.
Nkosi
@MCR que es el enfoque predeterminado con el Core DI listo para usar.
Nkosi
11
@Nkosi: Eche un vistazo a ActivatorUtilities.CreateInstance , es parte del Microsoft.Extensions.DependencyInjection.Abstractionspaquete (por lo que no hay dependencias específicas del contenedor)
Tseng
Gracias, @Tseng, parece la respuesta real que estamos buscando aquí.
BrainSlugs83
59

Cabe señalar que la forma recomendada es utilizar el patrón de opciones . Pero hay casos de uso en los que no es práctico (cuando los parámetros solo se conocen en tiempo de ejecución, no en el momento de inicio / compilación) o es necesario reemplazar dinámicamente una dependencia.

Es muy útil cuando necesita reemplazar una sola dependencia (ya sea una cadena, un entero u otro tipo de dependencia) o cuando usa una biblioteca de terceros que acepta solo parámetros de cadena / entero y necesita un parámetro de tiempo de ejecución.

Puede probar CreateInstance (IServiceProvider, Object []) como una mano de acceso directo (no estoy seguro de que funcione con parámetros de cadena / tipos de valor / primitivas (int, float, string), sin probar) (Solo probé y confirmó que funciona, incluso con múltiples parámetros de cadena) en lugar de resolver cada dependencia a mano:

_serviceCollection.AddSingleton<IService>(x => 
    ActivatorUtilities.CreateInstance<Service>(x, "");
);

Los parámetros (último parámetro de CreateInstance<T>/ CreateInstance) definen los parámetros que deben ser reemplazados (no resueltos por el proveedor). Se aplican de izquierda a derecha a medida que aparecen (es decir, la primera cadena se reemplazará con el primer parámetro de tipo cadena del tipo que se va a instanciar).

ActivatorUtilities.CreateInstance<Service> se utiliza en muchos lugares para resolver un servicio y reemplazar uno de los registros predeterminados para esta única activación.

Por ejemplo, si tiene una clase nombrada MyService, y tiene IOtherService, ILogger<MyService>como dependencias y desea resolver el servicio pero reemplazar el servicio predeterminado de IOtherService(diga su OtherServiceA) con OtherServiceB, podría hacer algo como:

myService = ActivatorUtilities.CreateInstance<Service>(serviceProvider, new OtherServiceB())

Luego IOtherService, se OtherServiceBinyectará el primer parámetro de , en lugar de que OtherServiceAlos parámetros restantes provengan del contenedor.

Esto es útil cuando tiene muchas dependencias y solo desea tratar una sola de manera especial (es decir, reemplazar un proveedor específico de la base de datos con un valor configurado durante la solicitud o para un usuario específico, algo que solo conoce en tiempo de ejecución y durante una solicitud y no cuando la aplicación está construida / iniciada).

También puede usar el método ActivatorUtilities.CreateFactory (Type, Type []) para crear un método de fábrica en su lugar, ya que ofrece un mejor rendimiento GitHub Reference y Benchmark .

Posteriormente, uno es útil cuando el tipo se resuelve con mucha frecuencia (como en SignalR y otros escenarios de alta solicitud). Básicamente, crearías una ObjectFactoryvía

var myServiceFactory = ActivatorUtilities.CreateFactory(typeof(MyService), new[] { typeof(IOtherService) });

luego almacenarlo en caché (como una variable, etc.) y llamarlo donde sea necesario

MyService myService = myServiceFactory(serviceProvider, myServiceOrParameterTypeToReplace);

## Actualización: Lo intenté yo mismo para confirmar que también funciona con cadenas y números enteros, y de hecho funciona. Aquí el ejemplo concreto con el que probé:

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        services.AddTransient<HelloWorldService>();
        services.AddTransient(p => p.ResolveWith<DemoService>("Tseng", "Stackoverflow"));

        var provider = services.BuildServiceProvider();

        var demoService = provider.GetRequiredService<DemoService>();

        Console.WriteLine($"Output: {demoService.HelloWorld()}");
        Console.ReadKey();
    }
}

public class DemoService
{
    private readonly HelloWorldService helloWorldService;
    private readonly string firstname;
    private readonly string lastname;

    public DemoService(HelloWorldService helloWorldService, string firstname, string lastname)
    {
        this.helloWorldService = helloWorldService ?? throw new ArgumentNullException(nameof(helloWorldService));
        this.firstname = firstname ?? throw new ArgumentNullException(nameof(firstname));
        this.lastname = lastname ?? throw new ArgumentNullException(nameof(lastname));
    }

    public string HelloWorld()
    {
        return this.helloWorldService.Hello(firstName, lastName);
    }
}

public class HelloWorldService
{
    public string Hello(string name) => $"Hello {name}";
    public string Hello(string firstname, string lastname) => $"Hello {firstname} {lastname}";
}

// Just a helper method to shorten code registration code
static class ServiceProviderExtensions
{
    public static T ResolveWith<T>(this IServiceProvider provider, params object[] parameters) where T : class => 
        ActivatorUtilities.CreateInstance<T>(provider, parameters);
}

Huellas dactilares

Output: Hello Tseng Stackoverflow
Tseng
fuente
6
Así es también como ASP.NET Core crea una instancia de los controladores de forma predeterminada ControllerActivatorProvider , no se resuelven directamente desde el IoC (a menos que .AddControllersAsServicesse use, que reemplaza el ControllerActivatorProviderconServiceBasedControllerActivator
Tseng
1
ActivatorUtilities.CreateInstance()es exactamente lo que necesitaba. ¡Gracias!
Billy Jo
1
@Tseng ¿Sería tan amable de revisar su código publicado y publicar una actualización? Después de hacer la extensión y las clases de nivel superior HellloWorldService, todavía me encuentro con demoservice.HelloWorld como indefinido. No entiendo cómo se suponía que esto funcionaba lo suficiente como para solucionarlo. Mi objetivo es comprender cómo funciona este mecanismo cuando lo necesito.
Desarrollador SOHO
1
@SOHODeveloper: Bueno, obviamente public string HelloWorld()faltaba la implementación del método
Tseng
Esta respuesta más elegante y debería ser aceptada ... ¡Gracias!
Éxodo
15

Si no se siente cómodo al renovar el servicio, puede usar el Parameter Objectpatrón.

Así que extrae el parámetro de cadena en su propio tipo

public class ServiceArgs
{
   public string Arg1 {get; set;}
}

Y el constructor ahora se verá como

public Service(IOtherService service1, 
               IAnotherOne service2, 
               ServiceArgs args)
{

}

Y la configuración

_serviceCollection.AddSingleton<ServiceArgs>(_ => new ServiceArgs { Arg1 = ""; });
_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService, Service>();

El primer beneficio es que si necesita cambiar el constructor del servicio y agregarle nuevos servicios, entonces no tiene que cambiar las new Service(...llamadas. Otro beneficio es que la configuración es un poco más limpia.

Sin embargo, para un constructor con uno o dos parámetros, esto podría ser demasiado.

Adrian Iftode
fuente
2
Sería más intuitivo para los parámetros complejos usar el patrón de Opciones y es la forma recomendada para el patrón de opciones, sin embargo, es menos adecuado para los parámetros que solo se conocen en tiempo de ejecución (es decir, de una solicitud o una reclamación)
Tseng