Wie registriere ich mehrere Implementierungen derselben Schnittstelle in Asp.Net Core?


238

Ich habe Dienste, die von derselben Schnittstelle abgeleitet sind.

public interface IService { }
public class ServiceA : IService { }
public class ServiceB : IService { } 
public class ServiceC : IService { }

In der Regel können Sie mit anderen IoC-Containern Unitykonkrete Implementierungen von einigen registrieren Key, die sie unterscheiden.

Wie registriere ich diese Dienste in ASP.NET Core und löse sie zur Laufzeit basierend auf einem Schlüssel auf?

Ich sehe keine AddService-Methoden, die einen keyoder nameParameter annehmen , die normalerweise zur Unterscheidung der konkreten Implementierung verwendet werden.

    public void ConfigureServices(IServiceCollection services)
    {            
         // How do I register services of the same interface?            
    }


    public MyController:Controller
    {
       public void DoSomething(string key)
       { 
          // How do I resolve the service by key?
       }
    }

Ist das Factory-Muster hier die einzige Option?

Update1
Ich habe den Artikel hier durchgesehen , der zeigt, wie das Factory-Muster verwendet wird, um Service-Instanzen abzurufen, wenn wir mehrere konkrete Implementierungen haben. Es ist jedoch immer noch keine vollständige Lösung. Wenn ich die _serviceProvider.GetService()Methode aufrufe, kann ich keine Daten in den Konstruktor einfügen.

Betrachten Sie zum Beispiel Folgendes:

public class ServiceA : IService
{
     private string _efConnectionString;
     ServiceA(string efconnectionString)
     {
       _efConnecttionString = efConnectionString;
     } 
}

public class ServiceB : IService
{    
   private string _mongoConnectionString;
   public ServiceB(string mongoConnectionString)
   {
      _mongoConnectionString = mongoConnectionString;
   }
}

public class ServiceC : IService
{    
    private string _someOtherConnectionString
    public ServiceC(string someOtherConnectionString)
    {
      _someOtherConnectionString = someOtherConnectionString;
    }
}

Wie kann _serviceProvider.GetService()die entsprechende Verbindungszeichenfolge eingefügt werden? In Unity oder einer anderen IoC-Bibliothek können wir dies bei der Typregistrierung tun. Ich kann jedoch IOption verwenden , für die ich alle Einstellungen vornehmen muss. Ich kann keine bestimmte Verbindungszeichenfolge in den Dienst einfügen.

Beachten Sie auch, dass ich versuche, die Verwendung anderer Container (einschließlich Unity) zu vermeiden, da ich dann auch alles andere (z. B. Controller) beim neuen Container registrieren muss.

Die Verwendung des Factory-Musters zum Erstellen von Dienstinstanzen ist außerdem gegen DIP gerichtet, da dadurch die Anzahl der Abhängigkeiten erhöht wird, über die ein Client hier Details verfügt .

Ich denke, der Standard-DI in ASP.NET Core fehlen zwei Dinge:

  1. Die Möglichkeit, Instanzen mithilfe eines Schlüssels zu registrieren
  2. Die Möglichkeit, statische Daten während der Registrierung in Konstruktoren einzufügen


2
Es gibt endlich eine Erweiterung in Nuget für namenbasierte Registrierungen, hoffe es kann helfen
Neleus

Hallo, entschuldigen Sie meine dumme Frage, aber ich bin ungefähr neu bei Microsoft.Extensions.DependencyInjection ... denken Sie, dass Sie 3 leere Schnittstellen erstellen, die Iservice erweitern, wie "öffentliche Schnittstelle IServiceA: IService" und dann "öffentliche Klasse ServiceA: IServiceA" "... könnte eine gute Übungsoption sein?
Emiliano Magliocca

Ist dieser Artikel von Nutzen? stevejgordon.co.uk/…
Mike B

Kann Update1zu einer anderen Frage verschoben werden, da das Injizieren von Dingen in Konstruktoren sich stark von der Ermittlung des zu konstruierenden Objekts unterscheidet
Neil

Antworten:


245

Ich habe eine einfache Problemumgehung durchgeführt, Funcals ich mich in dieser Situation befand.

Erklären Sie zunächst einen gemeinsamen Delegierten:

public delegate IService ServiceResolver(string key);

Richten Sie dann in Ihrem Startup.csdie mehreren konkreten Registrierungen und eine manuelle Zuordnung dieser Typen ein:

services.AddTransient<ServiceA>();
services.AddTransient<ServiceB>();
services.AddTransient<ServiceC>();

services.AddTransient<ServiceResolver>(serviceProvider => key =>
{
    switch (key)
    {
        case "A":
            return serviceProvider.GetService<ServiceA>();
        case "B":
            return serviceProvider.GetService<ServiceB>();
        case "C":
            return serviceProvider.GetService<ServiceC>();
        default:
            throw new KeyNotFoundException(); // or maybe return null, up to you
    }
});

Und verwenden Sie es aus jeder bei DI registrierten Klasse:

public class Consumer
{
    private readonly IService _aService;

    public Consumer(ServiceResolver serviceAccessor)
    {
        _aService = serviceAccessor("A");
    }

    public void UseServiceA()
    {
        _aService.DoTheThing();
    }
}

Beachten Sie, dass in diesem Beispiel der Schlüssel für die Auflösung der Einfachheit halber und weil OP insbesondere nach diesem Fall gefragt hat, eine Zeichenfolge ist.

Sie können jedoch einen beliebigen benutzerdefinierten Auflösungstyp als Schlüssel verwenden, da Sie normalerweise keinen großen n-case-Schalter benötigen, der Ihren Code verrottet. Hängt davon ab, wie Ihre App skaliert.


1
@MatthewStevenMonkan hat meine Antwort mit einem Beispiel aktualisiert
Miguel A. Arilla

2
Die Verwendung eines solchen Fabrikmusters ist der beste Weg. Danke für das Teilen!
Sergey Akopov

2
+1 Sehr ordentlich und sauber, denn wenn wir andere Di-Container verwenden, müssen wir deren Paket einschließen, wenn wir Abhängigkeiten auflösen müssen, z. ILifetimeScope in AutoFac.
Anupam Singh

1
@AnupamSingh Meiner Meinung nach benötigen die meisten kleinen bis mittleren Anwendungen, die auf .NET Core ausgeführt werden, kein DI-Framework. Sie fügen lediglich Komplexität und unerwünschte Abhängigkeiten hinzu. Die Schönheit und Einfachheit des integrierten DI ist mehr als ausreichend und kann es auch auch mit Leichtigkeit erweitert werden.
Miguel A. Arilla

7
Erklärung der Abstimmungen - Es ist sehr interessant, aber ich überarbeite derzeit eine massive Codebasis, um all diese Func-Magie zu entfernen, die jemand vor einigen Jahren (vor der MS DI-Revolution) getan hat kann weiter unten eine verschlungene DI-Auflösung verursachen. Zum Beispiel habe ich an einem Windows-Diensthandler gearbeitet, der über 1,6.000 Codezeilen mit Func zu tun hatte, und nachdem ich die empfohlene DI-Methode ausgeführt habe, habe ich sie auf 0,2.000 Zeilen reduziert. OK-Zeilen des Codes bedeuten nichts .. außer es ist jetzt einfacher zu lesen und wiederzuverwenden ...
Piotr Kula

79

Eine andere Möglichkeit ist die Verwendung der Erweiterungsmethode GetServicesvon Microsoft.Extensions.DependencyInjection.

Registrieren Sie Ihre Dienste als:

services.AddSingleton<IService, ServiceA>();
services.AddSingleton<IService, ServiceB>();
services.AddSingleton<IService, ServiceC>();

