Introduction to Dependency Injection
Dependency Injection (DI) is a fundamental design pattern used in ASP.NET Core to manage the creation and consumption of dependencies. It promotes loose coupling, testability, and maintainability by externalizing the responsibility of instantiating objects.
Instead of a component creating its own dependencies, these dependencies are "injected" into the component from an external source, typically a DI container. This makes it easier to swap implementations, mock dependencies for testing, and organize your code.
Core Concepts
Understanding these key terms is crucial for grasping DI in ASP.NET Core:
- Dependency: An object that another object needs to perform its function. For example, a service that a controller needs.
- Service: The dependency that will be injected.
- Client: The object that receives the dependency.
- Container: The framework (in this case, the ASP.NET Core built-in DI container) responsible for managing the lifecycle and instantiation of services and injecting them into clients.
Working with Services
Services are typically represented by interfaces, promoting abstraction. This allows you to define a contract without specifying a concrete implementation, making your application more flexible.
Example: An ILogger Interface
public interface ILogger
{
void LogMessage(string message);
}
public class ConsoleLogger : ILogger
{
public void LogMessage(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
Service Registration
Before services can be injected, they must be registered with the DI container. This is typically done in the Program.cs
(or Startup.cs
in older versions) file using the builder.Services
collection.
ASP.NET Core provides three primary lifetimes for registering services:
- Singleton: A single instance of the service is created and reused throughout the application's lifetime.
- Scoped: A single instance of the service is created per client request (e.g., per HTTP request in a web application).
- Transient: A new instance of the service is created every time it's requested.
Registration Methods:
AddSingleton<TService, TImplementation>()
AddScoped<TService, TImplementation>()
AddTransient<TService, TImplementation>()
Example Registration:
// In Program.cs (or Startup.cs)
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
// Register ConsoleLogger as a singleton
builder.Services.AddSingleton<ILogger, ConsoleLogger>();
// Register another service as scoped
builder.Services.AddScoped<IScopedService, MyScopedService>();
// Register a transient service
builder.Services.AddTransient<ITransientService, MyTransientService>();
var app = builder.Build();
// ... rest of the application setup
Service Resolution
Once registered, services can be resolved and injected into other components, such as controllers, razor pages, or other services.
The most common way to inject dependencies is through constructor injection. The DI container automatically resolves and provides the required services when an instance of the component is created.
Example: Constructor Injection in a Controller
public class HomeController : Controller
{
private readonly ILogger _logger;
private readonly IScopedService _scopedService;
// Constructor injection
public HomeController(ILogger logger, IScopedService scopedService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_scopedService = scopedService ?? throw new ArgumentNullException(nameof(scopedService));
}
public IActionResult Index()
{
_logger.LogMessage("HomeController Index action called.");
_scopedService.PerformAction();
return View();
}
}
The DI container will look for registrations for ILogger
and IScopedService
. If found, it will create instances of their registered implementations and pass them to the HomeController
constructor.
You can also manually resolve services from the IServiceProvider
, though this is less common and generally discouraged in favor of constructor injection.
Best Practices
- Favor Interface Programming: Always depend on abstractions (interfaces) rather than concrete implementations.
- Keep Services Small and Focused: Each service should have a single responsibility (Single Responsibility Principle).
- Understand Service Lifetimes: Choose the appropriate lifetime (Singleton, Scoped, Transient) to avoid unexpected behavior or memory leaks.
- Use Constructor Injection: This is the most recommended and cleanest way to inject dependencies.
- Avoid Service Locator: While possible, the Service Locator pattern can lead to tightly coupled code and make testing harder.