Dependency Injection in .NET Core: A Comprehensive Guide

Dependency Injection (DI) is a key design pattern in .NET Core that promotes loose coupling, testability, and maintainability. This guide explains the concepts, types of service lifetimes, implementation, and best practices for using DI in .NET Core applications. Learn how to build modular and scalable applications with built-in Dependency Injection.

By · · Updated · 13 min read · beginner

Introduction

Dependency Injection (DI) is a fundamental concept in modern software development that promotes loose coupling, testability, and maintainability. In .NET Core, Dependency Injection is a first-class citizen and is deeply integrated into the framework. It allows developers to manage dependencies in a clean and efficient way, making applications more modular and easier to maintain.

This document provides a detailed explanation of Dependency Injection in .NET Core, including its concepts, benefits, types of service lifetimes, implementation, and best practices.


What is Dependency Injection?

Dependency Injection is a design pattern that enables the inversion of control (IoC) by delegating the responsibility of creating and managing dependencies to a container or framework. Instead of hardcoding dependencies inside a class, they are "injected" into the class at runtime.

Key Terms:

  • Dependency: A service or object that another object relies on to perform its functionality.
  • Service: A reusable piece of functionality, typically represented as an interface or a class.
  • Service Container: A framework component that manages the creation and resolution of services (e.g., IServiceProvider in .NET Core).

Why Use Dependency Injection?

  1. Loose Coupling: Classes depend on abstractions (interfaces) rather than concrete implementations.
  2. Testability: Dependencies can be mocked or stubbed during unit testing.
  3. Maintainability: Changes to one part of the application are less likely to affect others.
  4. Scalability: Services with appropriate lifetimes (e.g., scoped) can be reused efficiently.
  5. Separation of Concerns: The responsibility of creating and managing dependencies is separated from the business logic.

Dependency Injection in .NET Core

.NET Core provides built-in support for Dependency Injection. It uses a Service Container (IServiceProvider) to manage services and their lifetimes. Developers can register services and their implementations in the Startup.cs file (or Program.cs in .NET 6+).

Service Registration

Services are registered with the IServiceCollection interface, which is part of the Microsoft.Extensions.DependencyInjection namespace. Once registered, the services can be resolved using the IServiceProvider.

Example:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyService, MyService>();
}

Service Resolution

Services are resolved using constructor injection, property injection, or method injection. Constructor injection is the most common and recommended approach in .NET Core.

Example:

public class MyController : Controller
{
    private readonly IMyService _myService;

    public MyController(IMyService myService)
    {
        _myService = myService;
    }

    public IActionResult Index()
    {
        var result = _myService.DoSomething();
        return View(result);
    }
}

Types of Service Lifetimes in .NET Core

In .NET Core, services can have different lifetimes, depending on how they are registered and used:

1. Transient

  • A new instance of the service is created every time it is requested.
  • Use case: Lightweight services that are stateless and do not need to be shared.

Example:

services.AddTransient<IMyService, MyService>();

2. Scoped

  • A new instance of the service is created once per client request (e.g., in a web application).
  • Use case: Services that need to be shared within a single request but not across multiple requests.

Example:

services.AddScoped<IUserRepository, UserRepository>();

3. Singleton

  • A single instance of the service is created and shared throughout the application's lifetime.
  • Use case: Services that need to maintain state across the entire application (e.g., caching, configuration).

Example:

services.AddSingleton<ICacheService, CacheService>();

Implementing Dependency Injection in .NET Core

Step 1: Define the Service Interface

Define an interface that represents the service.

public interface IMyService
{
    string GetMessage();
}

Step 2: Implement the Service

Create a class that implements the interface.

public class MyService : IMyService
{
    public string GetMessage()
    {
        return "Hello from MyService!";
    }
}

Step 3: Register the Service

Register the service in the IServiceCollection during application startup.

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyService, MyService>();
}

Step 4: Inject the Service

Inject the service into a class (e.g., controller, service) using constructor injection.

public class HomeController : Controller
{
    private readonly IMyService _myService;

    public HomeController(IMyService myService)
    {
        _myService = myService;
    }

    public IActionResult Index()
    {
        var message = _myService.GetMessage();
        return Content(message);
    }
}

Advanced Dependency Injection Features in .NET Core

1. Property Injection

Although constructor injection is the preferred method, property injection is also supported. However, it should be used sparingly.

Example:

public class MyController : Controller
{
    [FromServices]
    public IMyService MyService { get; set; }
}

2. Method Injection

In some cases, you may want to inject a service into a method rather than a constructor or property.

Example:

public void ProcessData([FromServices] IMyService myService)
{
    var result = myService.DoSomething();
}

3. Factory Pattern

You can use a factory delegate to create services dynamically.

Example:

services.AddTransient<IMyService>(provider => new MyService());

4. Named Services

You can register multiple implementations of the same interface and resolve them by name.

Example:

services.AddTransient<IMyService, MyServiceA>("ServiceA");
services.AddTransient<IMyService, MyServiceB>("ServiceB");

// Resolve by name
var serviceA = serviceProvider.GetRequiredService<IMyService>("ServiceA");
var serviceB = serviceProvider.GetRequiredService<IMyService>("ServiceB");

5. Generics

You can register and resolve generic services.

Example:

services.AddTransient(typeof(IRepository<>), typeof(Repository<>));

Best Practices for Dependency Injection in .NET Core

  1. Use Constructor Injection: Always prefer constructor injection over property or method injection.
  2. Follow the Single Responsibility Principle: Keep classes focused on their core responsibilities.
  3. Use Appropriate Lifetimes: Choose the correct service lifetime based on the use case.
  4. Avoid Circular Dependencies: Design your services carefully to avoid circular dependencies.
  5. Use Interfaces for Abstractions: Always define services as interfaces to promote loose coupling.
  6. Test Dependencies: Use DI to easily mock dependencies in unit tests.

Example: Full Implementation in a .NET Core Web Application

Service Interface

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

Service Implementation

public class LoggerService : ILoggerService
{
    public void Log(string message)
    {
        Console.WriteLine(
quot;Log: {message}"); } }

Register the Service

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddSingleton<ILoggerService, LoggerService>();
}

Use the Service in a Controller

public class HomeController : Controller
{
    private readonly ILoggerService _loggerService;

    public HomeController(ILoggerService loggerService)
    {
        _loggerService = loggerService;
    }

    public IActionResult Index()
    {
        _loggerService.Log("HomeController.Index called.");
        return Content("Hello, World!");
    }
}

Dependency Injection is a powerful design pattern that is deeply integrated into .NET Core. It promotes loose coupling, testability, and maintainability, making it an essential tool for building modern, scalable applications. By understanding the concepts, lifetimes, and best practices of DI in .NET Core, developers can create robust and efficient applications.

By leveraging the built-in DI container in .NET Core, developers can focus on writing clean, modular, and reusable code, while the framework handles the complexities of service creation and management.