Jak zarejestrować wiele implementacji tego samego interfejsu w Asp.Net Core?

239

Mam usługi pochodzące z tego samego interfejsu.

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

Zazwyczaj inne kontenery IoC, takie jak, Unitypozwalają rejestrować konkretne implementacje przez niektóre, Keyktóre je wyróżniają.

W programie ASP.NET Core, w jaki sposób mogę zarejestrować te usługi i rozwiązać je w czasie wykonywania na podstawie jakiegoś klucza?

Nie widzę żadnych Addmetod usługi, które pobierają parametr keylub name, które zwykle byłyby używane do rozróżnienia konkretnej implementacji.

    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?
       }
    }

Czy wzór fabryczny jest tutaj jedyną opcją?

Aktualizacja
1 Poszedłem przez ten artykuł , który pokazuje, jak używać wzorca fabrycznego, aby uzyskać wystąpienia usług, gdy mamy wiele konkretnych implementacji. Jednak wciąż nie jest to kompletne rozwiązanie. Podczas wywoływania _serviceProvider.GetService()metody nie mogę wstrzykiwać danych do konstruktora.

Rozważ na przykład:

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

Jak _serviceProvider.GetService()wstrzyknąć odpowiedni ciąg połączenia? W Unity lub innej bibliotece IoC możemy to zrobić przy rejestracji typu. Mogę jednak użyć IOption , co wymaga ode mnie wprowadzenia wszystkich ustawień. Nie mogę wstrzyknąć określonego ciągu połączenia do usługi.

Zauważ też, że staram się unikać używania innych kontenerów (w tym Unity), ponieważ wtedy muszę zarejestrować wszystko inne (np. Kontrolery) w nowym kontenerze.

Ponadto używanie wzorca fabrycznego do tworzenia instancji usług jest niezgodne z DIP, ponieważ zwiększa liczbę zależności, które klient ma tutaj .

Tak więc myślę, że domyślnemu DI w ASP.NET Core brakuje dwóch rzeczy:

  1. Możliwość rejestrowania instancji za pomocą klucza
  2. Możliwość wstrzykiwania statycznych danych do konstruktorów podczas rejestracji
LP13
źródło
4
Możliwy duplikat rozwiązania wtrysku zależności według nazwy
adem caglin
2
Nuget
Cześć, przepraszam za moje głupie pytanie, ale jestem nowy w Microsoft.Extensions.DependencyInjection ... czy myślisz, że stworzysz 3 puste interfejsy, które rozszerzają Iservice, takie jak „publiczny interfejs IServiceA: IService” i „public class ServiceA: IServiceA „... może być dobrą praktyką?
Emiliano Magliocca,
czy ten artykuł ma jakiekolwiek zastosowanie? stevejgordon.co.uk/…
Mike B,
Można Update1przejść do innego pytania, ponieważ wstrzykiwanie rzeczy do konstruktorów różni się bardzo od określenia, który obiekt ma zostać zbudowany
Neil

Odpowiedzi:

245

FuncKiedy znalazłem się w takiej sytuacji, zrobiłem proste obejście .

Najpierw zadeklaruj udostępnionego delegata:

public delegate IService ServiceResolver(string key);

Następnie Startup.csskonfiguruj wiele konkretnych rejestracji i ręczne mapowanie tych typów:

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

I użyj go z dowolnej klasy zarejestrowanej w DI:

public class Consumer
{
    private readonly IService _aService;

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

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

Należy pamiętać, że w tym przykładzie kluczem do rozwiązania jest ciąg znaków, ze względu na prostotę i ponieważ OP poprosił w szczególności o tę sprawę.

Ale możesz użyć dowolnego niestandardowego typu rozdzielczości jako klucza, ponieważ zwykle nie chcesz, aby ogromny przełącznik n-case gnił kod. Zależy od sposobu skalowania aplikacji.

Miguel A. Arilla
źródło
1
@MatthewStevenMonkan zaktualizował moją odpowiedź przykładem
Miguel A. Arilla
2
Najlepszym sposobem jest użycie takiego wzoru fabrycznego. Dzięki za udostępnienie!
Sergey Akopov,
2
+1 Bardzo schludne i czyste, ponieważ kiedy używamy innego pojemnika, musimy dołączyć jego paczkę, ilekroć musimy rozwiązać zależności, np. ILifetimeScope w AutoFac.
Anupam Singh
1
@AnupamSingh Moim zdaniem większość małych i średnich aplikacji działających na platformie .NET Core nie potrzebuje żadnych ram DI, tylko dodaje złożoności i niepożądanych zależności, piękno i prostota wbudowanego DI jest więcej niż wystarczająca i może z łatwością można również rozszerzyć.
Miguel A. Arilla,
7
Objaśnienie głosowania w dół - jest bardzo interesujące, ale obecnie refaktoryzuję ogromną bazę kodu, aby usunąć całą tę magię Func, którą ktoś zrobił kilka lat temu (przed rewolucją MS DI) Problem polega na tym, że dramatycznie zwiększa złożoność powiązań właściwości, które może powodować zawiłą rozdzielczość DI w dalszej linii. Na przykład pracowałem nad usługą obsługi systemu Windows, która zawierała ponad 1,6 tys. Linii kodu związanych z Func, a po wykonaniu zalecanego sposobu DI zmniejszyłem go do 0,2 tys. Linii. OK-Wiersze kodu nic nie znaczą… poza tym, że teraz są łatwiejsze do odczytania i ponownego użycia…
Piotr Kula
79

Inną opcją jest użycie metody rozszerzenia GetServicesz Microsoft.Extensions.DependencyInjection.

Zarejestruj swoje usługi jako:

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

Następnie rozwiąż z odrobiną Linq:

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

lub

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

(zakładając, że IServicema właściwość ciągu o nazwie „Nazwa”)

Upewnij się, że masz using Microsoft.Extensions.DependencyInjection;

Aktualizacja

Źródło AspNet 2.1: GetServices

rnrneverdies
źródło
6
Nie jestem pewien, ale myślę, że to nie jest deterministyczne. Wszelkie wyniki, które dziś uzyskasz, jutro mogą się zmienić, nie wydaje się to dobrą praktyką.
rnrneverdies
4
głosuj na link do GetServices, który pokazał mi, że możesz poprosić o listę usług zależnych usług, proszącIEnumerable<IService>
Johnny 5
20
serviceProvider.GetServices <IService> () utworzy instancję każdego z ServiceA, ServiceB i ServiceC. Chcesz wywołać konstruktora tylko jednej usługi - tej, której naprawdę potrzebujesz. Jest to duży problem, jeśli implementacje nie są lekkie lub masz wiele implementacji IService (na przykład masz automatycznie wygenerowane implementacje IRepository dla każdego modelu).
Uros,
6
Zgadzam się z @Uros. To nie jest dobre rozwiązanie. Wyobraź sobie, co się stanie, jeśli zarejestrujesz 10 implementacji IService, a instancja, której naprawdę potrzebujesz, jest ostatnią. W tym przypadku 9 instancji jest tworzonych przez DI, które nigdy nie są używane.
thomai
4
Zły pomysł: wiele nieużywanych instancji, anty-wzorzec lokalizatora usług i bezpośrednie połączenie z faktyczną implementacją (typ <ServiceA>).
Rico Suter,
20

To nie jest obsługiwane przez Microsoft.Extensions.DependencyInjection.

Możesz jednak podłączyć inny mechanizm wstrzykiwania zależności, na przykład StructureMap Zobacz stronę główną i projekt GitHub .

To wcale nie jest trudne:

  1. Dodaj zależność do StructureMap w swoim project.json:

    "Structuremap.Microsoft.DependencyInjection" : "1.0.1",
  2. Wstaw go do potoku ASP.NET wewnątrz ConfigureServicesi zarejestruj swoje klasy (zobacz dokumenty)

    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. Następnie, aby uzyskać nazwane wystąpienie, musisz poprosić o IContainer

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

Otóż ​​to.

Aby zbudować przykład, potrzebujesz

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

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

        public string Name {get; set; }
    }