Dann lösen Sie mit ein wenig Linq:

var services = serviceProvider.GetServices<IService>();
var serviceB = services.First(o => o.GetType() == typeof(ServiceB));

oder

var serviceZ = services.First(o => o.Name.Equals("Z"));

(vorausgesetzt, das IServicehat eine String-Eigenschaft namens "Name")

Stellen Sie sicher, zu haben using Microsoft.Extensions.DependencyInjection;

Aktualisieren

AspNet 2.1 Quelle: GetServices


6
Ich bin mir nicht sicher, aber ich denke, es ist nicht deterministisch. Alle Ergebnisse, die Sie heute erhalten, können sich morgen ändern. Dies scheint keine gute Praxis zu sein.
rnrneverdies

4
Upvote zu dem Link für GetServices, der mir zeigte, dass Sie eine Liste von Diensten einen abhängigen Dienst anfordern können, indem SieIEnumerable<IService>
Johnny 5

20
serviceProvider.GetServices <IService> () instanziiert jeweils ServiceA, ServiceB und ServiceC. Sie möchten den Konstruktor nur eines Dienstes aufrufen - den, den Sie tatsächlich benötigen. Dies ist ein großes Problem, wenn Implementierungen nicht leicht sind oder Sie viele Implementierungen von IService haben (z. B. haben Sie automatisch generierte Implementierungen von IRepository für jedes Modell).
Uros

6
Ich stimme @Uros zu. Dies ist keine gute Lösung. Stellen Sie sich vor, was passiert, wenn Sie 10 IService-Implementierungen registrieren und die tatsächlich benötigte Instanz die letzte ist. In diesem Fall werden tatsächlich 9 Instanzen von DI erstellt, die niemals verwendet werden.
Thomai

4
Schlechte Idee: Mehrere nicht verwendete Instanzen, Anti-Pattern für Service Locator und direkte Kopplung an die tatsächliche Implementierung (Typ von <ServiceA>).
Rico Suter

20

Es wird nicht unterstützt von Microsoft.Extensions.DependencyInjection.

Sie können jedoch auch einen anderen Mechanismus zur Abhängigkeitsinjektion anschließen, z. B. StructureMap "Startseite" und " GitHub-Projekt" .

