.NET Core DI, sposoby przekazywania parametrów do konstruktora

105

Posiadanie następującego konstruktora usługi

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

     }
}

Jakie są możliwości przekazywania parametrów przy użyciu mechanizmu .NET Core IOC

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

Czy jest inny sposób?

Boris
źródło
3
Zmień swój projekt. Wyodrębnij argument do obiektu parametru i wstrzyknij go.
Steven

Odpowiedzi:

125

Parametr wyrażenia ( w tym przypadku x ) delegata fabryki to plik IServiceProvider.

Użyj tego, aby rozwiązać zależności,

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

Delegat fabryki to opóźnione wywołanie. Zawsze, gdy typ ma zostać rozwiązany, przekaże ukończonego dostawcę jako parametr delegata.

Nkosi
źródło
1
tak, właśnie to robię teraz, ale czy jest inny sposób? może bardziej elegancki? Chodzi mi o to, że byłoby trochę dziwnie mieć inne parametry, które są zarejestrowanymi usługami. Szukam czegoś bardziej jak normalne rejestrowanie usług i przekazywanie tylko argumentów niezwiązanych z usługą, w tym przypadku argument. Coś jak robi Autofac .WithParameter("argument", "");
boris,
1
Nie, dostawca jest tworzony ręcznie, co jest złe. Delegat to opóźnione wywołanie. Zawsze, gdy typ ma zostać rozwiązany, przekaże ukończonego dostawcę jako parametr delegata.
Nkosi,
@MCR to domyślne podejście z Core DI po wyjęciu z pudełka.
Nkosi,
12
@Nkosi: Spójrz na ActivatorUtilities.CreateInstance , jego część Microsoft.Extensions.DependencyInjection.Abstractionspakietu (więc brak zależności od konkretnego kontenera)
Tseng
Dzięki, @Tseng, która wygląda jak rzeczywista odpowiedź, której tutaj szukamy.
BrainSlugs83
63

Należy zauważyć, że zalecanym sposobem jest użycie wzorca opcji . Ale są przypadki użycia, w których jest to niepraktyczne (kiedy parametry są znane tylko w czasie wykonywania, a nie w czasie uruchamiania / kompilacji) lub trzeba dynamicznie zastąpić zależność.

Jest to bardzo przydatne, gdy musisz zamienić pojedynczą zależność (czy to ciąg, liczbę całkowitą lub inny typ zależności) lub gdy używasz biblioteki innej firmy, która akceptuje tylko parametry typu string / integer i potrzebujesz parametru wykonawczego.

Możesz spróbować CreateInstance (IServiceProvider, Object []) jako skrótu (nie jestem pewien, czy działa z parametrami ciągów / typami wartości / prymitywami (int, float, string), nietestowane) (Właśnie wypróbowałem i potwierdziłem, że działa, nawet z wiele parametrów łańcuchowych) zamiast rozwiązywać ręcznie każdą zależność:

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

Parametry (ostatni parametr z CreateInstance<T>/ CreateInstance) definiują parametry, które powinny zostać zastąpione (nierozpoznane przez dostawcę). Są one stosowane od lewej do prawej, gdy się pojawiają (tj. Pierwszy łańcuch zostanie zastąpiony pierwszym parametrem typu łańcuchowego typu, który ma być utworzony).

ActivatorUtilities.CreateInstance<Service> jest używany w wielu miejscach do rozwiązywania usługi i zastępowania jednej z domyślnych rejestracji dla tej pojedynczej aktywacji.

Na przykład, jeśli masz klasę o nazwie MyServicei ma IOtherService, ILogger<MyService>jak zależnościami i chcesz rozwiązać usługę ale zastąpić domyślną usługę IOtherService(powiedzmy ITS OtherServiceA) z OtherServiceB, można zrobić coś takiego:

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

Wtedy pierwszy parametr IOtherServicezostanie OtherServiceBwstrzyknięty, a nie OtherServiceApozostałe parametry będą pochodzić z kontenera.

Jest to przydatne, gdy masz wiele zależności i chcesz tylko traktować tylko jeden z nich (tj. Zastąpić konkretnego dostawcę bazy danych wartością skonfigurowaną podczas żądania lub dla określonego użytkownika, coś, co znasz tylko w czasie wykonywania i podczas żądania i nie podczas budowania / uruchamiania aplikacji).

Możesz także użyć metody ActivatorUtilities.CreateFactory (Type, Type []), aby zamiast tego utworzyć metodę fabryczną, ponieważ oferuje ona lepszą wydajność GitHub Reference i Benchmark .

Później jeden jest przydatny, gdy typ jest bardzo często rozpoznawany (na przykład w SignalR i innych scenariuszach z wysokim żądaniem). Zasadniczo utworzyłbyś ObjectFactoryvia

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

następnie buforuj ją (jako zmienną itp.) i wywołuj ją w razie potrzeby

MyService myService = myServiceFactory(serviceProvider, myServiceOrParameterTypeToReplace);

## Aktualizacja: po prostu wypróbowałem to sam, aby potwierdzić, że działa również z ciągami znaków i liczbami całkowitymi, i rzeczywiście działa. Oto konkretny przykład, który przetestowałem z:

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);
}

Wydruki

Output: Hello Tseng Stackoverflow
Tseng
źródło
6
Jest to również, jak ASP.NET Rdzeń instancję kontrolerów domyślnie ControllerActivatorProvider , nie są one bezpośrednio rozwiązany od MKOl (chyba że .AddControllersAsServicesużywany jest, który zastępuje ControllerActivatorProviderzServiceBasedControllerActivator
Tseng
16

Jeśli nie podoba Ci się nowa usługa, możesz użyć Parameter Objectwzoru.

Więc wyodrębnij parametr ciągu do jego własnego typu

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

A konstruktor teraz będzie wyglądał

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

}

I konfiguracja

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

Pierwszą korzyścią jest to, że jeśli musisz zmienić konstruktor usługi i dodać do niego nowe usługi, nie musisz zmieniać new Service(...wywołań. Kolejną korzyścią jest to, że konfiguracja jest nieco czystsza.

Dla konstruktora z jednym lub dwoma parametrami może to być jednak za dużo.

Adrian Iftode
źródło
2
W przypadku złożonych parametrów byłoby bardziej intuicyjne użycie wzorca Options i jest to zalecany sposób dla wzorca opcji, jednak jest mniej odpowiedni dla parametrów znanych tylko w czasie wykonywania (tj. Z żądania lub roszczenia)
Tseng
0

Za pomocą tego procesu można również wstrzyknąć zależności

_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService>(x=>new Service( x.GetService<IOtherService>(), x.GetService<IAnotherOne >(), "" ));
Alamgir
źródło