c# - register - tag helper asp net core




Como registrar várias implementações da mesma interface no Asp.Net Core? (11)

A maioria das respostas aqui viola o princípio de responsabilidade única (uma classe de serviço não deve resolver as dependências) e / ou usa o antipadrão do localizador de serviços.

Outra opção para evitar esses problemas é:

  • use um parâmetro de tipo genérico adicional na interface ou uma nova interface que implemente a interface não genérica,
  • implementar uma classe de adaptador / interceptador para adicionar o tipo de marcador e, em seguida,
  • use o tipo genérico como "nome"

Escrevi um artigo com mais detalhes: Injeção de Dependências no .NET: Uma maneira de solucionar os registros nomeados ausentes

Eu tenho serviços derivados da mesma interface

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

Normalmente, outros contêineres de COI como o Unity permitem registrar implementações concretas por alguma Key que as distingue.

No Asp.Net Core, como faço para registrar esses serviços e resolvê-los em tempo de execução com base em alguma chave?

Não vejo que o método Add Service use o parâmetro key ou name que normalmente é usado para distinguir a implementação concreta.

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


    public MyController:Controller
    {
       public void DoSomeThing(string key)
       { 
          // How do get service based on key
       }
    }

O padrão de fábrica é a única opção aqui?

Update1
Analisei o artigo here que mostra como usar o padrão de fábrica para obter instâncias de serviço quando temos várias implementações de concreção. No entanto, ainda não está solução completa. Quando chamo o método _serviceProvider.GetService() , não consigo injetar dados no construtor. Por exemplo, considere este exemplo

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

Como _serviceProvider.GetService() injeta uma seqüência de conexão apropriada? No Unity ou em qualquer outro COI, podemos fazer isso no momento do registro do tipo. IOption entanto, posso usar a IOption que exigirá que eu injete todas as configurações. Não posso injetar uma IOption conexão específica no serviço.

Observe também que estou tentando evitar o uso de outros contêineres (incluindo o Unity), porque também tenho que registrar todo o resto (por exemplo, controladores) no novo contêiner.

Também o uso do padrão de fábrica para criar uma instância de serviço é contra o DIP, pois a fábrica aumenta o número de dependências que um cliente é forçado a depender dos detalhes aqui

Então eu acho que o DI padrão no núcleo do ASP.NET está faltando 2 coisas
1> Registrar instâncias usando a chave
2> Injete dados estáticos no construtor durante o registro


Embora a implementação pronta para uso não a ofereça, aqui está um projeto de amostra que permite registrar instâncias nomeadas e injetar INamedServiceFactory em seu código e extrair instâncias por nome. Ao contrário de outras soluções faciais aqui, ele permitirá que você registre várias instâncias da mesma implementação, mas configuradas de maneira diferente

https://github.com/macsux/DotNetDINamedInstances


Que tal um serviço para serviços?

Se tivéssemos uma interface INamedService (com propriedade .Name), poderíamos escrever uma extensão IServiceCollection para .GetService (nome da string), onde a extensão usaria esse parâmetro e faria um .GetServices () sozinho e em cada retorno instância, localize a instância cujo INamedService.Name corresponda ao nome fornecido.

Como isso:

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

Portanto, seu IMyService deve implementar INamedService, mas você obterá a resolução baseada em chave desejada, certo?

Para ser justo, ter que ter essa interface INamedService parece feio, mas se você quiser ir além e tornar as coisas mais elegantes, um [NamedServiceAttribute ("A")] na implementação / classe pode ser encontrado pelo código neste extensão, e iria funcionar tão bem. Para ser ainda mais justo, o Reflection é lento, portanto uma otimização pode estar em ordem, mas honestamente isso é algo que o mecanismo de DI deveria estar ajudando. Velocidade e simplicidade são os principais contribuintes para o TCO.

Em suma, não há necessidade de uma fábrica explícita, porque "encontrar um serviço nomeado" é um conceito tão reutilizável e as classes de fábrica não são dimensionadas como uma solução. E um Func <> parece bom, mas um bloco de comutação é tão ruim e, novamente, você estará escrevendo Funcs tão frequentemente quanto escrevendo Fábricas. Comece simples, reutilizável, com menos código e, se isso não resultar em você, vá para o complexo.


Aparentemente, você pode simplesmente injetar IEnumerable na sua interface de serviço! E, em seguida, encontre a instância que você deseja usar o LINQ.

Meu exemplo é para o serviço AWS SNS, mas você pode fazer o mesmo para qualquer serviço injetado.

Comece

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

SNSConfig

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

appsettings.json

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

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

     .........
}

Agora você pode obter o serviço SNS para a região que deseja em seu serviço ou controlador personalizado

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

Eu enfrentei o mesmo problema e quero compartilhar como o resolvi e por quê.

Como você mencionou, existem dois problemas. O primeiro:

No Asp.Net Core, como faço para registrar esses serviços e resolvê-los em tempo de execução com base em alguma chave?