Es ist überhaupt nicht schwer:

  1. Fügen Sie StructureMap eine Abhängigkeit hinzu in project.json:

    "Structuremap.Microsoft.DependencyInjection" : "1.0.1",
  2. Injizieren Sie es in die ASP.NET-Pipeline ConfigureServicesund registrieren Sie Ihre Klassen (siehe Dokumente).

    public IServiceProvider ConfigureServices(IServiceCollection services) // returns IServiceProvider !
    {
        // Add framework services.
        services.AddMvc();
        services.AddWhatever();
    
        //using StructureMap;
        var container = new Container();
        container.Configure(config =>
        {
            // Register stuff in container, using the StructureMap APIs...
            config.For<IPet>().Add(new Cat("CatA")).Named("A");
            config.For<IPet>().Add(new Cat("CatB")).Named("B");
            config.For<IPet>().Use("A"); // Optionally set a default
            config.Populate(services);
        });
    
        return container.GetInstance<IServiceProvider>();
    }
    
  3. Um eine benannte Instanz zu erhalten, müssen Sie die anfordern IContainer

    public class HomeController : Controller
    {
        public HomeController(IContainer injectedContainer)
        {
            var myPet = injectedContainer.GetInstance<IPet>("B");
            string name = myPet.Name; // Returns "CatB"
    

Das ist es.

Für das zu erstellende Beispiel benötigen Sie

    public interface IPet
    {
        string Name { get; set; }
    }

    public class Cat : IPet
    {
        public Cat(string name)
        {
            Name = name;
        }

        public string Name {get; set; }
    }

Ich habe diesen Ansatz ausprobiert, erhalte jedoch Laufzeitfehler auf meinem Controller, da IContainer nicht in den Build-Plänen enthalten ist. Muss ich etwas tun, damit IContainer automatisch injiziert wird?
Mohrtan

Übrigens verwende ich StructureMap.Micorosoft.DependencyInjection 1.3.0.
Mohrtan

Geben Sie den neuen Container in ConfigureServices zurück?
Gerardo Grignoli

Ich gebe die IServiceProviderInstance des neuen Containers zurück, wie in Schritt 2 oben angegeben. Ich habe das genau kopiert und es nur für meine Typen geändert. Dies ist eine gute Lösung und funktioniert perfekt. Der einzige Nachteil ist, dass ich keinen injizierten Container verwenden kann und auf einen statischen Container zurückgreife, was ich nicht tun möchte.
Mohrtan

1
Es funktioniert für mich, danke GerardoGrignoli. @mohrtan Der Beispielcode ist hier, wenn Sie dies noch untersuchen. github.com/Yawarmurtaza/AspNetCoreStructureMap
Yawar Murtaza

13

Sie haben Recht, der integrierte ASP.NET Core-Container hat nicht das Konzept, mehrere Dienste zu registrieren und dann einen bestimmten abzurufen. Wie Sie vorschlagen, ist in diesem Fall eine Factory die einzige echte Lösung.

Alternativ können Sie zu einem Container eines Drittanbieters wie Unity oder StructureMap wechseln, der die von Ihnen benötigte Lösung bietet (hier dokumentiert: https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing- the-default-services-container ).


13

Ich habe das gleiche Problem und möchte mitteilen, wie ich es gelöst habe und warum.

Wie Sie erwähnt haben, gibt es zwei Probleme. Der Erste:

Wie registriere ich diese Dienste in Asp.Net Core und löse sie zur Laufzeit basierend auf einem Schlüssel auf?

Welche Möglichkeiten haben wir? Leute schlagen zwei vor:

  • Verwenden Sie eine benutzerdefinierte Fabrik (wie _myFactory.GetServiceByKey(key))

  • Verwenden Sie einen anderen DI-Motor (wie _unityContainer.Resolve<IService>(key))

Ist das Factory-Muster hier die einzige Option?

Tatsächlich sind beide Optionen Fabriken, da jeder IoC-Container auch eine Fabrik ist (hoch konfigurierbar und jedoch kompliziert). Und es scheint mir, dass andere Optionen auch Variationen des Factory-Musters sind.

Welche Option ist dann besser? Hier stimme ich @Sock zu, der die Verwendung einer benutzerdefinierten Factory vorgeschlagen hat, und deshalb.

Erstens versuche ich immer zu vermeiden, neue Abhängigkeiten hinzuzufügen, wenn sie nicht wirklich benötigt werden. Daher stimme ich Ihnen in diesem Punkt zu. Darüber hinaus ist die Verwendung von zwei DI-Frameworks schlechter als die Erstellung einer benutzerdefinierten Factory-Abstraktion. Im zweiten Fall müssen Sie eine neue Paketabhängigkeit hinzufügen (wie Unity), aber abhängig von einer neuen Factory-Schnittstelle ist dies hier weniger böse. Ich glaube, die Hauptidee von ASP.NET Core DI ist die Einfachheit. Es werden nur minimale Funktionen nach dem KISS-Prinzip beibehalten . Wenn Sie eine zusätzliche Funktion benötigen, dann DIY oder verwenden Sie ein entsprechendes Plungin , das die gewünschte Funktion implementiert (Open Closed Principle).

Zweitens müssen wir häufig viele benannte Abhängigkeiten für einen einzelnen Dienst einfügen. Im Falle von Unity müssen Sie möglicherweise Namen für Konstruktorparameter angeben (using InjectionConstructor). Diese Registrierung verwendet Reflexion und eine intelligente Logik, um Argumente für den Konstruktor zu erraten. Dies kann auch zu Laufzeitfehlern führen, wenn die Registrierung nicht mit den Konstruktorargumenten übereinstimmt. Auf der anderen Seite haben Sie bei Verwendung Ihrer eigenen Factory die volle Kontrolle darüber, wie die Konstruktorparameter bereitgestellt werden. Es ist besser lesbar und wird beim Kompilieren aufgelöst. Wieder KISS-Prinzip .

Das zweite Problem:

Wie kann _serviceProvider.GetService () die entsprechende Verbindungszeichenfolge einfügen?

Erstens stimme ich Ihnen zu, dass es keine gute Idee ist , abhängig von neuen Dingen wie IOptions(und daher vom Paket Microsoft.Extensions.Options.ConfigurationExtensions). Ich habe einige Diskussionen darüber gesehen, IOptionswo es unterschiedliche Meinungen über seinen Nutzen gab. Auch hier versuche ich zu vermeiden, neue Abhängigkeiten hinzuzufügen, wenn sie nicht wirklich benötigt werden. Wird es wirklich gebraucht? Ich denke nicht. Andernfalls müsste jede Implementierung davon abhängen, ohne dass eine eindeutige Notwendigkeit von dieser Implementierung ausgeht (für mich sieht es nach einer Verletzung des ISP aus, wo ich auch Ihnen zustimme). Dies gilt auch etwa in der Fabrik abhängig , aber in diesem Fall ist es kann vermieden werden.

Der ASP.NET Core DI bietet zu diesem Zweck eine sehr schöne Überlastung:

var mongoConnection = //...
var efConnection = //...
var otherConnection = //...
services.AddTransient<IMyFactory>(
             s => new MyFactoryImpl(
                 mongoConnection, efConnection, otherConnection, 
                 s.GetService<ISomeDependency1>(), s.GetService<ISomeDependency2>())));

Hallo, entschuldigen Sie meine dumme Frage, aber ich bin ungefähr neu bei Microsoft.Extensions.DependencyInjection ... denken Sie, dass Sie 3 Schnittstellen erstellen, die Iservice erweitern, wie "öffentliche Schnittstelle IServiceA: IService" und dann "öffentliche Klasse ServiceA: IServiceA"? ... könnte eine gute Übungsoption sein?
Emiliano Magliocca

1
@ emiliano-magliocca Im Allgemeinen sollten Sie sich IServiceAin Ihrem Fall nicht auf Schnittstellen verlassen, die Sie nicht verwenden (ISP) . Da Sie IServicenur Methoden von verwenden , sollten Sie nur von abhängig sein IService.
Neleus

1
@ cagatay-kalan Bei Fragen von OP kann er sein Ziel mit ASP.NET Core DI problemlos erreichen. Keine anderen DI-Frameworks erforderlich.
Neleus

1
@EmilianoMagliocca Es kann leicht auf diese Weise gelöst werden: services.AddTransient<MyFirstClass>( s => new MyFirstClass(s.GetService<Escpos>()));für die erste Klasse und services.AddTransient<MySecondClass>( s => new MySecondClass(s.GetService<Usbpos>()));für die zweite.
Neleus

1
@EmilianoMagliocca In meinem Beispiel haben sowohl 'MyFirstClass' als auch 'MySecondClass' denselben ctor-Parameter des Schnittstellentyps, den sowohl Escpos als auch Usbpos implementieren. Der obige Code weist den IoC-Container also nur an, wie 'MyFirstClass' und 'MySecondClass' instanziiert werden. Nichts mehr. Daher müssen Sie möglicherweise einige andere Schnittstellen 'MyFirstClass' und 'MySecondClass' zuordnen. Es hängt von Ihren Bedürfnissen ab und ich habe es in meinem Beispiel nicht behandelt.
Neleus

13

Ich injiziere einfach eine IEnumerable

ConfigureServices in Startup.cs

Assembly.GetEntryAssembly().GetTypesAssignableFrom<IService>().ForEach((t)=>
                {
                    services.AddScoped(typeof(IService), t);
                });

Dienstordner

public interface IService
{
    string Name { get; set; }
}

public class ServiceA : IService
{
    public string Name { get { return "A"; } }
}

public class ServiceB : IService
{    
    public string Name { get { return "B"; } }
}

public class ServiceC : IService
{    
    public string Name { get { return "C"; } }
}

MyController.cs

public class MyController
{
    private readonly IEnumerable<IService> _services;
    public MyController(IEnumerable<IService> services)
    {
        _services = services;
    }
    public void DoSomething()
    {
        var service = _services.Where(s => s.Name == "A").Single();
    }
...
}

Extensions.cs

    public static List<Type> GetTypesAssignableFrom<T>(this Assembly assembly)
    {
        return assembly.GetTypesAssignableFrom(typeof(T));
    }
    public static List<Type> GetTypesAssignableFrom(this Assembly assembly, Type compareType)
    {
        List<Type> ret = new List<Type>();
        foreach (var type in assembly.DefinedTypes)
        {
            if (compareType.IsAssignableFrom(type) && compareType != type)
            {
                ret.Add(type);
            }
        }
        return ret;
    }

In der DoSomething () -Methode des Controllers können Sie typeof verwenden, um den gewünschten Service aufzulösen: var service = _services.FirstOrDefault (t => t.GetType () == typeof (ServiceA));
Ciaran Bruen

Ich habe buchstäblich alles versucht, und dies ist die einzige Lösung, die für mich funktioniert hat. Vielen Dank!
Skatz1990

@ Skatz1990 Probieren Sie die unten erstellte Lösung in einem anderen Beitrag aus. Ich denke, es ist sauberer und einfacher zu bedienen.
T Brown

12

Die meisten Antworten hier verstoßen gegen das Prinzip der Einzelverantwortung (eine Serviceklasse sollte Abhängigkeiten nicht selbst auflösen) und / oder verwenden das Anti-Pattern des Service Locator.

Eine andere Möglichkeit, diese Probleme zu vermeiden, ist:

  • Verwenden Sie einen zusätzlichen generischen Typparameter für die Schnittstelle oder eine neue Schnittstelle, die die nicht generische Schnittstelle implementiert.
  • Implementieren Sie eine Adapter- / Interceptor-Klasse, um den Markertyp hinzuzufügen, und dann
  • Verwenden Sie den generischen Typ als "Name".

Ich habe einen Artikel mit weiteren Details geschrieben: Abhängigkeitsinjektion in .NET: Eine Möglichkeit, fehlende benannte Registrierungen zu umgehen


Wie verstößt die akzeptierte Antwort gegen das Prinzip der Einzelverantwortung?
LP13

Siehe Kommentare von stackoverflow.com/a/52066039/876814 und auch in der akzeptierten Antwort wird der Dienst träge aufgelöst, dh Sie wissen nur, ob er zur Laufzeit fehlschlägt und es gibt keine Möglichkeit, dies beim Start nach dem Erstellen des Containers statisch zu überprüfen (ähnlich wie bei die Antwort im Kommentar). SRP, weil der Dienst nicht nur für seine Geschäftslogik, sondern auch für die Auflösung von Abhängigkeiten verantwortlich ist
Rico Suter

@RicoSuter Ich mag die Lösung in Ihrem Blog sehr, bin aber durch Ihren DI in der Startup-Klasse verwirrt. Insbesondere verstehe ich die Zeile MessagePublisher ("MyOrderCreatedQueue") nicht, da ich keinen Konstruktor mit dieser Signatur sehe. services.AddSingleton <IMessagePublisher <OrderCreatedMessage> (neuer MessagePublisher <OrderCreatedMessage> (neuer MessagePublisher ("MyOrderCreatedQueue"));
Lee Z

Vielen Dank, den Artikel aktualisiert und MyMessagePublisher als Beispielimplementierung von IMessagePublisher
Rico Suter

7

Etwas spät zu dieser Party, aber hier ist meine Lösung: ...

Startup.cs oder Program.cs wenn generischer Handler ...

services.AddTransient<IMyInterface<CustomerSavedConsumer>, CustomerSavedConsumer>();
services.AddTransient<IMyInterface<ManagerSavedConsumer>, ManagerSavedConsumer>();

IMyInterface von T Interface Setup

public interface IMyInterface<T> where T : class, IMyInterface<T>
{
    Task Consume();
}

Konkrete Implementierungen von IMyInterface von T.

public class CustomerSavedConsumer: IMyInterface<CustomerSavedConsumer>
{
    public async Task Consume();
}

public class ManagerSavedConsumer: IMyInterface<ManagerSavedConsumer>
{
    public async Task Consume();
}

Wenn es ein Problem damit gibt, wird hoffentlich jemand darauf hinweisen, warum dies der falsche Weg ist.


2
IMyInterface<CustomerSavedConsumer>und IMyInterface<ManagerSavedConsumer>sind verschiedene Diensttypen - dies beantwortet die Frage des OP überhaupt nicht.
Richard Hauer

2
Das OP wollte eine Möglichkeit, mehrere Implementierungen derselben Schnittstelle im Asp.net-Kern zu registrieren. Wenn ich das nicht getan habe, erkläre bitte wie (genau).
Grau

1
Während Sie korrekt sind, ermöglicht dieses Muster den gewünschten Effekt. Zumindest als ich versuchte, dies selbst zu tun, stolperte ich über diesen Beitrag und meine Lösung funktionierte am besten für meine Situation.
Grau

1
Ich gehe davon aus, dass das Problem eher darin bestand, dass die Registrierung mehrerer Implementierungen für eine einzelne Schnittstelle (mithilfe von MS DI) es dem Container nicht ermöglicht, eine Implementierung von einer anderen zu unterscheiden. In anderen DIs können Sie sie eingeben, damit der Container weiß, welche er auswählen soll. In MS Sie haben einen Delegaten zu verwenden und manuell auswählen. Ihre Lösung behandelt dieses Szenario nicht, da Ihre Schnittstellen unterschiedlich sind, sodass der Container problemlos die richtige Implementierung auswählen kann. Während Ihr Beispiel offensichtlich funktioniert, ist es keine Lösung für das angegebene Problem.
Richard Hauer

3
@Gray Obwohl Ihr Beitrag eine schlechte Presse hat, danke ich Ihnen, dass Sie diese Lösung vorgeschlagen haben. Es gibt den Lesern eine weitere Möglichkeit, die Einschränkungen in .net-Kernen DI zu überwinden. Obwohl es die Frage des OP möglicherweise nicht direkt beantwortet, bietet es eine perfekte alternative Lösung, worum es bei SO geht, oder?
Neil Watson

5

Anscheinend können Sie einfach IEnumerable Ihrer Serviceschnittstelle einfügen! Suchen Sie dann die gewünschte Instanz mit LINQ.

Mein Beispiel ist für den AWS SNS-Dienst, aber Sie können dasselbe für jeden injizierten Dienst wirklich tun.

Anfang

foreach (string snsRegion in Configuration["SNSRegions"].Split(',', StringSplitOptions.RemoveEmptyEntries))
{
    services.AddAWSService<IAmazonSimpleNotificationService>(
        string.IsNullOrEmpty(snsRegion) ? null :
        new AWSOptions()
        {
            Region = RegionEndpoint.GetBySystemName(snsRegion)
        }
    );
}

services.AddSingleton<ISNSFactory, SNSFactory>();

services.Configure<SNSConfig>(Configuration);

SNSConfig

public class SNSConfig
{
    public string SNSDefaultRegion { get; set; }
    public string SNSSMSRegion { get; set; }
}

appsettings.json

  "SNSRegions": "ap-south-1,us-west-2",
  "SNSDefaultRegion": "ap-south-1",
  "SNSSMSRegion": "us-west-2",

SNS-Fabrik

public class SNSFactory : ISNSFactory
{
    private readonly SNSConfig _snsConfig;
    private readonly IEnumerable<IAmazonSimpleNotificationService> _snsServices;

    public SNSFactory(
        IOptions<SNSConfig> snsConfig,
        IEnumerable<IAmazonSimpleNotificationService> snsServices
        )
    {
        _snsConfig = snsConfig.Value;
        _snsServices = snsServices;
    }

    public IAmazonSimpleNotificationService ForDefault()
    {
        return GetSNS(_snsConfig.SNSDefaultRegion);
    }

    public IAmazonSimpleNotificationService ForSMS()
    {
        return GetSNS(_snsConfig.SNSSMSRegion);
    }

    private IAmazonSimpleNotificationService GetSNS(string region)
    {
        return GetSNS(RegionEndpoint.GetBySystemName(region));
    }

    private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region)
    {
        IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region);

        if (service == null)
        {
            throw new Exception($"No SNS service registered for region: {region}");
        }

        return service;
    }
}