Gerardo Grignoli
źródło
Wypróbowałem to podejście, ale dostaję błędy w czasie wykonywania na moim kontrolerze, ponieważ IContainer nie znajduje się w planach kompilacji. Czy jest coś, co muszę wymagać, aby IContainer był automatycznie wstrzykiwany?
mohrtan
BTW, używam StructureMap.Micorosoft.DependencyInjection 1.3.0.
mohrtan
Czy zwracasz nowy kontener w ConfigureServices?
Gerardo Grignoli
Zwracam nowy kontener IServiceProviderInstance, jak wskazano w kroku 2 powyżej. Skopiowałem to dokładnie, zmieniając to tylko dla moich typów. To dobre rozwiązanie i działa idealnie. Jedyną wadą jest to, że nie mogę użyć wstrzykniętego pojemnika i uciekam się do statycznego pojemnika, czego nie chcę robić.
mohrtan
1
Działa dla mnie dzięki Gerardo Grignoli. @mohrtan przykładowy kod jest tutaj, jeśli nadal się tym zajmujesz. github.com/Yawarmurtaza/AspNetCoreStructureMap
Yawar Murtaza
13

Masz rację, wbudowany kontener ASP.NET Core nie ma koncepcji rejestrowania wielu usług, a następnie pobierania konkretnej, jak sugerujesz, fabryka jest jedynym realnym rozwiązaniem w tym przypadku.

Alternatywnie możesz przełączyć się na pojemnik innej firmy, taki jak Unity lub StructureMap, który zapewnia potrzebne rozwiązanie (udokumentowane tutaj: https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing- domyślny kontener usług ).

Skarpetka
źródło
13

Napotkałem ten sam problem i chcę podzielić się tym, jak go rozwiązałem i dlaczego.

Jak wspomniałeś, istnieją dwa problemy. Pierwszy:

W Asp.Net Core, jak zarejestrować te usługi i rozwiązać je w czasie wykonywania na podstawie jakiegoś klucza?

Jakie mamy opcje? Ludzie sugerują dwa:

  • Użyj niestandardowej fabryki (jak _myFactory.GetServiceByKey(key))

  • Użyj innego silnika DI (jak _unityContainer.Resolve<IService>(key))

Czy wzór fabryczny jest tutaj jedyną opcją?

W rzeczywistości obie opcje są fabrykami, ponieważ każdy kontener IoC jest również fabryką (wysoce konfigurowalny i skomplikowany). I wydaje mi się, że inne opcje są również odmianami wzoru fabrycznego.

Więc która opcja jest lepsza? Tutaj zgadzam się z @Sock, który zaproponował użycie fabryki niestandardowej i dlatego.

Po pierwsze, zawsze staram się unikać dodawania nowych zależności, gdy nie są one naprawdę potrzebne. Zgadzam się z tobą w tym punkcie. Co więcej, użycie dwóch struktur DI jest gorsze niż tworzenie niestandardowej abstrakcji fabryki. W drugim przypadku musisz dodać nową zależność pakietu (jak Unity), ale w zależności od nowego interfejsu fabrycznego jest tutaj mniej zła. Myślę, że główną ideą ASP.NET Core DI jest prostota. Zachowuje minimalny zestaw funkcji zgodnie z zasadą KISS . Jeśli potrzebujesz dodatkowej funkcji, zrób to sam lub skorzystaj z odpowiedniego Plungin, który implementuje pożądaną funkcję (zasada otwartej zamkniętej).

Po drugie, często musimy wprowadzić wiele nazwanych zależności dla pojedynczej usługi. W przypadku Unity może być konieczne podanie nazw parametrów konstruktora (za pomocą InjectionConstructor). Ta rejestracja używa refleksji i inteligentnej logiki do odgadywania argumentów dla konstruktora. Może to również prowadzić do błędów w czasie wykonywania, jeśli rejestracja nie pasuje do argumentów konstruktora. Z drugiej strony, korzystając z własnej fabryki, masz pełną kontrolę nad tym, jak podać parametry konstruktora. Jest bardziej czytelny i rozwiązany w czasie kompilacji. Znowu zasada KISS .

Drugi problem:

W jaki sposób _serviceProvider.GetService () wstrzykuje odpowiedni ciąg połączenia?

Po pierwsze, zgadzam się z tobą, że w zależności od nowych rzeczy, takich jak IOptions(a zatem od pakietu Microsoft.Extensions.Options.ConfigurationExtensions), nie jest dobrym pomysłem. Widziałem kilka dyskusji na temat IOptionsróżnych opinii na temat jego zalet. Ponownie staram się unikać dodawania nowych zależności, gdy nie są one naprawdę potrzebne. Czy to naprawdę potrzebne? Myśle że nie. W przeciwnym razie każda implementacja musiałaby na niej polegać bez wyraźnej potrzeby wynikającej z tej implementacji (dla mnie wygląda to na naruszenie usługodawcy internetowego, w przypadku którego również się z tobą zgadzam). Dotyczy to również zależności od fabryki, ale w tym przypadku można tego uniknąć.

