Developers are familiar with the basics (like registering services in Startup.cs), many struggle to use it effectively in real-world applications.
In this post, weβll go beyond the basics and look at advanced techniques, common mistakes, and how to design better, testable, and maintainable .NET applications using DI.
π§± Quick Recap: What Is Dependency Injection?
Dependency Injection is a design pattern that allows objects to receive their dependencies from an external source rather than creating them internally. In ASP.NET Core, the built-in IoC (Inversion of Control) container manages this for you.
public interface IEmailService
{
void Send(string to, string subject, string body);
}
public class SmtpEmailService : IEmailService
{
public void Send(string to, string subject, string body)
{
// SMTP logic here
}
}
// Registering the service
services.AddScoped();
β
Common Service Lifetimes
ASP.NET Core provides three primary lifetimes:
Singleton: Created once for the application's lifetime
Scoped: Created once per request
Transient: Created every time it's requested
π‘ Tip: Avoid using Singleton for services that depend on Scoped dependencies like DbContext. This can lead to runtime exceptions.
β οΈ Anti-Patterns to Avoid
- Service Locator Pattern Using IServiceProvider manually to resolve dependencies is a code smell:
// DON'T DO THIS
public class SomeService
{
private readonly IServiceProvider _provider;
public SomeService(IServiceProvider provider)
{
_provider = provider;
}
public void DoSomething()
{
var db = _provider.GetService<MyDbContext>();
}
}
This hides dependencies, making your code harder to test and maintain.
β Better: Use constructor injection instead.
π§ͺ DI and Unit Testing
One of the biggest benefits of DI is testability.
Letβs say you have a controller that depends on IUserService:
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var user = _userService.GetUserById(id);
return Ok(user);
}
}
You can now easily mock IUserService in a unit test:
var mock = new Mock<IUserService>();
mock.Setup(u => u.GetUserById(1)).Returns(new User { Id = 1, Name = "John" });
var controller = new UsersController(mock.Object);
var result = controller.Get(1);
π§ Tip: Use Interface Segregation for Better DI
Instead of large interfaces with many methods, split them into smaller, focused interfaces. This makes your services easier to mock and reduces coupling.
π§° Useful Libraries for DI Enhancements
Scrutor β Adds assembly scanning for service registration
π services.Scan(...)
Autofac β More powerful container if you outgrow the built-in one
MediatR β Combine DI with CQRS pattern for better decoupling
β
Final Thoughts
Dependency Injection is more than just registering services. Itβs a mindset that promotes loose coupling, better design, and easier testing.
By avoiding common anti-patterns and leveraging DI features smartly, you can build applications that are easier to maintain and extend over time.
π¬ Whatβs Next?
Want me to dive into Scoped pitfalls in async methods?
Curious about integrating Autofac or Scrutor in a clean architecture project?
Let me know in the comments β and if you found this helpful, consider giving it a β€οΈ!
π Tags:
#dotnet #aspnetcore #dependency-injection #csharp #architecture
Top comments (0)