Hey there! If you’ve been following along with my blog, you know we talked about Dependency Injection (DI) in .NET. We discussed how it makes your code better and why .NET developers love it. Today, we’re focusing on Service Lifetimes in DI.
If you’ve worked on .NET projects, especially with ASP.NET Core, you know about AddTransient, AddScoped and AddSingleton. These options are key when registering services. Choosing the wrong one ca cause problems like performance issues or memory leaks. So, let’s explore what they are, how they work and when to use them.
Service Lifetimes: An Overview
In .NET, dependency injection uses a built-in IoC (Inversion of Control) container. When you register services, you decide how the container manages their lifecycle. The three main lifetimes are:
- Transient: Created each time they are requested
- Scoped: Created once per request
- Singleton: Created once and shared throughout the application
Let’s dive into each one, with examples from my own experience.
AddTransient: Temporary but Flexible
The AddTransient method gives you a new instance of a service every time it’s needed. It’s perfect for services that are lightweight and don’t keep state.
Example:
services.AddTransient<IMyService, MyService>();
When to use AddTransient:
- For services that are stateless.
- When the service is lightweight and doesn’t use many resources.
- Great for utility or helper classes that don’t keep any state.
Deep Dive: Transient in Real-World Scenarios
In microservices, especially in payment processing, you might have utility services. For example, in my online consultation booking project, we had a utility to generate appointment codes. Since each generation was independent, AddTransient was efficient and saved memory.
Pitfalls of using AddTransient
Using AddTransient for services that share data or maintain a connection state is a mistake. I once used it for a database context, leading to many connections being opened. This increased resource use and hurt performance.
Best Practices for AddTransient
- Use it for lightweight services that don’t share state
- Avoid it for database contexts or caching mechanism
- Use proper execution handling to handle transient failures in critical operations.
AddScoped: Keeping it Local
AddScoped ensures a new instance of the service is created once per request. It’s good for services that need to keep some state during a single request.
Example
services.AddScoped<IOrderService, OrderService>();
Use Case and Scenarios
Imagine a web app where each user logs in and has personalized data. Using AddScoped for user profile services means each user gets their own instance. Data from one session doesn’t leak to another.
My Experience: Challenges with AddScoped
In my medical booking app, I used AddScoped for logging services. But since the logger needed to keep data beyond a single request, it was better as a Singleton. This mistake showed me the importance of thinking about data scope before choosing a lifetime.
Performance Considerations
While AddScoped is good for per-request data, be careful of scenarios where scoped services are injected into singleton instances. This can cause unexpected behaviors, as the Singleton will hold onto scoped service, leading to unpredictable outcomes.
AddSingleton: Once Instance to Rule Them All
The AddSingleton method makes sure there’s only one instance of a service in the app. This instance is shared everywhere it’s needed.
Example
services.AddSingleton<ILoggingService, LoggingService>();
Practical Use Cases
Singletons are perfect for storing app-wide settings or config data. In a past project, we used it for global config. This ensured all services had the same settings.
Common Mistakes with Singleton
Singleton can be tricky for services that need user-specific data. I learned this the hard way with API requests. Storing user session data in a singleton caused issues.
Singleton in Distributed Systems
Be careful with singleton in apps that scale out. They’re not thread-safe. Use distributed caches or database-backed singletons for multi-instance setups.
Combining Lifetimes: Real-World Patterns
It’s often best to mix different lifetimes. Use Singleton for config, Scoped for database, and Transient for lightweight services.
My Approach
In a recent payment system, we mixed lifetimes, The transaction logger was a Singleton, payment context was Scoped, and utility calculators were Transient. This saved resources and kept data consistent.
Advanced Techniques: Factory Patterns with DI
For lifetimes that change based on conditions, use factories. Factories let you decide between Transient, Scoped, or Singleton at runtime.
Example
services.AddSingleton<IServiceFactory, ServiceFactory>();
I want to explain this much deeper because it has greatly helped in my previous projects.
Let’s first define what is Factory Pattern, Factory Pattern is a creational design pattern that provides a way to create objects without specifying the exact class of object that will be created. In the context of Dependency Injection (DI), it helps resolve instances with lifetimes that change based on conditions or runtime requirements.
Why Use Factory Patterns with DI?
In some scenarios, you may need to determine the lifetime of a service at runtime instead of at startup. For example:
- You might need to choose between Transient, Scoped and Singleton based on user input or configuration settings.
- You might want to dynamically choose between multiple implementations of the same interface.
Using a factory pattern with DI allows you to:
- Maintain flexibility when choosing the lifetime of objects.
- Resolve different implementations of a service at runtime.
How to implement a Factory Pattern with DI
Step 1: Define an interface for your service
public interface IMyService
{
void Execute();
}
Step 2: Implement multiple concrete services
public class TransientService : IMyService
{
public void Execute() => Console.WriteLine("Executing Transient Service");
}
public class ScopedService : IMyService
{
public void Execute() => Console.WriteLine("Executing Scoped Service");
}
public class SingletonService : IMyService
{
public void Execute() => Console.WriteLine("Executing Singleton Service");
}
Step 3: Create a factory interface
public interface IServiceFactory
{
IMyService GetService(string key);
}
Step 4: Implement the factory
public class ServiceFactory : IServiceFactory
{
private readonly IServiceProvider _serviceProvider;
public ServiceFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IMyService GetService(string key)
{
return key switch
{
"transient" => _serviceProvider.GetRequiredService<TransientService>(),
"scoped" => _serviceProvider.GetRequiredService<ScopedService>(),
"singleton" => _serviceProvider.GetRequiredService<SingletonService>(),
_ => throw new ArgumentException($"Invalid key: {key}")
};
}
}
Step 5: Register services and factory in DI
var builder = WebApplication.CreateBuilder(args);
// Register services with appropriate lifetimes
builder.Services.AddTransient<TransientService>();
builder.Services.AddScoped<ScopedService>();
builder.Services.AddSingleton<SingletonService>();
// Register the factory as a singleton
builder.Services.AddSingleton<IServiceFactory, ServiceFactory>();
var app = builder.Build();
Step 6: Use the factory in your application
app.MapGet("/", (IServiceFactory serviceFactory, string type) =>
{
try
{
var service = serviceFactory.GetService(type);
service.Execute();
return Results.Ok("Service executed.");
}
catch (ArgumentException ex)
{
return Results.BadRequest(ex.Message);
}
});
app.Run();
Example Usage:
- Run the application
- Access the endpoint with different types
- /api/service?type=transient
- /api/service?type=scoped
- /api/service?type=singleton
Expected Output:
- Transient: Executing Transient Service
- Scoped: Executing Scoped Service
- Singleton: Executing Singleton Service
Advantages of using Factory Pattern with DI
- Runtime Decision-Making: Choosing between Transient, Scoped and Singleton at runtime.
- Multiple Implementations: Dynamically resolve different implementations of the same interface.
- Separation of Concerns: Keeps your service resolution logic centralized in one place.
Wrapping Up
Picking the right service lifetime in .NET is key for maintainable apps. Whether it’s AddTransient, AddScoped or AddSingleton, think about your use case and the implications.
If you want to learn more, here are some resources:
Thanks for following along. I hope this clears up when and why to use each lifetime.