Development

Real-World Examples for Service Lifetimes in .NET Core

Understanding service lifetimes in .NET Core is essential for managing object lifecycles, memory, and performance. Let’s go over the three primary service lifetimes — Singleton, Scoped, and Transient — and look at some practical, real-world examples for each.

1. Singleton: A Single Instance Throughout the Application’s Lifetime

A Singleton service is created once and shared across all requests. It’s best for services that hold state or perform tasks that don’t change per request.

Example Use Cases:

  • Configuration Management: A service that loads application configurations at startup and makes them available throughout the application.
C#
public class ConfigurationService
{
    private readonly IConfiguration _configuration;
    public ConfigurationService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string GetSetting(string key) => _configuration[key];
}

// Registering as a singleton
services.AddSingleton<ConfigurationService>();

Caching: An in-memory cache service that stores frequently accessed data, like product information or user roles, to reduce database calls.

C#
public class CacheService
{
    private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

    public T Get<T>(string key)
    {
        return _cache.TryGetValue(key, out T value) ? value : default;
    }

    public void Set<T>(string key, T value)
    {
        _cache.Set(key, value);
    }
}

// Registering as a singleton
services.AddSingleton<CacheService>();

Logging: Most logging frameworks in .NET (like Serilog) are registered as singletons because they don’t need to change during the application’s lifetime and should be accessed globally.

C#
services.AddSingleton<ILogger>(provider => new LoggerConfiguration().CreateLogger());

2. Scoped: One Instance per HTTP Request

Scoped services are created once per request and disposed of when the request is completed. This is suitable for services that hold request-specific data or rely on database contexts, which should be fresh for each request.

Example Use Cases:

  • Database Context (EF Core DbContext): Each HTTP request should have its own DbContext to ensure data consistency and isolation.
C#
services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")),
    ServiceLifetime.Scoped);

This way, each request has a unique database context instance, preventing data collisions and ensuring thread safety.

Unit of Work Pattern: Often used in combination with the Repository pattern, a Unit of Work service can coordinate the work of multiple repositories, committing changes at the end of a request.

C#
public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;

    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task SaveChangesAsync()
    {
        await _context.SaveChangesAsync();
    }
}

// Registering as scoped
services.AddScoped<IUnitOfWork, UnitOfWork>();

3. Transient: A New Instance per Request or Injection

Transient services are created each time they’re requested. They’re ideal for lightweight, stateless services that don’t require shared state across requests or components.

Example Use Cases:

  • Utility/Helper Classes: Services that perform operations like data formatting, string manipulation, or generating IDs.
C#
public class GuidGenerator
{
    public Guid GenerateNewGuid() => Guid.NewGuid();
}

// Registering as transient
services.AddTransient<GuidGenerator>();

Email Service: In a scenario where sending an email involves formatting and preparing data unique to each request, a transient email service would be appropriate.

C#
public class EmailService : IEmailService
{
    public Task SendEmailAsync(string recipient, string subject, string body)
    {
        // Send email logic here
    }
}

// Registering as transient
services.AddTransient<IEmailService, EmailService>();

Background Task Processors: A background processor that handles jobs for each incoming request can be transient if the processor only lives for a single request.

C#
public class BackgroundTaskProcessor : IBackgroundTaskProcessor
{
    public void Process(Task task)
    {
        // Process task logic here
    }
}

// Registering as transient
services.AddTransient<IBackgroundTaskProcessor, BackgroundTaskProcessor>();

Choosing the Right Service Lifetime

When deciding which lifetime to use, consider the following:

  • Singleton: Use when the service needs to maintain global state or is computationally expensive to instantiate. Beware of concurrency issues when the service holds mutable state.
  • Scoped: Ideal for request-level services, such as database contexts or units of work, where each request should operate on a fresh instance.
  • Transient: Suitable for lightweight, stateless services that can be created and disposed of quickly without maintaining any state.

Each of these lifetimes supports different scenarios, and selecting the correct one ensures both efficient resource management and optimal performance in your .NET Core applications.

Shares: