.NET Core DI, Möglichkeiten zum Übergeben von Parametern an den Konstruktor

101

Mit dem folgenden Servicekonstruktor

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

     }
}

Welche Möglichkeiten haben Sie, die Parameter mithilfe des .NET Core IOC-Mechanismus zu übergeben?

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

Gibt es einen anderen Weg?

Boris
quelle
3
Ändern Sie Ihr Design. Extrahieren Sie das Argument in ein Parameterobjekt und fügen Sie es ein.
Steven

Antworten:

120

Der Ausdrucksparameter ( in diesem Fall x ) des Factory-Delegaten ist a IServiceProvider.

Verwenden Sie dies, um die Abhängigkeiten aufzulösen.

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

Der Werksdelegierte ist ein verzögerter Aufruf. Wann immer der Typ aufgelöst werden soll, wird der abgeschlossene Provider als Delegate-Parameter übergeben.

Nkosi
quelle
1
Ja, so mache ich es gerade, aber gibt es einen anderen Weg? eleganter vielleicht? Ich meine, es würde ein bisschen komisch aussehen, andere Parameter zu haben, die registrierte Dienste sind. Ich suche eher nach einer normalen Registrierung der Dienste und übergebe nur die Nicht-Dienst-Argumente, in diesem Fall das Argument. So etwas wie Autofac tut.WithParameter("argument", "");
Boris
1
Nein, Sie erstellen den Anbieter manuell, was schlecht ist. Der Delegat ist ein verzögerter Aufruf. Wann immer der Typ aufgelöst werden soll, wird der abgeschlossene Provider als Delegate-Parameter übergeben.
Nkosi
@MCR ist der Standardansatz mit dem sofort einsatzbereiten Core DI.
Nkosi
11
@Nkosi: Schauen Sie sich ActivatorUtilities.CreateInstance an , seinen Teil des Microsoft.Extensions.DependencyInjection.AbstractionsPakets (also keine container-spezifischen Abhängigkeiten)
Tseng
Danke, @Tseng, das sieht aus wie die eigentliche Antwort, nach der wir hier suchen.
BrainSlugs83
58

Es ist zu beachten, dass die empfohlene Methode die Verwendung des Optionsmusters ist . Es gibt jedoch Anwendungsfälle, in denen dies unpraktisch ist (wenn Parameter nur zur Laufzeit bekannt sind, nicht zur Start- / Kompilierungszeit) oder Sie eine Abhängigkeit dynamisch ersetzen müssen.

Dies ist sehr nützlich, wenn Sie eine einzelne Abhängigkeit ersetzen müssen (sei es eine Zeichenfolge, eine Ganzzahl oder eine andere Art von Abhängigkeit) oder wenn Sie eine Bibliothek eines Drittanbieters verwenden, die nur Zeichenfolgen- / Ganzzahlparameter akzeptiert und Laufzeitparameter benötigen.

Sie können CreateInstance (IServiceProvider, Object []) als Verknüpfungshand versuchen (nicht sicher, ob es mit Zeichenfolgenparametern / Werttypen / Grundelementen (int, float, string) funktioniert, nicht getestet) (Sie haben es einfach ausprobiert und bestätigt, dass es funktioniert, auch mit mehrere Zeichenfolgenparameter), anstatt jede einzelne Abhängigkeit von Hand aufzulösen:

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

Die Parameter (letzter Parameter von CreateInstance<T>/ CreateInstance) definieren die Parameter, die ersetzt werden sollen (nicht vom Anbieter aufgelöst). Sie werden von links nach rechts angewendet, sobald sie angezeigt werden (dh die erste Zeichenfolge wird durch den ersten Parameter vom Typ "Zeichenfolge" des zu instanziierenden Typs ersetzt).

ActivatorUtilities.CreateInstance<Service> wird an vielen Stellen verwendet, um einen Dienst aufzulösen und eine der Standardregistrierungen für diese einzelne Aktivierung zu ersetzen.

