Dependency Injection

Dependency Injection in .NET

Dependency Injection (DI) is a powerful design pattern that is widely used in modern software development. In .NET, DI is a first-class citizen, integrated into the framework to promote clean code, testability, and scalability. This chapter explores what Dependency Injection is, why it matters, and how to implement it effectively in .NET applications.

What is Dependency Injection?

Dependency Injection is a design pattern that deals with how objects and their dependencies are created and managed. Instead of a class creating its dependencies, they are “injected” into the class from an external source, typically a DI container.

Key Concepts

Why Use Dependency Injection?

  1. Separation of Concerns: Classes do not need to know how to create or manage their dependencies.
  2. Improved Testability: Dependencies can be mocked or replaced for unit testing.
  3. Loose Coupling: Classes depend on abstractions (e.g., interfaces) rather than concrete implementations, making the code more flexible.
  4. Centralized Configuration: Dependencies can be configured in one place and reused throughout the application.

How Dependency Injection Works in .NET

In .NET, Dependency Injection is built into the framework via the Microsoft.Extensions.DependencyInjection namespace. The DI container manages the lifecycle of objects and provides them wherever they are needed.

Dependency Injection Lifecycle

When the application starts:

  1. Dependencies are registered with the DI container in Program.cs.
  2. The DI container resolves and injects these dependencies where they are required.

Types of Dependency Injection

  1. Constructor Injection: Dependencies are provided through the class constructor. This is the most common and preferred method.
  2. Method Injection: Dependencies are passed as method parameters.
  3. Property Injection: Dependencies are assigned to properties of the class.

This chapter focuses on Constructor Injection as it is the most widely used and aligns well with .NET practices.

Implementing Dependency Injection in .NET

Let’s walk through a step-by-step example to understand how DI is implemented in a .NET application.

Step 1: Define an Interface

Start by defining an interface that represents the dependency. For example, a simple logging service:

public interface ILoggerService
{
    void Log(string message);
}

Step 2: Implement the Interface

Create a class that implements the interface:

public class ConsoleLoggerService : ILoggerService
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

Step 3: Register the Dependency

In Program.cs, register the interface and its implementation with the DI container:

var builder = WebApplication.CreateBuilder(args);

// Register the ILoggerService with its implementation
builder.Services.AddSingleton<ILoggerService, ConsoleLoggerService>();

var app = builder.Build();

app.MapGet("/", (ILoggerService logger) =>
{
    logger.Log("Hello, Dependency Injection!");
    return "Check the logs!";
});

app.Run();

Step 4: Inject the Dependency

Use Constructor Injection to inject the dependency into a class, such as a controller or service:

public class HomeController : Controller
{
    private readonly ILoggerService _logger;

    public HomeController(ILoggerService logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        _logger.Log("Index action called.");
        return View();
    }
}

Step 5: Using Scoped, Transient, and Singleton Lifetimes

When registering services, you can specify their lifetime:

  1. Singleton: A single instance is created and shared throughout the application.

    builder.Services.AddSingleton<ILoggerService, ConsoleLoggerService>();
    
  2. Scoped: A new instance is created for each HTTP request.

    builder.Services.AddScoped<ILoggerService, ConsoleLoggerService>();
    
  3. Transient: A new instance is created every time it is requested.

    builder.Services.AddTransient<ILoggerService, ConsoleLoggerService>();
    

Real-World Example

Here’s a complete example of using DI in an ASP.NET Core MVC application to manage a product catalog:

Service Interface

public interface IProductService
{
    IEnumerable<string> GetProducts();
}

Service Implementation

public class ProductService : IProductService
{
    public IEnumerable<string> GetProducts()
    {
        return new List<string> { "Product 1", "Product 2", "Product 3" };
    }
}

Register the Service

builder.Services.AddScoped<IProductService, ProductService>();

Inject into Controller

public class ProductController : Controller
{
    private readonly IProductService _productService;

    public ProductController(IProductService productService)
    {
        _productService = productService;
    }

    public IActionResult Index()
    {
        var products = _productService.GetProducts();
        return View(products);
    }
}

Best Practices

  1. Prefer Constructor Injection: It is clean, consistent, and ensures dependencies are provided when the class is instantiated.
  2. Use Interfaces: Always depend on abstractions (interfaces) rather than concrete implementations.
  3. Register Dependencies Centrally: Configure all services in a single location (typically Program.cs).
  4. Choose the Right Lifetime: Use Singleton for shared state, Scoped for request-specific services, and Transient for lightweight and stateless services.
  5. Avoid Service Locator Pattern: Directly inject dependencies instead of using the DI container to resolve them manually.

Conclusion

Dependency Injection simplifies managing dependencies in .NET applications by promoting loose coupling, testability, and maintainability. By integrating DI into your project, you can focus on building features without worrying about how objects are created and managed. The examples in this chapter should give you a strong foundation to implement DI effectively in your .NET applications.