Dependency Injection in .NET Core: Principles & Examples

published on 01 September 2024

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:

  1. Define interfaces for dependencies
  2. Create implementing classes
  3. Register services in DI container
  4. 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.

Key concepts of Dependency Injection

Main ideas behind DI

DI in .NET Core relies on two core principles:

  1. Loose coupling: Classes receive dependencies from outside, not creating them internally.
  2. 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:

  1. Define interfaces for dependencies
  2. Implement these interfaces
  3. Register services in the container (usually in Program.cs)
  4. 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:

  1. Use property injection:
public class A
{
    public B B { get; set; }
}

public class B
{
    public A A { get; set; }
}
  1. 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:

  1. Hide Dispose method:
public class DisposableService : IDisposableService
{
    void IDisposable.Dispose()
    {
        // Disposal logic
    }
}
  1. 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:

  1. Unable to resolve service Fix: Register the service in DI container

    builder.Services.AddScoped<IUserService, UserService>();
    
  2. 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();
    });
    
  3. Multiple registrations of same service Fix: Use TryAdd* methods

    services.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:

  1. Set up DI container
  2. Register services with proper lifetimes
  3. Use constructor injection

DI is core to .NET Core, promoting loose coupling and modular design. Use it wisely to build scalable, maintainable apps.

Related posts

Read more