ASP.NET Core DI zapewnia bardzo ładne przeciążenie w tym celu:

var mongoConnection = //...
var efConnection = //...
var otherConnection = //...
services.AddTransient<IMyFactory>(
             s => new MyFactoryImpl(
                 mongoConnection, efConnection, otherConnection, 
                 s.GetService<ISomeDependency1>(), s.GetService<ISomeDependency2>())));
Neleus
źródło
Cześć, przepraszam za moje głupie pytanie, ale jestem nowy w Microsoft.Extensions.DependencyInjection ... czy uważasz, że tworzą 3 interfejsy, które rozszerzają Iservice, takie jak „publiczny interfejs IServiceA: IService” i „public class ServiceA: IServiceA” ... może być dobrą praktyką?
Emiliano Magliocca
1
@ emiliano-magliocca Ogólnie, w twoim przypadku nie powinieneś polegać na interfejsach, których nie używasz (ISP) IServiceA. Ponieważ używasz IServicetylko metod od , powinieneś mieć zależność IServicetylko od.
neleus
1
@ cagatay-kalan W przypadku pytania OP może on łatwo osiągnąć swój cel dzięki ASP.NET Core DI. Nie ma potrzeby stosowania innych frameworków DI.
neleus
1
@EmilianoMagliocca Można to łatwo rozwiązać w ten sposób: services.AddTransient<MyFirstClass>( s => new MyFirstClass(s.GetService<Escpos>()));dla pierwszej klasy i services.AddTransient<MySecondClass>( s => new MySecondClass(s.GetService<Usbpos>()));dla drugiej.
neleus
1
@EmilianoMagliocca w moim przykładzie zarówno „MyFirstClass”, jak i „MySecondClass” mają ten sam parametr ctor typu interfejsu, który implementują zarówno Escpos, jak i Usbpos. Tak więc powyższy kod instruuje tylko kontener IoC, jak utworzyć instancję „MyFirstClass” i „MySecondClass”. Nic więcej. Ponadto może być konieczne mapowanie niektórych innych interfejsów na „MyFirstClass” i „MySecondClass”. To zależy od twoich potrzeb i nie opisałem tego w moim przykładzie.
neleus
13

Po prostu wstrzykuję IEnumerable

ConfigureServices w Startup.cs

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

Folder usług

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;
    }
T Brown
źródło
W metodzie DoSomething () kontrolera możesz użyć typeof do rozwiązania żądanej usługi: var service = _services.FirstOrDefault (t => t.GetType () == typeof (ServiceA));
Ciaran Bruen
Dosłownie próbowałem wszystkiego i to jest jedyne rozwiązanie, które mi się udało. Dzięki!
Skatz1990
@ Skatz1990 Wypróbuj rozwiązanie, które utworzyłem poniżej w innym poście. Myślę, że jest czystszy i prostszy w użyciu.
T Brown
12

Większość odpowiedzi tutaj narusza zasadę pojedynczej odpowiedzialności (klasa usług nie powinna sama rozwiązywać zależności) i / lub używa anty-wzorca lokalizatora usług.

Inną opcją uniknięcia tych problemów jest:

  • użyj dodatkowego parametru typu ogólnego w interfejsie lub nowego interfejsu implementującego interfejs inny niż ogólny,
  • zaimplementuj klasę adaptera / przechwytywacza, aby dodać typ znacznika, a następnie
  • użyj typu ogólnego jako „name”

Napisałem artykuł z bardziej szczegółowymi informacjami: Wstrzykiwanie zależności w .NET: sposób obejścia brakujących nazwanych rejestracji

