Leaders Logo

Comparação de Transient, Scoped e Singleton: Abordagens Otimizadas para Injeção de Dependência em Csharp

Introdução à Injeção de Dependência

A injeção de dependência (DI) é um padrão de design que promove a separação de preocupações, facilitando a manutenção e a testabilidade de aplicações. No contexto do C#, a DI é amplamente utilizada em frameworks como ASP.NET Core, onde a configuração e o gerenciamento do ciclo de vida dos objetos são fundamentais para o desempenho e a escalabilidade (LARKIN et al., 2024). Neste artigo, abordaremos três dos principais modos de injeção de dependência: Transient, Scoped e Singleton, analisando suas características, vantagens e desvantagens.

O Que é Transient?

O ciclo de vida Transient é uma abordagem onde uma nova instância de um serviço é criada cada vez que ele é solicitado. Isso significa que, sempre que uma classe dependente é instanciada, uma nova instância do serviço é injetada. Essa abordagem é ideal para serviços leves e stateless.

public interface ITransientService 
{
    string GetData();
}

public class TransientService : ITransientService 
{
    public string GetData() 
    {
        return Guid.NewGuid().ToString();
    }
}

// Configuração no Startup.cs
services.AddTransient();

Neste exemplo, cada chamada ao método GetData() resultará em um novo GUID, demonstrando que uma nova instância do TransientService é criada a cada solicitação.

Vantagens e Desvantagens do Transient

As vantagens do ciclo de vida Transient incluem:

  • Isolamento: cada instância é independente, evitando conflitos entre diferentes partes da aplicação.
  • Simples de implementar e entender.

No entanto, existem desvantagens:

  • Alto consumo de recursos se o serviço for pesado, pois novas instâncias são criadas constantemente.
  • Não é adequado para serviços que mantêm estado, pois cada instância será nova e sem histórico.

Entendendo Scoped

O ciclo de vida Scoped é uma abordagem que cria uma instância de serviço por escopo de requisição. Isso significa que, durante a duração de uma única requisição HTTP, a mesma instância do serviço será utilizada. Quando a requisição termina, a instância é descartada. Este padrão é particularmente útil para serviços que precisam compartilhar dados durante uma única requisição (FOWLER, 2023).

public interface IScopedService 
{
    string GetRequestId();
}

public class ScopedService : IScopedService 
{
    private readonly string _requestId;

    public ScopedService() 
    {
        _requestId = Guid.NewGuid().ToString();
    }

    public string GetRequestId() 
    {
        return _requestId;
    }
}

// Configuração no Startup.cs
services.AddScoped();

Neste exemplo, todas as classes que injetarem IScopedService durante a mesma requisição receberão a mesma instância, permitindo compartilhar dados entre componentes.

Vantagens e Desvantagens do Scoped

As vantagens do ciclo de vida Scoped incluem:

  • Eficiência no uso de recursos, já que a mesma instância é reutilizada durante uma requisição.
  • Ideal para serviços que precisam manter estado durante a requisição, como repositórios de dados.

As desvantagens incluem:

  • Se utilizado fora de um contexto de requisição, pode levar a comportamentos inesperados.
  • Não é adequado para serviços que precisam ser compartilhados entre requisições.

Explorando Singleton

No ciclo de vida Singleton, uma única instância do serviço é criada e compartilhada em toda a aplicação. Essa instância é criada na primeira vez que o serviço é solicitado e, em seguida, reutilizada em todas as solicitações subsequentes. O padrão Singleton é ideal para serviços que são caros para criar ou que mantêm estado que deve ser compartilhado entre todas as partes da aplicação (ARUNACHALAM, 2024).

public interface ISingletonService 
{
    string GetInstanceId();
}

public class SingletonService : ISingletonService 
{
    private readonly string _instanceId;

    public SingletonService() 
    {
        _instanceId = Guid.NewGuid().ToString();
    }

    public string GetInstanceId() 
    {
        return _instanceId;
    }
}

// Configuração no Startup.cs
services.AddSingleton();

Com este exemplo, todas as partes da aplicação que dependem de ISingletonService compartilharão a mesma instância, resultando no mesmo instanceId.

Vantagens e Desvantagens do Singleton

As vantagens do ciclo de vida Singleton incluem:

  • Consumo de recursos reduzido, pois apenas uma instância é criada e reutilizada.
  • Ideal para serviços que mantêm estado global ou que são custosos para instanciar.

As desvantagens incluem:

  • Pode causar problemas de concorrência se não for adequadamente gerenciado, especialmente em aplicativos multithread.
  • Potencial para se tornar um ponto de acoplamento, dificultando testes e manutenção.

Quando Utilizar Cada Abordagem

Escolher entre Transient, Scoped e Singleton depende das necessidades específicas do seu aplicativo:

  • Utilize Transient para serviços leves e stateless que não precisam compartilhar estado.
  • Utilize Scoped para serviços que precisam manter estado durante uma única requisição e que são utilizados em componentes da mesma requisição.
  • Utilize Singleton para serviços que são caros para instanciar ou que precisam manter um estado compartilhado em toda a aplicação.

Exemplo Prático em um Aplicativo ASP.NET Core

Vamos considerar um cenário de um aplicativo ASP.NET Core que usa todos os três modos de injeção de dependência:

public class HomeController : Controller 
{
    private readonly ITransientService _transientService;
    private readonly IScopedService _scopedService;
    private readonly ISingletonService _singletonService;

    public HomeController(ITransientService transientService, IScopedService scopedService, ISingletonService singletonService) 
    {
        _transientService = transientService;
        _scopedService = scopedService;
        _singletonService = singletonService;
    }

