2016-08-26 2 views
62

У меня есть услуги, которые являются производными от того же интерфейсаКак зарегистрировать несколько реализаций одного и того же интерфейса в Asp.Net Core?

public interface IService 
{ 
} 

public class ServiceA : IService 
{ 
} 

public class ServiceB : IService 
{  
} 

public class ServiceC : IService 
{  
} 

Обычно другие контейнеры МОК как Unity позволяют регистрировать конкретные реализации некоторой Key, что отличает их.

В Asp.Net Core как я могу зарегистрировать эти службы и решить их во время выполнения на основе некоторого ключа?

Я не вижу никакого метода Add. Метод службы принимает key или name параметр, который обычно используется для различения конкретной реализации.

Есть ли заводская модель только здесь?

Update1
Я пошел, хотя в статье here, который показывает, как использовать фабричный шаблон, чтобы получить экземпляры служб, когда мы имеем несколько concreate реализации. Однако это еще не полное решение. когда я вызываю метод _serviceProvider.GetService(), я не могу вводить данные в конструктор. Например, рассмотрим следующий пример

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

Как _serviceProvider.GetService() вводить соответствующую строку соединения? В Unity или любом другом МОК мы можем сделать это во время регистрации типа. Я могу использовать IOption, однако мне потребуется ввести все настройки, я не могу вставить конкретную строку подключения в службу.

Также обратите внимание, что я стараюсь избегать использования других контейнеров (включая Unity), потому что тогда мне нужно зарегистрировать все остальное (например, контроллеры) с новым контейнером.

Кроме того, используя заводскую шаблон для создания экземпляра службы против DIP, как завод увеличивает количество зависимостей клиент вынужден зависеть от details here

Так что я думаю, что по умолчанию DI в ядре ASP.NET отсутствует 2 вещи
1> Регистрировать экземпляры с помощью ключа
2> Вставить статические данные в конструктор во время регистрации

+2