public interface ISNSFactory
{
    IAmazonSimpleNotificationService ForDefault();

    IAmazonSimpleNotificationService ForSMS();
}

Jetzt können Sie den SNS-Dienst für die gewünschte Region in Ihrem benutzerdefinierten Dienst oder Controller abrufen

public class SmsSender : ISmsSender
{
    private readonly IAmazonSimpleNotificationService _sns;

    public SmsSender(ISNSFactory snsFactory)
    {
        _sns = snsFactory.ForSMS();
    }

    .......
 }

public class DeviceController : Controller
{
    private readonly IAmazonSimpleNotificationService _sns;

    public DeviceController(ISNSFactory snsFactory)
    {
        _sns = snsFactory.ForDefault();
    }

     .........
}

5

Ein Fabrikansatz ist sicherlich realisierbar. Ein anderer Ansatz besteht darin, mithilfe der Vererbung einzelne Schnittstellen zu erstellen, die von IService erben, die geerbten Schnittstellen in Ihren IService-Implementierungen zu implementieren und die geerbten Schnittstellen anstelle der Basis zu registrieren. Ob das Hinzufügen einer Vererbungshierarchie oder von Fabriken das "richtige" Muster ist, hängt davon ab, mit wem Sie sprechen. Ich muss dieses Muster häufig verwenden, wenn ich mit mehreren Datenbankanbietern in derselben Anwendung arbeite, die ein generisches Beispiel verwenden, z. B. IRepository<T>als Grundlage für den Datenzugriff.

Beispielschnittstellen und Implementierungen:

public interface IService 
{
}

public interface IServiceA: IService
{}

public interface IServiceB: IService
{}

public IServiceC: IService
{}

public class ServiceA: IServiceA 
{}

public class ServiceB: IServiceB
{}

public class ServiceC: IServiceC
{}

Container:

container.Register<IServiceA, ServiceA>();
container.Register<IServiceB, ServiceB>();
container.Register<IServiceC, ServiceC>();

5

Nekromantie.
Ich denke, die Leute hier erfinden das Rad neu - und wenn ich so sagen darf ...
Wenn Sie eine Komponente per Schlüssel registrieren möchten, verwenden Sie einfach ein Wörterbuch:

System.Collections.Generic.Dictionary<string, IConnectionFactory> dict = 
    new System.Collections.Generic.Dictionary<string, IConnectionFactory>(
        System.StringComparer.OrdinalIgnoreCase);

dict.Add("ReadDB", new ConnectionFactory("connectionString1"));
dict.Add("WriteDB", new ConnectionFactory("connectionString2"));
dict.Add("TestDB", new ConnectionFactory("connectionString3"));
dict.Add("Analytics", new ConnectionFactory("connectionString4"));
dict.Add("LogDB", new ConnectionFactory("connectionString5"));