Rico Suter
źródło
w jaki sposób zaakceptowana odpowiedź narusza zasadę pojedynczej odpowiedzialności?
LP13,
Zobacz komentarze stackoverflow.com/a/52066039/876814, a także w zaakceptowanej odpowiedzi usługa jest rozwiązywana leniwie, tzn. Wiesz tylko, czy nie powiedzie się w czasie wykonywania i nie ma sposobu, aby sprawdzić to statycznie podczas uruchamiania po kompilacji kontenera (podobnie do odpowiedź w komentarzu). SRP, ponieważ usługa jest odpowiedzialna nie tylko za logikę biznesową, ale także za rozwiązywanie zależności
Rico Suter
@RicoSuter Naprawdę podoba mi się rozwiązanie na twoim blogu, ale jestem zdezorientowany przez twoją DI w klasie Startup. W szczególności nie rozumiem wiersza MessagePublisher („MyOrderCreatedQueue”), ponieważ nie widzę konstruktora z tym podpisem. services.AddSingleton <IMessagePublisher <OrderCreatedMessage>> (nowy MessagePublisher <OrderCreatedMessage> (nowy MessagePublisher („MyOrderCreatedQueue”)));
Lee Z
Dzięki, zaktualizowałem artykuł i wykorzystałem MyMessagePublisher jako przykładową implementację IMessagePublisher
Rico Suter
7

Trochę za późno na tę imprezę, ale oto moje rozwiązanie: ...

Startup.cs lub Program.cs, jeśli Generic Handler ...

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

IMyInterface T Interface Setup

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

Konkretne wdrożenia interfejsu IMyInterface of T

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

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

Mam nadzieję, że jeśli wystąpią jakiekolwiek problemy z robieniem tego w ten sposób, ktoś uprzejmie wskaże, dlaczego jest to niewłaściwy sposób.

Szary
źródło
2
IMyInterface<CustomerSavedConsumer>i IMyInterface<ManagerSavedConsumer>różnymi rodzajami usług - to wcale nie odpowiada na pytanie PO.
Richard Hauer,
2
OP szukał sposobu zarejestrowania wielu implementacji tego samego interfejsu w rdzeniu Asp.net. Jeśli tego nie zrobiłem, proszę wyjaśnić, jak (dokładnie).
Gray
1
Chociaż masz rację, ten wzór pozwala na efekt, który chciał operacja. Przynajmniej kiedy próbowałem to zrobić, natknąłem się na ten post i moje rozwiązanie najlepiej pasowało do mojej sytuacji.
Gray
1
Oczekuję, że problem polegał bardziej na tym, że rejestracja wielu implementacji dla jednego interfejsu (przy użyciu MS DI) nie pozwala kontenerowi na odróżnienie jednej implementacji od drugiej. W innych DI możesz je wpisać, aby kontener wiedział, który wybrać. W MS Państwo mają używać delegata i wybrać ręcznie. Twoje rozwiązanie nie rozwiązuje tego scenariusza, ponieważ interfejsy są różne, więc kontener nie ma problemu z wybraniem właściwej implementacji. Chociaż próbka oczywiście działa, nie jest to rozwiązanie problemu, jak stwierdzono.
Richard Hauer
3
@Gray Mimo, że Twój post został źle wydrukowany, dziękuję za zaproponowanie tego rozwiązania. Daje to czytelnikom kolejną opcję przezwyciężenia ograniczeń w rdzeniach DI .net. Chociaż może nie odpowiadać bezpośrednio na pytanie PO, stanowi idealne alternatywne rozwiązanie, na czym właśnie polega SO, prawda?
Neil Watson,
5

Najwyraźniej możesz po prostu wstrzyknąć IEnumerable interfejsu usługi! A następnie znajdź instancję, której chcesz używać LINQ.

Mój przykład dotyczy usługi AWS SNS, ale możesz zrobić to samo dla każdej wstrzykniętej usługi.

Uruchomienie

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 Factory

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

Teraz możesz uzyskać usługę SNS dla regionu, który chcesz w niestandardowej usłudze lub kontrolerze

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

     .........
}
ArcadeRenegade
źródło
5

Podejście fabryczne jest z pewnością realne. Innym podejściem jest wykorzystanie dziedziczenia do tworzenia indywidualnych interfejsów, które dziedziczą z IService, implementacji odziedziczonych interfejsów w implementacjach IService i rejestrowania odziedziczonych interfejsów zamiast bazy. To, czy dodanie hierarchii dziedziczenia lub fabryk jest „właściwym” wzorcem, wszystko zależy od tego, z kim rozmawiasz. Często muszę używać tego wzorca w kontaktach z wieloma dostawcami baz danych w tej samej aplikacji, która używa ogólnych, takich jak IRepository<T>podstawa dostępu do danych.

