C# -  Múltiplas implementações de uma interface


Neste artigo vamos discutir como fazer múltiplas implementações de uma interface usando o container de injeção de dependência nativo da plataforma .NET.

O contêiner IoC (inversão de controle) embutido na plataforma .NET e muito usado em aplicações ASP.NET Core pode não ter tantos recursos como outros contêineres IoC como o AutoFac ou o Unity, mas ele pode ser usado para atender aos seus requisitos na maioria dos casos.

No entanto, o contêiner IoC integrado, apresenta o seguinte problema: Ele não nos permite registrar vários serviços e, em seguida, recuperar uma instância de um serviço específica em tempo de execução.

Existem alguns contêineres IoC que permitem registrar tipos concretos usando uma chave exclusiva que distingue as instâncias desses tipos. No entanto, o contêiner IoC integrado da ASP.NET Core não tem suporte para isso.

Portanto, registrar serviços que possuem uma interface comum e resolvê-los em tempo de execução não é simples.

Vamos entender isso com um exemplo.



Apresentando o problema

Vamos supor que temos em um projeto ASP .NET Core MVC  seguinte interface definida:

public interface IRegistraLog
{

    public bool Registrar(string texto);

}

Esta interface é implementada por 3 classes :

1- ArquivoRegistraLog

using System;

namespace AspnMultiplasInterfaces.Logs
{
    public class ArquivoRegistraLog : IRegistraLog
    {
        public bool Registrar(string texto)
        {
            throw new NotImplementedException();
        }
    }
}

2- DatabaseRegistraLog

using System;

namespace AspnMultiplasInterfaces.Logs
{
    public class DatabaseRegistraLog : IRegistraLog
    {
        public bool Registrar(string texto)
        {
            throw new NotImplementedException();
        }
    }
}

3- EventoRegistraLog

using System;

namespace AspnMultiplasInterfaces.Logs
{
    public class EventoRegistraLog : IRegistraLog
    {
        public bool Registrar(string texto)
        {
            throw new NotImplementedException();
        }
    }
}

A classe ArquivoRegistraLog é usada para registrar dados em um arquivo, a classe DatabaseRegistraLog é usada para registrar dados em um banco de dados e a classe EventoRegistraLog é usada para registrar dados no log de eventos para que os logs possam ser visualizados usando a ferramenta Event Viewer.

Agora, suponha que você tenha adicionado instâncias dessas classes como serviços com escopo no método ConfigureServices da classe Startup, conforme mostrado abaixo :

public void ConfigureServices(IServiceCollection services)
 {
            services.AddScoped<IRegistraLog, ArquivoRegistraLog>();
            services.AddScoped<IRegistraLog, DatabaseRegistraLog>();
            services.AddScoped<IRegistraLog, EventoRegistraLog>();

            services.AddControllersWithViews();
 }

Vamos agora fazer a injeção de dependência desses serviços no controlador HomeController e verificar qual o comportamento:

 public class HomeController : Controller
 {
     public HomeController(IRegistraLog arquivoLog, IRegistraLog dbLog,
         IRegistraLog eventoLog)
    {
            var arq = arquivoLog;
            var db = dbLog;
            var evt = eventoLog;
    }
   ...
}

Estamos injetando os 3 serviços no construtor e a seguir obtendo a instância de cada serviço.

Colocando um breakpoint e executando o projeto, ao inspecionar o valor das variáveis arq, db e evt iremos notar que em todas o valor atribuído será a instância de EventoRegistraLog :

Isso esta acontecendo porque o Contâiner DI esta injetando apenas objetos EventoRegistraLog pois esta foi a última implementação registrada no método ConfigureServices.

Apresentando o solução

Vemos então esta limitação do Contêiner DI nativo da plataforma .NET.

Como superamos essa limitação do contêiner IoC integrado no ASP.NET Core ?

Uma forma de contornar essa limitação é usar uma coleção de serviços IEnumerable para registrar os serviços e um delegate para recuperar uma instância de serviço específica.

public class HomeController : Controller
{
        public HomeController(IEnumerable<IRegistraLog> registraLogs)
        {
            foreach (var registraLog in registraLogs)
            {
                var tipoLog = registraLog.GetType();
            }
        }
 ... 
}

No código acima temos que as 3 instâncias de cada serviço será injetada no construtor e podemos verificar o tipo do objeto para decidir qual implementação do serviço usar.

Usando um delegate para retornar uma instância específica

Podemos retornar uma instância específica definindo uma enumeração com os tipos de serviços:

public enum TipoServico
{
        ArquivoLog,
        DatabaseLog,
        EventoLog
}

A seguir definimos um delegate ServiceResolver e no método ConfigureServices() definimos o registro de cada serviço usando o tempo de serviço AddScoped, e , a seguir podemos retornar a instância de cada serviço dependendo do tipo de serviço selecionando em tempo de execução:

  public class Startup
    {
        private delegate IRegistraLog ServiceResolver(TipoServico tipoServico);

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ArquivoRegistraLog>();
            services.AddScoped<DatabaseRegistraLog>();
            services.AddScoped<EventoRegistraLog>();

            services.AddTransient<ServiceResolver>(serviceProvider => nomeServico =>
            {
                switch (nomeServico)
                {
                    case TipoServico.ArquivoLog:
                        return serviceProvider.GetService<ArquivoRegistraLog>();
                    case TipoServico.DatabaseLog:
                        return serviceProvider.GetService<DatabaseRegistraLog>();
                    case TipoServico.EventoLog:
                        return serviceProvider.GetService<EventoRegistraLog>();
                    default:
                        return null;
                }
            });

            services.AddControllersWithViews();
        }
   ...
}

Ao final usamos a injeção de dependência para resolver o tipo de serviço :

public class HomeController : Controller
 {
        public HomeController(Func<TipoServico, IRegistraLog> serviceResolver)
        {
            var service = serviceResolver(TipoServico.ArquivoLog);
        }

...
}

Outra solução possível é usar um parâmetro de tipo genérico na interface.

Pegue o código do projeto aqui : AspnMultiplasInterfaces.zip

"E eu, quando o vi, caí a seus pés como morto; e ele pôs sobre mim a sua destra, dizendo-me: Não temas; Eu sou o primeiro e o último; E o que vivo e fui morto, mas eis aqui estou vivo para todo o sempre. Amém. E tenho as chaves da morte e do inferno."
Apocalipse 1:17,18

Referências:


José Carlos Macoratti