Und dann registrieren Sie das Wörterbuch bei der Service-Sammlung:

services.AddSingleton<System.Collections.Generic.Dictionary<string, IConnectionFactory>>(dict);

Wenn Sie dann nicht bereit sind, das Wörterbuch abzurufen und per Schlüssel darauf zuzugreifen, können Sie das Wörterbuch ausblenden, indem Sie der Service-Sammlung eine zusätzliche Schlüssel-Suchmethode hinzufügen:
(Die Verwendung von Delegate / Closure sollte einem potenziellen Betreuer eine Chance geben verstehen, was los ist - die Pfeilnotation ist etwas kryptisch)

services.AddTransient<Func<string, IConnectionFactory>>(
    delegate (IServiceProvider sp)
    {
        return
            delegate (string key)
            {
                System.Collections.Generic.Dictionary<string, IConnectionFactory> dbs = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService
 <System.Collections.Generic.Dictionary<string, IConnectionFactory>>(sp);

                if (dbs.ContainsKey(key))
                    return dbs[key];

                throw new System.Collections.Generic.KeyNotFoundException(key); // or maybe return null, up to you
            };
    });

Jetzt können Sie mit beiden auf Ihre Typen zugreifen

IConnectionFactory logDB = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<Func<string, IConnectionFactory>>(serviceProvider)("LogDB");
logDB.Connection

oder

System.Collections.Generic.Dictionary<string, IConnectionFactory> dbs = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<System.Collections.Generic.Dictionary<string, IConnectionFactory>>(serviceProvider);
dbs["logDB"].Connection

Wie wir sehen können, ist der erste einfach völlig überflüssig, da Sie genau das auch mit einem Wörterbuch tun können, ohne Abschlüsse und AddTransient zu benötigen (und wenn Sie VB verwenden, werden nicht einmal die geschweiften Klammern unterschiedlich sein):

IConnectionFactory logDB = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<System.Collections.Generic.Dictionary<string, IConnectionFactory>>(serviceProvider)["logDB"];
logDB.Connection

(Einfacher ist besser - vielleicht möchten Sie es aber als Erweiterungsmethode verwenden)

Wenn Ihnen das Wörterbuch nicht gefällt, können Sie Ihre Benutzeroberfläche natürlich auch mit einer Eigenschaft Name(oder was auch immer) ausstatten und diese nach Schlüssel nachschlagen:

services.AddSingleton<IConnectionFactory>(new ConnectionFactory("ReadDB"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("WriteDB"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("TestDB"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("Analytics"));
services.AddSingleton<IConnectionFactory>(new ConnectionFactory("LogDB"));



// /programming/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core
services.AddTransient<Func<string, IConnectionFactory>>(
    delegate(IServiceProvider sp)
    {
        return
            delegate(string key)
            {
                System.Collections.Generic.IEnumerable<IConnectionFactory> svs = 
                    sp.GetServices<IConnectionFactory>();

                foreach (IConnectionFactory thisService in svs)
                {
                    if (key.Equals(thisService.Name, StringComparison.OrdinalIgnoreCase))
                        return thisService;
                }

                return null;
            };
    });

Dies erfordert jedoch eine Änderung der Benutzeroberfläche, um die Eigenschaft zu berücksichtigen, und das Durchlaufen vieler Elemente sollte viel langsamer sein als eine Suche nach assoziativen Arrays (Wörterbuch).
Es ist jedoch schön zu wissen, dass dies ohne Wörterbuch möglich ist.

Dies sind nur meine $ 0,05


Wenn der Dienst IDisposeimplementiert wurde, wer ist für die Entsorgung des Dienstes verantwortlich? Sie haben Wörterbuch alsSingleton
LP13

@ LP13: Sie können das Wörterbuch auch bei einem Delegaten als Wert registrieren, es dann in itransient registrieren und eine neue Instanz erstellen, z. GetRequiredService <T> () ["logDB"] ()
Stefan Steiger

5

Seit meinem obigen Beitrag bin ich in eine generische Fabrikklasse gewechselt

Verwendung

 services.AddFactory<IProcessor, string>()
         .Add<ProcessorA>("A")
         .Add<ProcessorB>("B");

 public MyClass(IFactory<IProcessor, string> processorFactory)
 {
       var x = "A"; //some runtime variable to select which object to create
       var processor = processorFactory.Create(x);
 }

Implementierung

public class FactoryBuilder<I, P> where I : class
{
    private readonly IServiceCollection _services;
    private readonly FactoryTypes<I, P> _factoryTypes;
    public FactoryBuilder(IServiceCollection services)
    {
        _services = services;
        _factoryTypes = new FactoryTypes<I, P>();
    }
    public FactoryBuilder<I, P> Add<T>(P p)
        where T : class, I
    {
        _factoryTypes.ServiceList.Add(p, typeof(T));

        _services.AddSingleton(_factoryTypes);
        _services.AddTransient<T>();
        return this;
    }
}
public class FactoryTypes<I, P> where I : class
{
    public Dictionary<P, Type> ServiceList { get; set; } = new Dictionary<P, Type>();
}

public interface IFactory<I, P>
{
    I Create(P p);
}

public class Factory<I, P> : IFactory<I, P> where I : class
{
    private readonly IServiceProvider _serviceProvider;
    private readonly FactoryTypes<I, P> _factoryTypes;
    public Factory(IServiceProvider serviceProvider, FactoryTypes<I, P> factoryTypes)
    {
        _serviceProvider = serviceProvider;
        _factoryTypes = factoryTypes;
    }

    public I Create(P p)
    {
        return (I)_serviceProvider.GetService(_factoryTypes.ServiceList[p]);
    }
}

Erweiterung

namespace Microsoft.Extensions.DependencyInjection
{
    public static class DependencyExtensions
    {
        public static IServiceCollection AddFactory<I, P>(this IServiceCollection services, Action<FactoryBuilder<I, P>> builder)
            where I : class
        {
            services.AddTransient<IFactory<I, P>, Factory<I, P>>();
            var factoryBuilder = new FactoryBuilder<I, P>(services);
            builder(factoryBuilder);
            return services;
        }
    }
}

Können Sie die Erweiterung der .AddFactory () -Methode bereitstellen?
Entwickler

Sorry Hab das gerade gesehen ... hinzugefügt
T Brown

3

Während es so aussieht, als hätte @Miguel A. Arilla klar darauf hingewiesen und ich für ihn gestimmt, habe ich zusätzlich zu seiner nützlichen Lösung eine andere Lösung erstellt, die ordentlich aussieht, aber viel mehr Arbeit erfordert.

Es hängt definitiv von der obigen Lösung ab. Im Grunde habe ich etwas Ähnliches erstellt Func<string, IService>>und es IServiceAccessorals Schnittstelle bezeichnet. Dann musste ich dem IServiceCollectionals solches einige weitere Erweiterungen hinzufügen :

public static IServiceCollection AddSingleton<TService, TImplementation, TServiceAccessor>(
            this IServiceCollection services,
            string instanceName
        )
            where TService : class
            where TImplementation : class, TService
            where TServiceAccessor : class, IServiceAccessor<TService>
        {
            services.AddSingleton<TService, TImplementation>();
            services.AddSingleton<TServiceAccessor>();
            var provider = services.BuildServiceProvider();
            var implementationInstance = provider.GetServices<TService>().Last();
            var accessor = provider.GetServices<TServiceAccessor>().First();

            var serviceDescriptors = services.Where(d => d.ServiceType == typeof(TServiceAccessor));
            while (serviceDescriptors.Any())
            {
                services.Remove(serviceDescriptors.First());
            }

            accessor.SetService(implementationInstance, instanceName);
            services.AddSingleton<TServiceAccessor>(prvd => accessor);
            return services;
        }

Der Service Accessor sieht folgendermaßen aus:

 public interface IServiceAccessor<TService>
    {
         void Register(TService service,string name);
         TService Resolve(string name);

    }

Als Endergebnis können Sie Dienste mit Namen oder benannten Instanzen registrieren, wie wir es früher mit anderen Containern getan haben. Zum Beispiel:

    services.AddSingleton<IEncryptionService, SymmetricEncryptionService, EncyptionServiceAccessor>("Symmetric");
    services.AddSingleton<IEncryptionService, AsymmetricEncryptionService, EncyptionServiceAccessor>("Asymmetric");

Das reicht vorerst aus, aber um Ihre Arbeit zu vervollständigen, ist es besser, mehr Erweiterungsmethoden hinzuzufügen, um alle Arten von Registrierungen nach demselben Ansatz abzudecken.

Es gab einen anderen Beitrag zu Stackoverflow, aber ich kann ihn nicht finden, in dem das Poster ausführlich erklärt hat, warum diese Funktion nicht unterstützt wird und wie man sie umgeht, im Grunde ähnlich wie bei @Miguel. Es war ein schöner Beitrag, obwohl ich nicht mit jedem Punkt einverstanden bin, weil ich denke, dass es Situationen gibt, in denen Sie wirklich benannte Instanzen benötigen. Ich werde diesen Link hier posten, sobald ich ihn wieder finde.

In der Tat müssen Sie diesen Selector oder Accessor nicht übergeben:

Ich verwende den folgenden Code in meinem Projekt und es hat bisher gut funktioniert.

 /// <summary>
    /// Adds the singleton.
    /// </summary>
    /// <typeparam name="TService">The type of the t service.</typeparam>
    /// <typeparam name="TImplementation">The type of the t implementation.</typeparam>
    /// <param name="services">The services.</param>
    /// <param name="instanceName">Name of the instance.</param>
    /// <returns>IServiceCollection.</returns>
    public static IServiceCollection AddSingleton<TService, TImplementation>(
        this IServiceCollection services,
        string instanceName
    )
        where TService : class
        where TImplementation : class, TService
    {
        var provider = services.BuildServiceProvider();
        var implementationInstance = provider.GetServices<TService>().LastOrDefault();
        if (implementationInstance.IsNull())
        {
            services.AddSingleton<TService, TImplementation>();
            provider = services.BuildServiceProvider();
            implementationInstance = provider.GetServices<TService>().Single();
        }
        return services.RegisterInternal(instanceName, provider, implementationInstance);
    }

    private static IServiceCollection RegisterInternal<TService>(this IServiceCollection services,
        string instanceName, ServiceProvider provider, TService implementationInstance)
        where TService : class
    {
        var accessor = provider.GetServices<IServiceAccessor<TService>>().LastOrDefault();
        if (accessor.IsNull())
        {
            services.AddSingleton<ServiceAccessor<TService>>();
            provider = services.BuildServiceProvider();
            accessor = provider.GetServices<ServiceAccessor<TService>>().Single();
        }
        else
        {
            var serviceDescriptors = services.Where(d => d.ServiceType == typeof(IServiceAccessor<TService>));
            while (serviceDescriptors.Any())
            {
                services.Remove(serviceDescriptors.First());
            }
        }
        accessor.Register(implementationInstance, instanceName);
        services.AddSingleton<TService>(prvd => implementationInstance);
        services.AddSingleton<IServiceAccessor<TService>>(prvd => accessor);
        return services;
    }

    //
    // Summary:
    //     Adds a singleton service of the type specified in TService with an instance specified
    //     in implementationInstance to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
    //
    // Parameters:
    //   services:
    //     The Microsoft.Extensions.DependencyInjection.IServiceCollection to add the service
    //     to.
    //   implementationInstance:
    //     The instance of the service.
    //   instanceName:
    //     The name of the instance.
    //
    // Returns:
    //     A reference to this instance after the operation has completed.
    public static IServiceCollection AddSingleton<TService>(
        this IServiceCollection services,
        TService implementationInstance,
        string instanceName) where TService : class
    {
        var provider = services.BuildServiceProvider();
        return RegisterInternal(services, instanceName, provider, implementationInstance);
    }

    /// <summary>
    /// Registers an interface for a class
    /// </summary>
    /// <typeparam name="TInterface">The type of the t interface.</typeparam>
    /// <param name="services">The services.</param>
    /// <returns>IServiceCollection.</returns>
    public static IServiceCollection As<TInterface>(this IServiceCollection services)
         where TInterface : class
    {
        var descriptor = services.Where(d => d.ServiceType.GetInterface(typeof(TInterface).Name) != null).FirstOrDefault();
        if (descriptor.IsNotNull())
        {
            var provider = services.BuildServiceProvider();
            var implementationInstance = (TInterface)provider?.GetServices(descriptor?.ServiceType)?.Last();
            services?.AddSingleton(implementationInstance);
        }
        return services;
    }

1
Dies half mir, mein Problem zu lösen, bei dem ich die Registrierung von Typen im Service Accessor verlor. Der Trick bestand darin, alle Bindungen für den Service Accessor zu entfernen und dann erneut hinzuzufügen!
Umar Farooq Khawaja

3

Ich habe dafür eine Bibliothek erstellt, die einige nette Funktionen implementiert. Code finden Sie auf GitHub: https://github.com/dazinator/Dazinator.Extensions.DependencyInjection NuGet: https://www.nuget.org/packages/Dazinator.Extensions.DependencyInjection/

Die Verwendung ist unkompliziert:

  1. Fügen Sie Ihrem Projekt das Nuget-Paket Dazinator.Extensions.DependencyInjection hinzu.
  2. Fügen Sie Ihre Named Service-Registrierungen hinzu.
    var services = new ServiceCollection();
    services.AddNamed<AnimalService>(names =>
    {
        names.AddSingleton("A"); // will resolve to a singleton instance of AnimalService
        names.AddSingleton<BearService>("B"); // will resolve to a singleton instance of BearService (which derives from AnimalService)
        names.AddSingleton("C", new BearService()); will resolve to singleton instance provided yourself.
        names.AddSingleton("D", new DisposableTigerService(), registrationOwnsInstance = true); // will resolve to singleton instance provided yourself, but will be disposed for you (if it implements IDisposable) when this registry is disposed (also a singleton).

        names.AddTransient("E"); // new AnimalService() every time..
        names.AddTransient<LionService>("F"); // new LionService() every time..

        names.AddScoped("G");  // scoped AnimalService
        names.AddScoped<DisposableTigerService>("H");  scoped DisposableTigerService and as it implements IDisposable, will be disposed of when scope is disposed of.

    });

Beachten Sie im obigen Beispiel, dass Sie für jede benannte Registrierung auch die Lebensdauer oder Singleton, Scoped oder Transient angeben.

Sie können Dienste auf zwei Arten auflösen, je nachdem, ob Sie damit vertraut sind, dass Ihre Dienste von diesem Paket abhängig sind:

public MyController(Func<string, AnimalService> namedServices)
{
   AnimalService serviceA = namedServices("A");
   AnimalService serviceB = namedServices("B"); // BearService derives from AnimalService
}

oder

public MyController(NamedServiceResolver<AnimalService> namedServices)
{
   AnimalService serviceA = namedServices["A"];
   AnimalService serviceB = namedServices["B"]; // instance of BearService returned derives from AnimalService
}

Ich habe diese Bibliothek speziell für die Zusammenarbeit mit Microsoft.Extensions.DependencyInjection entwickelt - zum Beispiel:

  1. Wenn Sie benannte Dienste registrieren, können alle von Ihnen registrierten Typen Konstruktoren mit Parametern haben - diese werden über DI auf die gleiche Weise erfüllt AddTransient<>, AddScoped<>und AddSingleton<>Methoden funktionieren normalerweise.

  2. Für vorübergehende und bereichsspezifische benannte Dienste erstellt die Registrierung eine, ObjectFactorydamit sie bei Bedarf sehr schnell neue Instanzen dieses Typs aktivieren kann. Dies ist viel schneller als andere Ansätze und entspricht der Vorgehensweise von Microsoft.Extensions.DependencyInjection.


2

Meine Lösung für das, was es wert ist ... erwog, zu Castle Windsor zu wechseln, da ich nicht sagen kann, dass mir eine der oben genannten Lösungen gefallen hat. Es tut uns leid!!

public interface IStage<out T> : IStage { }

public interface IStage {
      void DoSomething();
}

Erstellen Sie Ihre verschiedenen Implementierungen

public class YourClassA : IStage<YouClassA> { 
    public void DoSomething() 
    {
        ...TODO
    }
}

public class YourClassB : IStage<YourClassB> { .....etc. }

Anmeldung

services.AddTransient<IStage<YourClassA>, YourClassA>()
services.AddTransient<IStage<YourClassB>, YourClassB>()

Verwendung von Konstruktoren und Instanzen ...

public class Whatever
{
   private IStage ClassA { get; }

   public Whatever(IStage<YourClassA> yourClassA)
   {
         ClassA = yourClassA;
   }

   public void SomeWhateverMethod()
   {
        ClassA.DoSomething();
        .....
   }

1

Erweiterung der Lösung von @rnrneverdies. Anstelle von ToString () können auch die folgenden Optionen verwendet werden: 1) Bei Implementierung gemeinsamer Eigenschaften 2) Ein von @Craig Brunetti vorgeschlagener Dienst von Diensten.

public interface IService { }
public class ServiceA : IService
{
    public override string ToString()
    {
        return "A";
    }
}

public class ServiceB : IService
{
    public override string ToString()
    {
        return "B";
    }

}

/// <summary>
/// extension method that compares with ToString value of an object and returns an object if found
/// </summary>
public static class ServiceProviderServiceExtensions
{
    public static T GetService<T>(this IServiceProvider provider, string identifier)
    {
        var services = provider.GetServices<T>();
        var service = services.FirstOrDefault(o => o.ToString() == identifier);
        return service;
    }
}

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

    services.AddSingleton<IService, ServiceA>();
    services.AddSingleton<IService, ServiceB>();
    services.AddSingleton<IService, ServiceC>();

    var sp = services.BuildServiceProvider();
    var a = sp.GetService<IService>("A"); //returns instance of ServiceA
    var b = sp.GetService<IService>("B"); //returns instance of ServiceB

    //Remaining configurations....
}

1

Nachdem ich die Antworten hier und Artikel an anderer Stelle gelesen hatte, konnte ich es ohne Schnüre zum Laufen bringen. Wenn Sie mehrere Implementierungen derselben Schnittstelle haben, fügt der DI diese einer Sammlung hinzu, sodass Sie die gewünschte Version mithilfe von aus der Sammlung abrufen können typeof.

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(IService, ServiceA);
    services.AddScoped(IService, ServiceB);
    services.AddScoped(IService, ServiceC);
}