Então, quais opções temos? O pessoal sugere dois:

  • Use uma fábrica personalizada (como _myFactory.GetServiceByKey(key) )

  • Use outro mecanismo de DI (como _unityContainer.Resolve<IService>(key) )

O padrão de fábrica é a única opção aqui?

De fato, ambas as opções são fábricas, porque cada container IoC também é uma fábrica (embora altamente configurável e complicada). E parece-me que outras opções também são variações do padrão de fábrica.

Então, qual opção é melhor então? Concordo aqui com a @Sock, que sugeriu o uso de fábrica personalizada, e é por isso.

Primeiro, sempre tento evitar adicionar novas dependências quando elas não são realmente necessárias. Então, eu concordo com você neste ponto. Além disso, o uso de duas estruturas de DI é pior do que criar abstração de fábrica personalizada. No segundo caso, você precisa adicionar uma nova dependência de pacote (como o Unity), mas dependendo de uma nova interface de fábrica é menos ruim aqui. A principal idéia do ASP.NET Core DI, acredito, é a simplicidade. Mantém um conjunto mínimo de recursos seguindo o princípio do KISS . Se você precisar de algum recurso extra, faça bricolage ou use um Plungin correspondente que implemente o recurso desejado (Princípio Aberto Fechado).

Em segundo lugar, geralmente precisamos injetar muitas dependências nomeadas para um único serviço. No caso do Unity, você pode precisar especificar nomes para os parâmetros do construtor (usando InjectionConstructor ). Esse registro usa reflexão e alguma lógica inteligente para adivinhar argumentos para o construtor. Isso também pode levar a erros de tempo de execução se o registro não corresponder aos argumentos do construtor. Por outro lado, ao usar sua própria fábrica, você tem controle total de como fornecer os parâmetros do construtor. É mais legível e é resolvido em tempo de compilação. Princípio do KISS novamente.

O segundo problema:

Como _serviceProvider.GetService () injeta uma seqüência de conexão apropriada?

Primeiro, concordo com você que, dependendo de coisas novas como IOptions (e, portanto, do pacote Microsoft.Extensions.Options.ConfigurationExtensions ) não é uma boa ideia. Vi algumas discussões sobre IOptions em que havia opiniões diferentes sobre seu benefício. Novamente, tento evitar adicionar novas dependências quando elas não são realmente necessárias. É realmente necessário? Eu acho que não. Caso contrário, cada implementação teria que depender dela, sem necessidade clara dessa implementação (para mim, parece uma violação do ISP, onde também concordo com você). Isso também é verdade quanto à dependência da fábrica, mas, neste caso, pode ser evitado.

O ASP.NET Core DI fornece uma sobrecarga muito agradável para esse propósito:

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

Eu simplesmente injeto um IEnumerable

ConfigureServices em Startup.cs

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

Pasta Serviços

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

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

Extensions.cs

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

Minha solução para o que vale a pena ... considerou mudar para o Castelo Windsor, pois não posso dizer que gostei de nenhuma das soluções acima. Desculpa!!

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

Crie suas várias implementações

public interface IStage<out T> : IStage { }

public interface IStage {
      void DoSomething();
}

Cadastro

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

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

Uso do construtor e da instância ...

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

Não é suportado pelo Microsoft.Extensions.DependencyInjection .

Mas você pode conectar outro mecanismo de injeção de dependência, como o StructureMap Veja a página inicial e o projeto GitHub .

Não é nada difícil:

  1. Adicione uma dependência ao StructureMap no seu project.json :

    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>();
    }
  2. Injete-o no pipeline do ASP.NET dentro do ConfigureServices e registre suas classes (consulte a documentação)

    public class HomeController : Controller
    {
        public HomeController(IContainer injectedContainer)
        {
            var myPet = injectedContainer.GetInstance<IPet>("B");
            string name = myPet.Name; // Returns "CatB"
  3. Em seguida, para obter uma instância nomeada, você precisará solicitar o IContainer

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

É isso aí.

Para o exemplo construir, você precisa

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

Outra opção é usar o método de extensão GetServices de Microsoft.Extensions.DependencyInjection .

Registre seus serviços como:

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

Então resolva com um pouco de Linq:

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

ou

"Structuremap.Microsoft.DependencyInjection" : "1.0.1",

(supondo que IService tenha uma propriedade de sequência chamada "Nome")

Certifique-se de using Microsoft.Extensions.DependencyInjection;

Atualizar

Fonte AspNet 2.1: GetServices


Um pouco atrasado para esta festa, mas aqui está a minha solução: ...

Startup.cs ou Program.cs se o Manipulador Genérico ...

public class Whatever
{
   private IStage ClassA { get; }

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

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

IMyInterface da configuração da interface T

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

Implementações concretas do IMyInterface of T

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

Felizmente, se houver algum problema em fazê-lo dessa maneira, alguém indicará por que essa é a maneira errada de fazer isso.






coreclr