Wenn Sie zum Beispiel haben eine Klasse mit dem Namen MyService, und es hat IOtherService, ILogger<MyService>als Abhängigkeiten und Sie wollen den Dienst lösen , aber den Standard - Service ersetzen IOtherService(sagen sein OtherServiceA) mit OtherServiceB, man könnte etwas tun , wie:

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

Dann wird der erste Parameter IOtherServicewird erhalten OtherServiceBinjiziert, anstattOtherServiceA aber die übrigen Parameter aus dem Behälter kommen.

Dies ist hilfreich, wenn Sie viele Abhängigkeiten haben und nur eine einzelne speziell behandeln möchten (dh einen datenbankspezifischen Anbieter durch einen Wert ersetzen möchten, der während der Anforderung oder für einen bestimmten Benutzer konfiguriert wurde, was Sie nur zur Laufzeit und während einer Anforderung und wissen nicht wenn die Anwendung erstellt / gestartet wird).

Sie können stattdessen auch die ActivatorUtilities.CreateFactory-Methode (Type, Type []) verwenden , um die Factory-Methode zu erstellen, da sie eine bessere Leistung bietet. GitHub Reference and Benchmark .

Später ist eine nützlich, wenn der Typ sehr häufig aufgelöst wird (z. B. in SignalR und anderen Szenarien mit hohen Anforderungen). Grundsätzlich würden Sie ein ObjectFactoryVia erstellen

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

Speichern Sie es dann (als Variable usw.) und rufen Sie es bei Bedarf auf

MyService myService = myServiceFactory(serviceProvider, myServiceOrParameterTypeToReplace);

## Update: Ich habe es selbst versucht, um zu bestätigen, dass es auch mit Zeichenfolgen und Ganzzahlen funktioniert, und es funktioniert tatsächlich. Hier das konkrete Beispiel, mit dem ich getestet habe:

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

Druckt

Output: Hello Tseng Stackoverflow
Tseng
quelle
6
Auf diese Weise instanziiert ASP.NET Core die Controller standardmäßig als ControllerActivatorProvider . Sie werden nicht direkt vom IoC aufgelöst (es .AddControllersAsServicessei denn, es wird verwendet, was das ControllerActivatorProvidermitServiceBasedControllerActivator
Tseng
1
ActivatorUtilities.CreateInstance()ist genau das, was ich brauchte. Vielen Dank!
Billy Jo
1
@Tseng Würden Sie so freundlich sein, Ihren veröffentlichten Code zu überprüfen und ein Update zu veröffentlichen? Nachdem ich die Erweiterungs- und HellloWorldService-Klassen der obersten Ebene erstellt habe, bin ich immer noch mit demoservice.HelloWorld als undefiniert konfrontiert. Ich verstehe nicht, wie das funktionieren sollte, um es zu beheben. Mein Ziel ist es zu verstehen, wie dieser Mechanismus funktioniert, wenn ich ihn brauche.
SOHO Developer
1
@SOHODeveloper: Nun, offensichtlich public string HelloWorld()fehlte die Methodenimplementierung
Tseng
Diese Antwort ist eleganter und sollte akzeptiert werden ... Danke!
Exodium
14

Wenn Sie sich mit der Neuerung des Dienstes nicht wohl fühlen, können Sie das Parameter ObjectMuster verwenden.

Extrahieren Sie also den String-Parameter in einen eigenen Typ

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

Und der Konstruktor jetzt wird es so aussehen

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

}

Und das Setup

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

Der erste Vorteil besteht darin, dass Sie die new Service(...Aufrufe nicht ändern müssen, wenn Sie den Servicekonstruktor ändern und neue Services hinzufügen müssen . Ein weiterer Vorteil ist, dass das Setup etwas sauberer ist.

Für einen Konstruktor mit einem oder zwei Parametern kann dies jedoch zu viel sein.

Adrian Iftode
quelle
2
Es wäre für komplexe Parameter intuitiver, das Optionsmuster zu verwenden, und es wird die empfohlene Methode für das Optionsmuster empfohlen. Es ist jedoch weniger geeignet für Parameter, die nur zur Laufzeit bekannt sind (dh aus einer Anfrage oder einem Anspruch)
Tseng