// Any class that uses the service(s)
public class Consumer
{
    private readonly IEnumerable<IService> _myServices;

    public Consumer(IEnumerable<IService> myServices)
    {
        _myServices = myServices;
    }

    public UseServiceA()
    {
        var serviceA = _myServices.FirstOrDefault(t => t.GetType() == typeof(ServiceA));
        serviceA.DoTheThing();
    }

    public UseServiceB()
    {
        var serviceB = _myServices.FirstOrDefault(t => t.GetType() == typeof(ServiceB));
        serviceB.DoTheThing();
    }

    public UseServiceC()
    {
        var serviceC = _myServices.FirstOrDefault(t => t.GetType() == typeof(ServiceC));
        serviceC.DoTheThing();
    }
}

Besiegt den Zweck von IoC. Sie können genauso gut schreiben:var serviceA = new ServiceA();
James Curran

2
@JamesCurran nicht, wenn ServiceA Abhängigkeiten hat oder wenn Sie die Klasse einem Unit-Test unterziehen möchten.
Jorn.Beyers

0

Die Out-of-the-Box-Implementierung bietet dies zwar nicht, aber hier ist ein Beispielprojekt, mit dem Sie benannte Instanzen registrieren und dann INamedServiceFactory in Ihren Code einfügen und Instanzen nach Namen abrufen können. Im Gegensatz zu anderen Facory-Lösungen können Sie hier mehrere Instanzen derselben Implementierung registrieren, die jedoch unterschiedlich konfiguriert sind

https://github.com/macsux/DotNetDINamedInstances


0

Wie wäre es mit einem Service für Services?

Wenn wir eine INamedService-Schnittstelle (mit der Eigenschaft .Name) hätten, könnten wir eine IServiceCollection-Erweiterung für .GetService (Zeichenfolgenname) schreiben, wobei die Erweiterung diesen Zeichenfolgenparameter übernehmen und eine .GetServices () für sich selbst und für jede zurückgegebene ausführen würde Suchen Sie die Instanz, deren INamedService.Name mit dem angegebenen Namen übereinstimmt.

So was:

public interface INamedService
{
    string Name { get; }
}

public static T GetService<T>(this IServiceProvider provider, string serviceName)
    where T : INamedService
{
    var candidates = provider.GetServices<T>();
    return candidates.FirstOrDefault(s => s.Name == serviceName);
}

Daher muss Ihr IMyService INamedService implementieren, aber Sie erhalten die gewünschte schlüsselbasierte Auflösung, oder?

Um fair zu sein, scheint es hässlich, diese INamedService-Schnittstelle haben zu müssen. Wenn Sie jedoch noch weiter gehen und die Dinge eleganter gestalten möchten, kann der Code in diesem Code ein [NamedServiceAttribute ("A")] für die Implementierung / Klasse finden Erweiterung, und es würde genauso gut funktionieren. Um noch fairer zu sein, Reflection ist langsam, daher ist eine Optimierung möglicherweise angebracht, aber ehrlich gesagt hätte die DI-Engine dabei helfen sollen. Geschwindigkeit und Einfachheit tragen maßgeblich zu den Gesamtbetriebskosten bei.

Alles in allem ist keine explizite Factory erforderlich, da das Finden eines benannten Service ein so wiederverwendbares Konzept ist und Factory-Klassen nicht als Lösung skaliert werden können. Und ein Func <> scheint in Ordnung zu sein, aber ein Schalterblock ist so bleh , und wieder werden Sie Funcs so oft schreiben, wie Sie Fabriken schreiben würden. Beginnen Sie einfach, wiederverwendbar, mit weniger Code, und wenn sich herausstellt, dass dies für Sie nicht der Fall ist, werden Sie komplex.


2
Dies wird als Service Locator-Muster bezeichnet und ist in der Regel nicht die beste Route, es sei denn, Sie müssen unbedingt
Joe Phillips

@ JoePhillips Hast du einen Input, warum es keine gute Lösung ist? Ich liebe die Eleganz davon. Der einzige Nachteil, den ich mir vorstellen kann, ist, dass ich jedes Mal eine Instanz von allen erstelle, wenn Sie eine bekommen.
Peter