Возможная Дубликат [Dependency инъекции разрешающего по имени] (http://stackoverflow.com/questions/39072001/dependency-injection -разъем по имени) –

+1

Наконец-то есть [расширение в nuget] (https://www.nuget.org/packages/Neleus.DependencyInjection.Extensions) для регистрации на основе имени, надеюсь, что это может помочь – neleus

+0

Привет, извините для моего глупого вопроса, но я о новинках с Microso ft.Extensions.DependencyInjection ... Считаете ли вы, что создаете 3 пустых интерфейса, которые расширяют Iservice, такие как «открытый интерфейс IServiceA: IService», а не «public class ServiceA: IServiceA» ... может быть хорошим вариантом? –

ответ

35

Я сделал простой обходной путь, используя Func, когда я оказался в этой ситуации.

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

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

и использовать его из любого класса, зарегистрированного DI, как:

public class Consumer 
{ 
    private readonly Func<string, IService> _serviceAccessor; 

    public Consumer(Func<string, IService> serviceAccessor) 
    { 
     _serviceAccessor = serviceAccesor; 
    } 

    public void UseServiceA() 
    { 
     //use _serviceAccessor field to resolve desired type 
     _serviceAccessor("A").DoIServiceOperation(); 
    } 
} 

UPDATE

Имейте в виду, что в этом примере ключ для разрешения является строкой, ради простоты и потому, что ОП запрашивал этот случай в частности.

Но вы можете использовать любой настраиваемый тип разрешения в качестве ключа, так как обычно вы не хотите, чтобы гигантский переключатель n-case гнило ваш код. Зависит от того, как масштабируется ваше приложение.

+0

Как вы извлекаете одну из сервисов из строки из другого класса? –

+1

@MatthewStevenMonkan обновил мой ответ на примере –

+1

Использование фабричного шаблона, подобного этому, является лучшим способом. Спасибо, что поделился! –

5

Вы правы: встроенный контейнер ASP.NET Core не имеет концепции регистрации нескольких сервисов, а затем получения определенного, как вы полагаете, завод является единственным в этом случае.

В качестве альтернативы вы можете переключиться на сторонний контейнер, такой как Unity или StructureMap, который предоставляет необходимое вам решение (задокументировано здесь: https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing-the-default-services-container).

11

Не поддерживается Microsoft.Extensions.DependencyInjection.

Но вы можете подключить другой механизм впрыска зависимостей, например StructureMapSee it's Home page и это GitHub Project.

Это не сложно:

  1. Добавить зависимость к StructureMap в вашем project.json:

    "Structuremap.Microsoft.DependencyInjection" : "1.0.1", 
    
  2. Вводят его в магистральный трубопровод ASP.NET внутри ConfigureServices и зарегистрировать классы (see docs)

    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. Затем, чтобы получить именной экземпляр, вам нужно будет просить IContainer

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

Вот и все.

Для примера, чтобы построить, вам нужно

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

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

     public string Name {get; set; } 
    } 
+0

Я пробовал этот подход, но я получаю ошибки времени выполнения на моем контроллере, потому что IContainer не найден в планах сборки. Должен ли я что-то требовать, чтобы IContainer автоматически вводился? – mohrtan

+0

BTW, я использую StructureMap.Micorosoft.DependencyInjection 1.3.0. – mohrtan

+0

Вы возвращаете новый контейнер в ConfigureServices? –

8

Я сталкивался с такой же вопрос и хочу поделиться тем, как я ее решил и почему.

Как вы упомянули, есть две проблемы. Первое:

В Asp.Net Ядра, как зарегистрировать эти услуги и решить ее в выполнения на основе какой-то ключ?

Итак, какие у нас есть варианты? Люди предполагают два:

  • Используйте выборочный завод (как _myFactory.GetServiceByKey(key))

  • Используйте другой DI двигатель (как _unityContainer.Resolve<IService>(key))

ли шаблон Factory единственным вариантом здесь?

Фактически оба варианта - это заводы, потому что каждый контейнер IoC также является заводом (очень настраиваемый и сложный, хотя). И мне кажется, что другие варианты также являются вариантами шаблона Factory.

Итак, какой вариант лучше? Здесь я согласен с @Sock, который предложил использовать пользовательскую фабрику, и именно поэтому.

Во-первых, я всегда стараюсь избегать добавления новых зависимостей, когда они действительно не нужны. Поэтому я согласен с вами в этом вопросе. Более того, использование двух каркасов DI хуже, чем создание произвольной фабричной абстракции. Во втором случае вам нужно добавить новую зависимость пакета (например, Unity), но в зависимости от нового интерфейса фабрики здесь менее зло. Я считаю, что основная идея ASP.NET Core DI - это простота. Он поддерживает минимальный набор функций после KISS principle. Если вам нужна дополнительная функция, сделайте DIY или используйте соответствующий Plungin, который реализует желаемую функцию (принцип открытого закрывания).

Во-вторых, часто нам нужно вводить многие именованные зависимости для одиночной службы.В случае Unity вам может потребоваться указать имена для параметров конструктора (используя InjectionConstructor). Эта регистрация использует отражение и некоторые умные логики, чтобы угадать аргументы для конструктора. Это также может привести к ошибкам во время выполнения, если регистрация не соответствует аргументам конструктора. С другой стороны, при использовании вашей собственной фабрики вы полностью контролируете, как обеспечить параметры конструктора. Это более читаемо и разрешено во время компиляции. KISS principle еще раз.

Вторая проблема:

Как _serviceProvider.GetService() вводят соответствующее соединение строку?

Во-первых, я согласен с вами, что в зависимости от новых вещей, как IOptions (и, следовательно, на упаковке Microsoft.Extensions.Options.ConfigurationExtensions) не является хорошей идеей. Я видел некоторые дискуссии о IOptions, где были разные мнения о его преимуществах. Опять же, я стараюсь избегать добавления новых зависимостей, когда они действительно не нужны. Это действительно необходимо? Я думаю нет. В противном случае каждая реализация должна была бы зависеть от нее без какой-либо явной необходимости в этой реализации (для меня это выглядит как нарушение ISP, где я тоже согласен с вами). Это также верно в зависимости от завода, но в этом случае можно избежать .

ASP.NET Ядро DI обеспечивает очень хорошую перегрузку для этой цели:

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

Привет, извините за мой глупый вопрос, но я про новый с Microsoft.Extensions.DependencyInjection ... вы думаете, что создаете 3 интерфейса, которые расширяют Iservice как «открытый интерфейс IServiceA: IService», а не «public class ServiceA: IServiceA «... может быть хорошим вариантом? –

+0

Использование другого механизма DI не является злым даже в глазах Microsoft. Именно поэтому они создали расширяемую инфраструктуру DI. Попытка добиться того, что они уже сделали, - это изобретать колесо. Autofac, Unity, StructureMap, все это зрелые рамки DI, и я определенно использовал бы один из них. Но, конечно, я бы использовал небольшой слой абстракции, чтобы свести к минимуму мои прямые зависимости к любому из них. –

+1

@ emiliano-magliocca В общем, вы не должны зависеть от интерфейсов, которые вы не используете (ISP), 'IServiceA' в вашем случае. Поскольку вы используете методы только из 'IService', вы должны иметь зависимость от' IService'. – neleus

17

Другой вариант заключается в использовании метода расширения GetServices от Microsoft.Extensions.DependencyInjection.

Зарегистрируйте услуги, как:

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

Затем решить с немного Linq:

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

или

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

(при условии, что IService имеет строковое свойство «Name ")

Удостоверьтесь, что у вас есть using Microsoft.Extensions.DependencyInjection;

+0

В вашем коде, если я разрешил только один сервис, а не службы, какой из них будет возвращен? Услуга, которую я зарегистрировал первым или поздно? –

+2

Не уверен, но я думаю, что он не детерминирован. Любые результаты, которые вы получаете сегодня, могут измениться завтра, это не очень хорошая практика. –

+0

Довольно приятное решение. Должно быть на вершине. –

1

Очевидно, вы можете просто ввести IEnumerable своего сервисного интерфейса! И затем найдите экземпляр, который вы хотите использовать LINQ.

Мой пример для службы AWS SNS, но вы можете сделать то же самое для любой внедренной службы.

Startup

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 завод

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

Теперь вы можете получить услугу SNS для региона, который вы хотите в вашем таможенной службы или контроллер

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

    ......... 
} 
0

я сделал что-то подобное некоторое время назад, и я думаю, что реализация была хорошая.

Давайте предположим, что вы должны обрабатывать входящие сообщения, каждое сообщение имеет тип, поэтому общий подход здесь это использовать переключатель и сделать что-то вроде этого:

@Override 
public void messageArrived(String type, Message message) throws Exception { 
    switch(type) { 
     case "A": do Something with message; break; 
     case "B": do Something else with message; break; 
     ... 
    } 
} 

Мы можем избежать этого кода переключателя, а также мы можем избежать необходимости изменять этот код каждый раз, когда добавляется новый тип, следуя этой схеме:

@Override 
public void messageArrived(String type, Message message) throws Exception { 
    messageHandler.getMessageHandler(type).handle(message); 
} 

Это хорошо да? Итак, как мы можем добиться чего-то подобного?

Сначала давайте определим аннотацию, а также интерфейс

@Retention(RUNTIME) 
@Target({TYPE}) 
public @interface MessageHandler { 
    String value() 
} 

public interface Handler { 
    void handle(MqttMessage message); 
} 

и теперь давайте использовать эту аннотацию в классе:

@MessageHandler("myTopic") 
public class MyTopicHandler implements Handler { 

    @override 
    public void handle(MqttMessage message) { 
     // do something with the message 
    } 
} 

Последняя часть это написать класс MessageHandler, в этом классе вам просто нужно сохранить на карте все экземпляры ваших обработчиков, ключом карты будет имя темы, а также значение и экземпляр класса.В основном вы getMessageHandler метод будет выглядеть примерно так:

public Handler getMessageHandler(String topic) { 
    if (handlerMap == null) { 
     loadClassesFromClassPath(); // This method should load from classpath all the classes with the annotation MessageHandler and build the handlerMap 
    } 
    return handlerMap.get(topic); 
} 

Хорошая вещь с этим решением, это то, что, если вам нужно добавить новый обработчик для нового сообщения, вам просто нужно написать новый класс, добавьте аннотацию класс, и все. Остальная часть кода остается той же, поскольку она является общей и использует рефлексию для загрузки и создания экземпляров.

Надеюсь, вам это поможет.

0

Подход фабрики, безусловно, жизнеспособен. Другой подход заключается в использовании наследования для создания отдельных интерфейсов, которые наследуют от IService, реализации унаследованных интерфейсов в реализациях IService и регистрации унаследованных интерфейсов, а не базы. Является ли добавление иерархии наследования или фабрик «правильным» шаблоном, все зависит от того, с кем вы говорите. Мне часто приходится использовать этот шаблон при работе с несколькими поставщиками баз данных в том же приложении, которое использует общий тип, например IRepository<T>, в качестве основы для доступа к данным.

Пример интерфейсов и реализаций:

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.Register<IServiceA, ServiceA>(); 
container.Register<IServiceB, ServiceB>(); 
container.Register<IServiceC, ServiceC>(); 

 Смежные вопросы

  • Нет связанных вопросов^_^