Introduction
Dependency Injection (DI) is a powerful design pattern that helps decouple dependencies
, making applications more testable
, flexible
, and maintainable
. In .NET, the ServiceCollection
provides a structured way to register dependencies, but choosing the right approach is crucial.
In this article, you'll learn:
- Standard service registration in DI
- Handling multiple implementations with
IEnumerable<IMessageService>
- Using keyed service registration to resolve specific implementations (requires .NET 8+ or a compatible DI container)
By understanding these techniques, you'll be able to register
, resolve
, and manage
dependencies effectively in your applications.
Let’s dive in! 🚀
Example Dependencies
Before diving into service registration, let’s define some dependencies:
public interface IMessageService
{
void SendMessage(string message);
}
public class EMailService : IMessageService
{
public void SendMessage(string message) =>
Console.WriteLine($"EMail message sent: {message}");
}
public class SMSService : IMessageService
{
public void SendMessage(string message) =>
Console.WriteLine($"SMS message sent: {message}");
}
public class DefaultProcessor
{
private readonly IMessageService _messageService;
public DefaultProcessor (IMessageService messageService)
{
_messageService = messageService;
}
public void ProcessMessage (string message)
{
_messageService.SendMessage (message);
}
}
public class MessageProcessor
{
private readonly IEnumerable<IMessageService> _messageServices;
private readonly IServiceProvider _serviceProvider;
public MessageProcessor (IEnumerable<IMessageService> messageServices, IServiceProvider serviceProvider)
{
_messageServices = messageServices;
_serviceProvider = serviceProvider;
}
public void ProcessMessage (string message)
{
// Iterate through all the services registered as IMessageService.
foreach (var service in _messageServices)
service.SendMessage (message);
}
public void ProcessMessage (string message, MyServiceType serviceType)
{
var service = _serviceProvider.GetRequiredKeyedService<IMessageService> (serviceType);
service.SendMessage (message);
}
}
public enum MyServiceType
{
EMail,
SMS
}
These services implement IMessageService
, allowing us to register them in DI. Now, let’s explore different registration methods.
Standard Service Registration in Dependency Injection
The most common way to register services in DI is using AddSingleton
, AddScoped
, or AddTransient
. Here, we register multiple services against the same interface.
Example
ServiceProvider serviceProvider = new ServiceCollection()
.AddSingleton<IMessageService, EMailService>() // First registration
.AddSingleton<IMessageService, SMSService>() // Last registration overrides previous ones
.AddSingleton<DefaultProcessor>()
.AddSingleton<MessageProcessor>()
.BuildServiceProvider();
// Resolving default service (only last registered IMessageService is used)
DefaultProcessor defaultProcessor = serviceProvider.GetRequiredService<DefaultProcessor>();
defaultProcessor.ProcessMessage("Hello, Default behavior. Dependency Injection!");
Issue
When multiple services are registered under the same interface (IMessageService), only the last registered service (SMSService) will be used. This may not always be the intended behavior.
Solution
To resolve multiple implementations, we can inject IEnumerable<IMessageService>
.
Handling Multiple Implementations with IEnumerable<IMessageService>
If we register multiple implementations of IMessageService
, we can inject IEnumerable<IMessageService>
to iterate through all implementations.
Example
MessageProcessor messageProcessor = serviceProvider.GetRequiredService<MessageProcessor>();
messageProcessor.ProcessMessage("Hello, Dependency Injection!");
Inside MessageProcessor
, we loop through all available implementations
public class MessageProcessor
{
private readonly IEnumerable<IMessageService> _messageServices;
public MessageProcessor(IEnumerable<IMessageService> messageServices)
{
_messageServices = messageServices;
}
public void ProcessMessage(string message)
{
foreach (var service in _messageServices)
service.SendMessage(message);
}
}
Result
Both EMailService
and SMSService
will process the message.
Use Case
This technique is useful when all implementations need to be executed, such as logging to multiple outputs or notifying multiple users.
Using Keyed Service Registration
Keyed service registration allows developers to resolve specific implementations based on a unique key.
Example
ServiceProvider serviceProvider = new ServiceCollection()
.AddKeyedSingleton<IMessageService, EMailService>(MyServiceType.EMail)
.AddKeyedSingleton<IMessageService, SMSService>(MyServiceType.SMS)
.AddSingleton<MessageProcessor>()
.BuildServiceProvider();
MessageProcessor messageProcessor = serviceProvider.GetRequiredService<MessageProcessor>();
// Resolving specific services using keys
messageProcessor.ProcessMessage("Hello, Dependency Injection!", MyServiceType.EMail);
messageProcessor.ProcessMessage("Hello, Dependency Injection!", MyServiceType.SMS);
Advantages of Keyed Services
-
Explicit Control
– Developers choose which implementation to use at runtime. -
Improved Testability
– Easier to test specific scenarios. -
Better Performance
– Avoid unnecessary object creation.
Implementation in MessageProcessor
public class MessageProcessor
{
private readonly IServiceProvider _serviceProvider;
public MessageProcessor(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void ProcessMessage(string message, MyServiceType serviceType)
{
var service = _serviceProvider.GetRequiredKeyedService<IMessageService>(serviceType);
service.SendMessage(message);
}
}
Result
Depending on the passed key (MyServiceType.EMail
or MyServiceType.SMS
), the correct service is resolved.
Use Case
Useful in multi-tenant applications, message routing, and strategy-based pattern implementations.
Common Pitfalls
While using Dependency Injection, developers often make mistakes that affect performance and maintainability. Here are some pitfalls to avoid:
Common Pitfalls
Registering Multiple Services Without IEnumerable<IMessageService>
-
Issue
: If multiple services are registered under the same interface (IMessageService), only the last registered service is used, leading to unexpected behavior. -
Example
: Registering both EMailService and SMSService under IMessageService but only one gets resolved. -
Solution
: InjectIEnumerable<IMessageService>
to process all registered services.
Overusing Singleton Dependencies
-
Issue
: Using too many singletons may introduce shared state, causing unintended side effects in multi-threaded applications. -
Example
: A singleton logging service storing mutable state incorrectly. -
Solution
: Ensure singleton services are truly stateless, or use locks for thread safety.
Injecting Too Many Dependencies Into a Single Class
-
Issue
: Having too many constructor parameters may indicate poor design, making maintenance harder. -
Example
: A service requiring 10 injected dependencies becomes unwieldy. -
Solution
: Consider grouping related dependencies into helper classes or using facade patterns.
Misconfigured Dependency Lifetimes
-
Issue
: Using singleton dependencies for stateful objects can lead to unintended behaviors. -
Example
: A database connection should not be a singleton. It might retain stale data. -
Solution
: Use Scoped lifetime for request-bound objects and Transient for short-lived services.
Using Service Locator Instead of Direct Injection
-
Issue
: Fetching dependencies manually via_serviceProvider.GetRequiredService<T>()
inside services creates hidden dependencies, reducing testability. -
Example
: A class resolving dependencies dynamically instead of having them injected. -
Solution
: Prefer constructor injection for better readability and testability. -
Exception
: UsingserviceProvider.GetRequiredService<T>()
inside middleware or entry points is acceptable.
Not Disposing Dependencies Properly
-
Issue
: Forgetting to dispose of IDisposable services results in resource leaks. -
Example
: Not disposing DbContext in an EF Core application leads to excessive memory consumption. -
Solution
: Use Scoped dependencies for disposable objects and implement IAsyncDisposable where necessary.
Best Practices
-
Choose the Right Lifetime Scope
– Use Transient for lightweight services, Scoped for request-specific objects, and Singleton for shared dependencies. -
Keep Dependencies Minimal
– Avoid excessive injection; group related dependencies logically. -
Use Constructor Injection Over Property Injection
– Constructor injection ensures dependencies exist when the object is instantiated. -
Use IoC Containers Efficiently
– Register services properly, avoid redundant registrations, and ensure correct dependency resolution. -
Test Dependencies with Mocking Frameworks
– Use Moq or FakeItEasy to create mock dependencies instead of relying on real implementations.
Key Takeaways
-
Standard DI registration
uses the last-registered service for a given interface. - Use
IEnumerable<T>
to resolve and use all registered implementations of an interface. -
Keyed services
(in .NET 8+ or compatible containers) allow resolving a specific implementation by key. - Always choose the right service lifetime (
Transient
,Scoped
,Singleton
) for your scenario. - Avoid common pitfalls: hidden dependencies, overusing singletons, and misconfigured lifetimes.
- Prefer constructor injection for clarity and testability.
Further Reading
- Microsoft Docs: Dependency Injection in .NET
- Keyed Services in .NET 8
- Mark Seemann: Dependency Injection Principles, Practices, and Patterns
- Autofac Documentation
Final Thoughts
Understanding common pitfalls and best practices ensures a smooth and efficient DI setup. Avoid hidden dependencies, misconfigured lifetimes, and over-injected classes to keep your application scalable, maintainable, and testable. By following DI best practices, developers can write cleaner, more efficient, and modular applications while avoiding technical debt.
Understanding service registration techniques in Dependency Injection is crucial for maintainable, scalable, and flexible applications. This article explored:
-
Standard DI registration
, showing how last-registered services override previous ones. -
Using IEnumerable<T>
, allowing multiple implementations to coexist. -
Leveraging Keyed Services
, enabling explicit control over service resolution.
Choosing the right DI strategy depends on your application needs. Whether you're building an Asp Net Core API, a background service, or a multi-tenant system, understanding how services are registered and resolved ensures better architecture and performance.
Start experimenting with IEnumerable and Keyed Services, and you’ll have a more flexible, maintainable DI setup in your projects.
About the Author
Hi, I’m Jiten Shahani
, a passionate developer with a strong background in API development and C# programming. Although I’m new to .NET, my journey into learning ASP .NET Core began in December 2024, driven by a desire to build scalable and maintainable applications.
Through my exploration of dependency injection, I’ve realized how crucial it is for writing flexible, testable, and loosely coupled code. This article is a reflection of my learning process, aimed at helping fellow developers, especially beginners, understand the different ways to inject dependencies effectively in C#.
Feel free to connect with me to exchange ideas and learn together!
Top comments (0)