2
@ Peter Der Hauptgrund ist, dass es sehr, sehr schwer ist, damit zu arbeiten. Wenn Sie ein serviceLocator-Objekt an eine Klasse übergeben, ist es überhaupt nicht offensichtlich, welche Abhängigkeiten diese Klasse verwendet, da sie alle von einem magischen "Gott" -Objekt stammen. Stellen Sie sich vor, Sie müssen Referenzen des Typs finden, den Sie ändern möchten. Diese Fähigkeit verschwindet im Grunde genommen, wenn Sie alles über ein Service Locator-Objekt abrufen. Konstruktorinjektion ist viel klarer und zuverlässiger
Joe Phillips

Ich weiß nicht. Die Offensichtlichkeit ist für mich kein Minus ... denn wenn ich nachverfolgen möchte, wie meine Komponenten ihre Abhängigkeiten nutzen, hätte ich Unit-Tests dafür ... Tests, die sich nicht nur auf jede Abhängigkeit beziehen, sondern uns auch beim Verständnis helfen WIE jede Abhängigkeit benötigt wird. Wie sonst werden Sie sich dessen bewusst, wenn Sie Konstruktoren lesen?!?
Craig Brunetti

0

Ich bin auf dasselbe Problem gestoßen und habe mit einer einfachen Erweiterung gearbeitet, um benannte Dienste zuzulassen. Sie finden es hier:

Sie können so viele (benannte) Dienste hinzufügen, wie Sie möchten:

 var serviceCollection = new ServiceCollection();
 serviceCollection.Add(typeof(IMyService), typeof(MyServiceA), "A", ServiceLifetime.Transient);
 serviceCollection.Add(typeof(IMyService), typeof(MyServiceB), "B", ServiceLifetime.Transient);

 var serviceProvider = serviceCollection.BuildServiceProvider();

 var myServiceA = serviceProvider.GetService<IMyService>("A");
 var myServiceB = serviceProvider.GetService<IMyService>("B");

Mit der Bibliothek können Sie auch ein "Factory-Muster" wie folgt einfach implementieren:

    [Test]
    public void FactoryPatternTest()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.Add(typeof(IMyService), typeof(MyServiceA), MyEnum.A.GetName(), ServiceLifetime.Transient);
        serviceCollection.Add(typeof(IMyService), typeof(MyServiceB), MyEnum.B.GetName(), ServiceLifetime.Transient);

        serviceCollection.AddTransient<IMyServiceFactoryPatternResolver, MyServiceFactoryPatternResolver>();

        var serviceProvider = serviceCollection.BuildServiceProvider();

        var factoryPatternResolver = serviceProvider.GetService<IMyServiceFactoryPatternResolver>();

        var myServiceA = factoryPatternResolver.Resolve(MyEnum.A);
        Assert.NotNull(myServiceA);
        Assert.IsInstanceOf<MyServiceA>(myServiceA);

        var myServiceB = factoryPatternResolver.Resolve(MyEnum.B);
        Assert.NotNull(myServiceB);
        Assert.IsInstanceOf<MyServiceB>(myServiceB);
    }

    public interface IMyServiceFactoryPatternResolver : IFactoryPatternResolver<IMyService, MyEnum>
    {
    }

    public class MyServiceFactoryPatternResolver : FactoryPatternResolver<IMyService, MyEnum>, IMyServiceFactoryPatternResolver
    {
        public MyServiceFactoryPatternResolver(IServiceProvider serviceProvider)
        : base(serviceProvider)
        {
        }
    }

    public enum MyEnum
    {
        A = 1,
        B = 2
    }

Ich hoffe es hilft


0

Ich habe meine eigene Erweiterung über die IServiceCollectionverwendete WithNameErweiterung erstellt:

public static IServiceCollection AddScopedWithName<TService, TImplementation>(this IServiceCollection services, string serviceName)
        where TService : class
        where TImplementation : class, TService
    {
        Type serviceType = typeof(TService);
        Type implementationServiceType = typeof(TImplementation);
        ServiceCollectionTypeMapper.Instance.AddDefinition(serviceType.Name, serviceName, implementationServiceType.AssemblyQualifiedName);
        services.AddScoped<TImplementation>();
        return services;
    }

ServiceCollectionTypeMappereine Singleton - Instanz ist , dass die Karten IService> NameOfService> Implementationwo eine Schnittstelle viele Implementierungen mit unterschiedlichen Namen haben könnte, erlaubt diese Typen zu registrieren , als wir , wenn wee Bedarf lösen können und ist ein anderer Ansatz als Dienste mehrere Entschlossenheit zu wählen , was wir wollen.

 /// <summary>
/// Allows to set the service register mapping.
/// </summary>
public class ServiceCollectionTypeMapper
{
    private ServiceCollectionTypeMapper()
    {
        this.ServiceRegister = new Dictionary<string, Dictionary<string, string>>();
    }

    /// <summary>
    /// Gets the instance of mapper.
    /// </summary>
    public static ServiceCollectionTypeMapper Instance { get; } = new ServiceCollectionTypeMapper();

    private Dictionary<string, Dictionary<string, string>> ServiceRegister { get; set; }

    /// <summary>
    /// Adds new service definition.
    /// </summary>
    /// <param name="typeName">The name of the TService.</param>
    /// <param name="serviceName">The TImplementation name.</param>
    /// <param name="namespaceFullName">The TImplementation AssemblyQualifiedName.</param>
    public void AddDefinition(string typeName, string serviceName, string namespaceFullName)
    {
        if (this.ServiceRegister.TryGetValue(typeName, out Dictionary<string, string> services))
        {
            if (services.TryGetValue(serviceName, out _))
            {
                throw new InvalidOperationException($"Exists an implementation with the same name [{serviceName}] to the type [{typeName}].");
            }
            else
            {
                services.Add(serviceName, namespaceFullName);
            }
        }
        else
        {
            Dictionary<string, string> serviceCollection = new Dictionary<string, string>
            {
                { serviceName, namespaceFullName },
            };
            this.ServiceRegister.Add(typeName, serviceCollection);
        }
    }

    /// <summary>
    /// Get AssemblyQualifiedName of implementation.
    /// </summary>
    /// <typeparam name="TService">The type of the service implementation.</typeparam>
    /// <param name="serviceName">The name of the service.</param>
    /// <returns>The AssemblyQualifiedName of the inplementation service.</returns>
    public string GetService<TService>(string serviceName)
    {
        Type serviceType = typeof(TService);

        if (this.ServiceRegister.TryGetValue(serviceType.Name, out Dictionary<string, string> services))
        {
            if (services.TryGetValue(serviceName, out string serviceImplementation))
            {
                return serviceImplementation;
            }
            else
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }

So registrieren Sie einen neuen Dienst:

services.AddScopedWithName<IService, MyService>("Name");

Um den Service zu lösen, benötigen wir eine solche Erweiterung IServiceProvider.

/// <summary>
    /// Gets the implementation of service by name.
    /// </summary>
    /// <typeparam name="T">The type of service.</typeparam>
    /// <param name="serviceProvider">The service provider.</param>
    /// <param name="serviceName">The service name.</param>
    /// <returns>The implementation of service.</returns>
    public static T GetService<T>(this IServiceProvider serviceProvider, string serviceName)
    {
        string fullnameImplementation = ServiceCollectionTypeMapper.Instance.GetService<T>(serviceName);
        if (fullnameImplementation == null)
        {
            throw new InvalidOperationException($"Unable to resolve service of type [{typeof(T)}] with name [{serviceName}]");
        }
        else
        {
            return (T)serviceProvider.GetService(Type.GetType(fullnameImplementation));
        }
    }

Bei der Lösung:

serviceProvider.GetService<IWithdrawalHandler>(serviceName);

Denken Sie daran, dass serviceProvider in einen Konstruktor in unserer Anwendung als eingefügt werden kann IServiceProvider.

Ich hoffe das hilft.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.