Przykładowe interfejsy i implementacje:

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
{}

Pojemnik:

container.Register<IServiceA, ServiceA>();
container.Register<IServiceB, ServiceB>();
container.Register<IServiceC, ServiceC>();
jlc397
źródło
5

Nekromancja.
Myślę, że ludzie tutaj wymyślają na nowo koło - i źle, jeśli mogę tak powiedzieć ...
Jeśli chcesz zarejestrować komponent według klucza, po prostu użyj słownika:

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

A następnie zarejestruj słownik w kolekcji usług:

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

jeśli następnie nie chcesz uzyskać słownika i uzyskiwać do niego dostęp kluczem, możesz ukryć słownik, dodając dodatkową metodę wyszukiwania klucza do zbioru usług:
(użycie delegata / zamknięcia powinno dać potencjalnemu opiekunowi szansę na zrozumienie, co się dzieje - notacja ze strzałkami jest nieco tajemnicza)

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

Teraz możesz uzyskać dostęp do swoich typów za pomocą jednego z nich

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

lub

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

Jak widzimy, pierwszy jest po prostu całkowicie zbędny, ponieważ możesz zrobić to dokładnie za pomocą słownika, bez konieczności zamykania i AddTransient (a jeśli używasz VB, nawet nawiasy klamrowe będą się różnić):

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

(prostsze jest lepsze - możesz jednak użyć go jako metody rozszerzenia)

Oczywiście, jeśli nie podoba ci się słownik, możesz również wyposażyć interfejs w właściwość Name(lub cokolwiek innego) i sprawdzić to według klucza:

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

Ale to wymaga zmiany interfejsu w celu dostosowania go do właściwości, a przechodzenie przez wiele elementów powinno być znacznie wolniejsze niż wyszukiwanie tablic asocjacyjnych (słownik).
Miło jest wiedzieć, że można tego dokonać bez słownika.

To tylko moje 0,05 $

Stefan Steiger
źródło
Jeśli usługa została IDisposewdrożona, kto jest odpowiedzialny za zbywanie usługi? Zarejestrowałeś słownik jakoSingleton
LP13,
@ LP13: Możesz także zarejestrować słownik z delegatem jako wartość, a następnie możesz zarejestrować go jako przejściowy i utworzyć nową instancję, np. GetRequiredService <T> () [„logDB”] ()
Stefan Steiger,
5

od czasu mojego postu przeniosłem się do Generic Factory Class

Stosowanie

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

Realizacja

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

Rozbudowa

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;
        }
    }
}
T Brown
źródło
Czy możesz podać rozszerzenie metody .AddFactory ()?
programista
Przepraszam Właśnie to widziałem ... dodane
T Brown
3

Chociaż wydaje się, że @Mueluel A. Arilla wyraźnie to podkreślił i głosowałem za nim, stworzyłem na jego użyteczne rozwiązanie inne rozwiązanie, które wygląda schludnie, ale wymaga dużo więcej pracy.

To zdecydowanie zależy od powyższego rozwiązania. Zasadniczo stworzyłem coś podobnego Func<string, IService>>i nazwałem to IServiceAccessorinterfejsem, a następnie musiałem dodać kilka takich rozszerzeń IServiceCollection:

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

Usługa Accessor wygląda następująco:

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

    }

W rezultacie będziesz mógł zarejestrować usługi o nazwach lub nazwanych instancjach, tak jak to robiliśmy z innymi kontenerami .. na przykład:

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

Na razie wystarczy, ale aby zakończyć pracę, lepiej jest dodać więcej metod rozszerzenia, aby móc objąć wszystkie typy rejestracji według tego samego podejścia.

Był inny post na temat przepełnienia stosu, ale nie mogę go znaleźć, w którym plakat szczegółowo wyjaśnił, dlaczego ta funkcja nie jest obsługiwana i jak ją obejść, w zasadzie podobny do tego, co stwierdził @Miguel. To był miły post, chociaż nie zgadzam się z każdym punktem, ponieważ myślę, że są sytuacje, w których naprawdę potrzebujesz nazwanych instancji. Prześlę tutaj ten link, gdy go znajdę ponownie.

W rzeczywistości nie musisz przekazywać tego Selektora ani Akcesorium:

Korzystam z następującego kodu w moim projekcie i do tej pory działał dobrze.

 /// <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;
    }