    public IActionResult Index() 
    {
        ViewBag.TransientId = _transientService.GetData();
        ViewBag.ScopedId = _scopedService.GetRequestId();
        ViewBag.SingletonId = _singletonService.GetInstanceId();
        return View();
    }
}

// Configuração no Startup.cs
public void ConfigureServices(IServiceCollection services) 
{
    services.AddTransient();
    services.AddScoped();
    services.AddSingleton();
}

Neste exemplo, ao acessar a página inicial, cada método de serviço retornará um identificador único para Transient, um identificador compartilhado para Scoped dentro da requisição e um identificador constante para Singleton.

Escolhendo a melhor abordagem

A escolha entre Transient, Scoped e Singleton é crucial para a arquitetura de aplicações em C#. Cada abordagem tem suas vantagens e desvantagens, e a escolha correta pode melhorar significativamente a eficiência, a escalabilidade e a testabilidade do seu código. Compreender o ciclo de vida dos serviços e como eles interagem uns com os outros é essencial para a construção de aplicações robustas e eficientes.

Exemplos Adicionais e Casos de Uso

Além dos exemplos já discutidos, é útil explorar mais detalhadamente os cenários em que cada abordagem pode ser aplicada. Vamos considerar alguns casos de uso mais complexos, onde a escolha do ciclo de vida de um serviço se torna crucial.

Exemplo de Transient em Serviços de Logging

Serviços de logging que não mantêm estado e são leves são bons candidatos para a abordagem Transient. Cada vez que um logger é solicitado, uma nova instância pode ser criada, garantindo que cada operação de logging seja independente e não interfira com as operações de outras partes da aplicação.

public interface ILoggingService 
{
    void Log(string message);
}

public class LoggingService : ILoggingService 
{
    public void Log(string message) 
    {
        Console.WriteLine($"Log: {message} at {DateTime.Now}");
    }
}

// Configuração no Startup.cs
services.AddTransient();

Exemplo de Scoped em Repositórios de Dados

Em aplicações que interagem com um banco de dados, é comum usar serviços Scoped para repositórios. Durante a vida útil de uma requisição, você pode querer garantir que todas as operações de banco de dados sejam executadas no mesmo contexto de unidade de trabalho (LARKIN et al., 2024).

public interface IDataRepository 
{
    void Add(DataEntity entity);
}

public class DataRepository : IDataRepository 
{
    private readonly DataContext _context;

    public DataRepository(DataContext context) 
    {
        _context = context;
    }

    public void Add(DataEntity entity) 
    {
        _context.Entities.Add(entity);
        _context.SaveChanges();
    }
}

// Configuração no Startup.cs
services.AddScoped();

Exemplo de Singleton em Configurações de Aplicação

Configurações que são lidas uma vez e usadas em toda a aplicação são ideais para a abordagem Singleton. Isso evita a sobrecarga de ler as mesmas configurações repetidamente e garante que todos os componentes da aplicação compartilhem as mesmas configurações (ARUNACHALAM, 2024).

public interface IAppSettings 
{
    string GetSetting(string key);
}

public class AppSettings : IAppSettings 
{
    private readonly IConfiguration _configuration;

    public AppSettings(IConfiguration configuration) 
    {
        _configuration = configuration;
    }

    public string GetSetting(string key) 
    {
        return _configuration[key];
    }
}

// Configuração no Startup.cs
services.AddSingleton();

Testes Unitários e Injeção de Dependência

Um dos principais benefícios da injeção de dependência é a facilidade de testar componentes isoladamente. Ao usar DI, você pode facilmente injetar mocks ou stubs de serviços em seus testes unitários, permitindo que você verifique o comportamento de suas classes de forma independente (FOWLER, 2023).

public class HomeControllerTests 
{
    [Fact]
    public void Index_ReturnsViewWithCorrectData() 
    {
        // Arrange
        var transientServiceMock = new Mock();
        var scopedServiceMock = new Mock();
        var singletonServiceMock = new Mock();

        transientServiceMock.Setup(ts => ts.GetData()).Returns("TransientData");
        scopedServiceMock.Setup(ss => ss.GetRequestId()).Returns("ScopedData");
        singletonServiceMock.Setup(ss => ss.GetInstanceId()).Returns("SingletonData");

        var controller = new HomeController(transientServiceMock.Object, scopedServiceMock.Object, singletonServiceMock.Object);

        // Act
        var result = controller.Index() as ViewResult;

        // Assert
        Assert.NotNull(result);
        Assert.Equal("TransientData", result.ViewBag.TransientId);
        Assert.Equal("ScopedData", result.ViewBag.ScopedId);
        Assert.Equal("SingletonData", result.ViewBag.SingletonId);
    }
}

Conclusão

A injeção de dependência é uma prática essencial para o desenvolvimento de aplicações C# escaláveis e testáveis. Ao entender as diferenças entre Transient, Scoped e Singleton, você pode tomar decisões informadas sobre como gerenciar seus serviços, melhorando a eficiência e a clareza do seu código. Ao aplicar essas práticas em sua arquitetura de software, você não só otimiza a performance da aplicação, mas também facilita a manutenção e a evolução do seu sistema ao longo do tempo.

Referências

  • LARKIN, Kirk; SMITH, Steve; DAHLER, Brandon. Dependency Injection in .NET Core. Disponível em: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-9.0. Acesso em: 16 dez. 2024.
  • FOWLER, Martin. Artigo de Martin Fowler sobre Injeção de Dependência. Disponível em: https://martinfowler.com/articles/injection.html. Acesso em: 15 dez. 2024.
  • ARUNACHALAM, Nataraj. C# Corner sobre Injeção de Dependência em ASP.NET Core. Disponível em: https://www.c-sharpcorner.com/article/dependency-injection-in-asp-net-core/. Acesso em: 15 dez. 2024.
Sobre o autor