DI in .NET Core improves code quality and flexibility. Here's what you need to know:
- Manages how objects get dependencies
- Built into .NET Core framework
- Uses interfaces to abstract implementations
- Promotes loose coupling
Key benefits:
- Easier unit testing
- Better modularity and reusability
- Simplified object lifetime management
How to use DI:
- Define interfaces for dependencies
- Create implementing classes
- Register services in DI container
- Use constructor injection
Service lifetimes:
- Transient: New instance each time
- Scoped: New instance per request
- Singleton: Single instance for app
Quick comparison:
Aspect | Without DI | With DI |
---|---|---|
Coupling | Tight | Loose |
Testability | Hard | Easy |
Flexibility | Low | High |
Maintenance | Complex | Simpler |
DI is key for building modern, scalable .NET Core apps. Master it to create robust, maintainable code.
Related video from YouTube
Key concepts of Dependency Injection
Main ideas behind DI
DI in .NET Core relies on two core principles:
- Loose coupling: Classes receive dependencies from outside, not creating them internally.
- Separation of concerns: Each class focuses on its main job, not managing dependencies.
Example:
// Without DI
public class OrderProcessor
{
private OrderValidator _validator = new OrderValidator();
public void ProcessOrder(Order order)
{
if (_validator.IsValid(order))
{
// Process order
}
}
}
// With DI
public class OrderProcessor
{
private readonly IOrderValidator _validator;
public OrderProcessor(IOrderValidator validator)
{
_validator = validator;
}
public void ProcessOrder(Order order)
{
if (_validator.IsValid(order))
{
// Process order
}
}
}
The DI version doesn't know about OrderValidator
's specifics, making it more flexible and testable.
Advantages of using DI
DI offers several perks for .NET Core developers:
Advantage | Description |
---|---|
Better testing | Easy to use mocks for unit tests |
Easier maintenance | Changes to one part don't affect others |
More flexibility | Switch between implementations easily |
Code reuse | Share dependencies across the app |
Testing example:
// Unit test for OrderProcessor
public void TestOrderProcessing()
{
var mockValidator = new Mock<IOrderValidator>();
mockValidator.Setup(v => v.IsValid(It.IsAny<Order>())).Returns(true);
var processor = new OrderProcessor(mockValidator.Object);
processor.ProcessOrder(new Order());
// Assert order processing
}
This test uses a mock IOrderValidator
, isolating OrderProcessor
for testing.
DI in .NET Core isn't just a pattern - it's baked in. The built-in container (IServiceProvider
) handles registration, resolution, and disposal of dependencies.
To use DI effectively:
- Define interfaces for dependencies
- Implement these interfaces
- Register services in the container (usually in
Program.cs
) - Use constructor injection in your classes
Setting up DI in .NET Core
Setting up DI in .NET Core is straightforward:
Installing needed packages
Most .NET Core projects don't need extra packages for DI. It's included by default in ASP.NET Core projects.
For console apps or class libraries, you might need to install:
dotnet add package Microsoft.Extensions.DependencyInjection
Setting up the DI container
Set up the DI container in Program.cs
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// App config here
app.Run();
Adding services to the container
Use builder.Services
to register services:
Lifetime | Method | Usage |
---|---|---|
Transient | AddTransient<TInterface, TImplementation>() |
New instance per request |
Scoped | AddScoped<TInterface, TImplementation>() |
New instance per client request |
Singleton | AddSingleton<TInterface, TImplementation>() |
Single instance for app lifetime |
Example:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<ITransientService, TransientService>();
builder.Services.AddScoped<IScopedService, ScopedService>();
builder.Services.AddSingleton<ISingletonService, SingletonService>();
var app = builder.Build();
Use constructor injection to use services:
public class MyController : Controller
{
private readonly ITransientService _transientService;
public MyController(ITransientService transientService)
{
_transientService = transientService;
}
// Controller actions
}
Service lifetimes in .NET Core
Understanding service lifetimes is crucial for effective DI in .NET Core. The framework offers three main lifetimes: Transient, Scoped, and Singleton.
Transient services
Created each time they're requested. New instance for every ask.
builder.Services.AddTransient<INumberService, NumberService>();
Good for lightweight, stateless operations like generating IDs or simple calculations.
Scoped services
Created once per client request (usually an HTTP request in web apps).
builder.Services.AddScoped<IUserService, UserService>();
Useful for maintaining state within a request, like a shopping cart service.
Singleton services
Created once for the entire app lifetime and shared across all requests.
builder.Services.AddSingleton<ILogService, LogService>();
Ideal for app-wide shared resources like caching or logging services.
Summary:
Lifetime | Creation | Best For |
---|---|---|
Transient | Every request | Stateless, light operations |
Scoped | Once per client request | State within a request |
Singleton | Once per app | App-wide shared resources |
Choose lifetimes based on the service's purpose and state needs. Using the wrong lifetime can cause unexpected behavior or resource issues.
Be careful injecting scoped services into singletons - it can cause problems as the scoped service might live too long.
How to implement DI: Step-by-step
Implementing DI in .NET Core:
Creating interfaces
Define interfaces for your services:
public interface IProduct {
List<Product> GetProducts();
}
Writing service classes
Create classes implementing these interfaces:
public class ProductService : IProduct {
public List<Product> GetProducts() {
// Implementation here
}
}
Using constructor injection
Use constructor injection to receive dependencies:
public class ProductController : ControllerBase {
private readonly IProduct _product;
public ProductController(IProduct product) {
_product = product;
}
[HttpGet]
public List<Product> GetProducts() {
return _product.GetProducts();
}
}
When to use property injection
Property injection can be useful for optional dependencies, but be cautious:
public class ErrorAlertController {
public string Title { get; set; }
public string Description { get; set; }
}
Register services in Program.cs
:
builder.Services.AddScoped<IProduct, ProductService>();
This approach allows easy swapping of implementations:
public class PrimeDayProductService : IProduct {
public List<Product> GetProducts() {
// Special Prime Day implementation
}
}
// Update registration
builder.Services.AddScoped<IProduct, PrimeDayProductService>();
Advanced DI techniques
As apps grow, you might need advanced DI techniques:
Using factory patterns
Factories help create objects dynamically:
public class DeviceFactory
{
private readonly IServiceProvider _serviceProvider;
public DeviceFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Device CreateDevice(DeviceType deviceType)
{
var label = _serviceProvider.GetRequiredService<ILabelGenService>().Generate();
return deviceType switch
{
DeviceType.Watch => new Watch(label),
DeviceType.Phone => new Phone(label),
DeviceType.Laptop => new Laptop(label),
_ => throw new NotImplementedException()
};
}
}
// Register in Program.cs
services.AddSingleton<DeviceFactory>();
Handling circular dependencies
Avoid circular dependencies, but if needed:
- Use property injection:
public class A
{
public B B { get; set; }
}
public class B
{
public A A { get; set; }
}
- Use
Lazy<T>
:
public class A
{
private readonly Lazy<B> _b;
public A(Lazy<B> b) => _b = b;
}
public class B
{
private readonly Lazy<A> _a;
public B(Lazy<A> a) => _a = a;
}
// Register
services.AddTransient<A>();
services.AddTransient<B>();
Lazy loading dependencies
Use Lazy<T>
for performance:
public class ExpensiveService
{
private readonly Lazy<IDataAccess> _dataAccess;
public ExpensiveService(Lazy<IDataAccess> dataAccess)
{
_dataAccess = dataAccess;
}
public void DoWork()
{
var data = _dataAccess.Value.GetData();
// Process data
}
}
// Register
services.AddTransient<IDataAccess, DataAccess>();
services.AddTransient<ExpensiveService>();
sbb-itb-29cd4f6
DI best practices in .NET Core
Follow these guidelines for better DI:
Designing good services
Create focused services following the Single Responsibility Principle:
public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body);
}
public class EmailService : IEmailService
{
public async Task SendEmailAsync(string to, string subject, string body)
{
// Implementation details
}
}
Avoiding service locator pattern
Use constructor injection instead of service locator:
public class OrderProcessor
{
private readonly IEmailService _emailService;
public OrderProcessor(IEmailService emailService)
{
_emailService = emailService;
}
public async Task ProcessOrder(Order order)
{
// Process order
await _emailService.SendEmailAsync(order.CustomerEmail, "Order Confirmation", "Your order has been processed.");
}
}
Managing disposable dependencies
Be careful with disposable dependencies:
- Hide Dispose method:
public class DisposableService : IDisposableService
{
void IDisposable.Dispose()
{
// Disposal logic
}
}
- Register with restrictive interface:
services.AddScoped<IMyService, MyDisposableService>();
Enable scope validation in Program.cs
:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseDefaultServiceProvider(options =>
options.ValidateScopes = true);
Common mistakes and how to fix them
Avoid these DI pitfalls:
Using DI too much
Don't overuse DI. For simple classes, use them directly:
public class SimpleClass
{
public void DoSomething() { /* ... */ }
}
Misusing service lifetimes
Choose the right lifetime:
Lifetime | Use Case |
---|---|
Transient | Lightweight, stateless services |
Scoped | Services needing state within a request |
Singleton | Services maintaining state across the app |
Be careful with scoped services in singletons.
Making singletons thread-safe
Implement thread-safety in singletons:
public class ThreadSafeSingleton
{
private static readonly object _lock = new object();
private static ThreadSafeSingleton _instance;
public static ThreadSafeSingleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ThreadSafeSingleton();
}
}
}
return _instance;
}
}
}
Enable scope validation:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseDefaultServiceProvider(options =>
options.ValidateScopes = true);
Testing with DI
Testing DI-based apps:
Unit testing DI code
Use mocks for dependencies:
[Fact]
public void TestServiceMethod()
{
var mockDependency = new Mock<IDependency>();
mockDependency.Setup(d => d.SomeMethod()).Returns("Expected Result");
var serviceUnderTest = new ServiceClass(mockDependency.Object);
var result = serviceUnderTest.MethodToTest();
Assert.Equal("Expected Result", result);
}
Using mocks in tests
For integration tests, use WebApplicationFactory
:
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.Replace(ServiceDescriptor.Scoped<IDataProvider>(sp =>
{
var mock = new Mock<IDataProvider>();
mock.Setup(x => x.GetData()).Returns(new Data { Value = "Test Data" });
return mock.Object;
}));
});
}
}
[Fact]
public async Task IntegrationTest()
{
await using var factory = new CustomWebApplicationFactory();
var client = factory.CreateClient();
var response = await client.GetAsync("/api/data");
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("Test Data", content);
}
DI examples in .NET Core
DI in MVC controllers
public class HomeController : Controller
{
private readonly IDateTime _dateTime;
public HomeController(IDateTime dateTime)
{
_dateTime = dateTime;
}
public IActionResult Index()
{
var serverTime = _dateTime.Now;
ViewData["Message"] = serverTime.Hour < 12 ? "Good Morning!" :
serverTime.Hour < 17 ? "Good Afternoon!" :
"Good Evening!";
return View();
}
}
// In Program.cs
services.AddSingleton<IDateTime, SystemDateTime>();
DI in background services
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IHost _host;
public Worker(ILogger<Worker> logger, IHost host)
{
_logger = logger;
_host = host;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
_host.StopAsync();
}
}
DI in console apps
class Program
{
static void Main(string[] args)
{
var services = new ServiceCollection();
ConfigureServices(services);
services
.AddSingleton<Executor, Executor>()
.BuildServiceProvider()
.GetService<Executor>()
.Execute();
}
private static void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ITest, Test>();
}
}
public class Executor
{
private readonly ITest _test;
public Executor(ITest test)
{
_test = test;
}
public void Execute()
{
_test.RunTest();
}
}
Fixing DI problems
Common DI issues and fixes:
-
Unable to resolve service Fix: Register the service in DI container
builder.Services.AddScoped<IUserService, UserService>();
-
Accessing scoped services from root provider Fix: Create a scope in middleware
app.Use(async (context, next) => { using (var scope = context.RequestServices.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>(); // Use dbContext } await next(); });
-
Multiple registrations of same service Fix: Use
TryAdd*
methodsservices.TryAddSingleton<IMyService, MyService>();
Debugging tips:
- Trace service resolutions
- Check service lifetimes
- Use built-in tools like
validateScopes: true
Conclusion
DI in .NET Core improves code quality by:
- Enhancing testability
- Boosting modularity
- Improving organization
To use DI effectively:
- Set up DI container
- Register services with proper lifetimes
- Use constructor injection
DI is core to .NET Core, promoting loose coupling and modular design. Use it wisely to build scalable, maintainable apps.