Assil
źródło
1
Pomogło to rozwiązać mój problem polegający na tym, że traciłem rejestrację typów w module dostępu do usługi. Sztuką było usunięcie wszystkich powiązań dla akcesora usługi, a następnie dodanie go ponownie!
Umar Farooq Khawaja
3

Stworzyłem do tego bibliotekę, która implementuje kilka fajnych funkcji. Kod można znaleźć na GitHub: https://github.com/dazinator/Dazinator.Extensions.DependencyInjection NuGet: https://www.nuget.org/packages/Dazinator.Extensions.DependencyInjection/

Użycie jest proste:

  1. Dodaj pakiet nuget Dazinator.Extensions.DependencyInjection do swojego projektu.
  2. Dodaj swoje rejestracje usługi nazwanej.
    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.

    });

W powyższym przykładzie zauważ, że dla każdej nazwanej rejestracji określasz także czas życia lub Singleton, Scoped lub Transient.

Możesz rozwiązać usługi na jeden z dwóch sposobów, w zależności od tego, czy czujesz się komfortowo, aby twoje usługi zależały od tego pakietu:

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

lub

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

Specjalnie zaprojektowałem tę bibliotekę, aby dobrze współpracowała z Microsoft.Extensions.DependencyInjection - na przykład:

  1. Kiedy rejestrujesz nazwane usługi, każdy rejestrowany typ może mieć konstruktora z parametrami - będą spełniane przez DI, w taki sam sposób AddTransient<>, jak AddScoped<>i AddSingleton<>metody działają normalnie.

  2. W przypadku nazwanych usług przejściowych i o określonych zakresach rejestr jest budowany, ObjectFactoryaby w razie potrzeby bardzo szybko aktywować nowe wystąpienia tego typu. Jest to o wiele szybsze niż w przypadku innych podejść i zgodne z tym, jak działa Microsoft.Extensions.DependencyInjection.

Darrell
źródło
2

Moje rozwiązanie za to, co jest warte ... rozważałem przejście na Castle Windsor, ponieważ nie mogę powiedzieć, że podobało mi się którekolwiek z powyższych rozwiązań. Przepraszam!!

public interface IStage<out T> : IStage { }

public interface IStage {
      void DoSomething();
}

Twórz różne wdrożenia

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

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

Rejestracja

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

Konstruktor i użycie instancji ...

public class Whatever
{
   private IStage ClassA { get; }

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

   public void SomeWhateverMethod()
   {
        ClassA.DoSomething();
        .....
   }
trochę
źródło
1

Rozszerzenie rozwiązania @rnrneverdies. Zamiast ToString () można również użyć następujących opcji: 1) Z implementacją wspólnej właściwości, 2) Usługa usług sugerowana przez @Craig Brunetti.

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....
}
vrluckyin
źródło
1

Po przeczytaniu odpowiedzi tutaj i artykułów gdzie indziej udało mi się sprawić, że działał bez żadnych zobowiązań. Gdy masz wiele implementacji tego samego interfejsu, DI doda je do kolekcji, dzięki czemu możliwe będzie pobranie żądanej wersji z kolekcji za pomocą 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();
    }
}
Ciaran Bruen
źródło
Pokonuje cel IoC. Równie dobrze możesz napisać:var serviceA = new ServiceA();
James Curran
2
@JamesCurran nie, jeśli ServiceA ma zależności lub jeśli chcesz przetestować klasę w jednostce.
Jorn.Beyers
0

Chociaż implementacja tego nie oferuje, tutaj jest przykładowy projekt, który pozwala zarejestrować nazwane wystąpienia, a następnie wstrzyknąć INamedServiceFactory do kodu i wyciągnąć wystąpienia według nazwy. W przeciwieństwie do innych rozwiązań fakultatywnych tutaj, możesz zarejestrować wiele instancji tej samej implementacji, ale skonfigurowanych inaczej

https://github.com/macsux/DotNetDINamedInstances

Andrew Stakhov
źródło
0

A co z usługą dla usług?

Gdybyśmy mieli interfejs INamedService (z właściwością .Name), moglibyśmy napisać rozszerzenie IServiceCollection dla .GetService (nazwa ciągu), w którym rozszerzenie wziąłoby ten parametr ciągu i wykonałoby .GetServices () na sobie i za każdym razem zwracane instancja, znajdź instancję, której INamedService.Name pasuje do podanej nazwy.

Lubię to:

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

Dlatego twoja IMyService musi zaimplementować INamedService, ale uzyskasz pożądaną rozdzielczość kluczową, prawda?

Szczerze mówiąc, konieczność posiadania tego interfejsu INamedService wydaje się brzydka, ale jeśli chcesz pójść dalej i uczynić rzeczy bardziej eleganckimi, kod [NamedServiceAttribute („A”)] na implementacji / klasie może znaleźć kod w tym rozszerzenie i działałoby równie dobrze. Aby być jeszcze bardziej uczciwym, Refleksja jest powolna, więc może być optymalizacja, ale szczerze mówiąc, to jest coś, w czym silnik DI powinien był pomóc. Szybkość i prostota wnoszą ogromny wkład w TCO.

Podsumowując, nie ma potrzeby tworzenia jawnej fabryki, ponieważ „znalezienie nazwanej usługi” to koncepcja wielokrotnego użytku, a klasy fabryk nie skalują się jako rozwiązanie. I Func <> wydaje się w porządku, ale blok przełączników jest tak ponury i znowu będziesz pisał Funcs tak często, jak piszemy w Fabrykach. Zacznij od prostego, wielokrotnego użytku, z mniejszym kodem, a jeśli okaże się, że nie możesz tego zrobić dla ciebie, to idź skomplikowany.

Craig Brunetti
źródło
2
Nazywa się to wzorcem lokalizatora usług i zazwyczaj nie jest najlepszą drogą, chyba że absolutnie musisz
Joe Phillips
@JoePhillips Czy masz jakieś uwagi, dlaczego nie jest to dobre rozwiązanie? uwielbiam jego elegancję. Jedynym minusem, jaki mogę wymyślić, jest to, że tworzę instancję wszystkich z nich za każdym razem, gdy ją otrzymujesz.
Peter
2
@Peter Głównym powodem jest to, że bardzo trudno jest z nim pracować. Jeśli przekazujesz obiekt serviceLocator do klasy, wcale nie jest oczywiste, jakich zależności używa ta klasa, ponieważ pobiera je wszystkie od magicznego obiektu „boga”. Wyobraź sobie, że musisz znaleźć referencje typu, który chcesz zmienić. Ta umiejętność w zasadzie znika, gdy dostajesz wszystko przez obiekt lokalizatora usług. Wtrysk konstruktora jest znacznie bardziej przejrzysty i niezawodny
Joe Phillips
Nie wiem. Oczywistość nie jest dla mnie minusem ... ponieważ gdybym chciał śledzić, w jaki sposób moje komponenty wykorzystują swoje zależności, przeprowadziłbym dla nich testy jednostkowe ... testy, które nie tylko odnoszą się do każdej zależności, ale pomagają nam zrozumieć JAK każda zależność jest potrzebna. Jak inaczej będziesz tego świadomy, czytając konstruktory?!?
Craig Brunetti,
0

Natrafiłem na ten sam problem i pracowałem z prostym rozszerzeniem, aby umożliwić usługi nazwane. Możesz go znaleźć tutaj:

Pozwala ci dodać tyle usług (nazwanych), jak chcesz:

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

Biblioteka umożliwia także łatwe wdrożenie „wzorca fabrycznego” w następujący sposób:

    [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
    }

Mam nadzieję, że to pomoże

Subgurim
źródło
0

Utworzyłem własne rozszerzenie w stosunku do IServiceCollectionużywanego WithNamerozszerzenia:

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

ServiceCollectionTypeMapperjest pojedyncza instancja że mapy IService> NameOfService> Implementationgdzie interfejs może mieć wiele wdrożeń o różnych nazwach, pozwala zarejestrować typy niż możemy rozwiązać, gdy maleńki potrzeba i jest inne podejście niż usługi resolve wielu, aby wybrać to, co chcemy.

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

Aby zarejestrować nową usługę:

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

Aby rozwiązać usługę, potrzebujemy takiego rozszerzenia 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));
        }
    }

Po rozwiązaniu:

serviceProvider.GetService<IWithdrawalHandler>(serviceName);

Pamiętaj, że serviceProvider może zostać wstrzyknięty do konstruktora w naszej aplikacji jako IServiceProvider.

Mam nadzieję, że to pomoże.

svladimirrc
źródło