Introduction to Dependency Injection
Dependency Injection (DI) is a design pattern used in object-oriented programming. It is a software design pattern that implements the inversion of control (IoC) principle. In DI, a class receives its dependencies from an external source rather than creating them itself. This makes code more modular, testable, and maintainable.
.NET Core has a built-in, lightweight dependency injection container that is simple to use and highly efficient. This document will guide you through the fundamentals of using DI in your .NET Core applications.
Why Dependency Injection?
DI offers several key advantages:
- Decoupling: Classes don't need to know how to create their dependencies, leading to looser coupling between components.
- Testability: It's easier to substitute mock or fake implementations of dependencies during testing.
- Maintainability: Changes to a dependency don't necessarily require changes in the classes that use it, as long as the interface remains the same.
- Reusability: Components become more reusable as they are not tied to specific implementations of their dependencies.
- Configuration: The configuration of which implementation to use for a given interface can be centralized.
The Built-in DI Container
The .NET Core framework provides a default DI container available through the Microsoft.Extensions.DependencyInjection NuGet package. This container manages the creation and lifetime of objects (services) and injects them into other objects (clients) that require them.
The core components of the DI system are:
IServiceCollection: An interface representing a collection of service descriptors. This is where you register your services.IServiceProvider: An interface representing a container that provides access to the registered services. This is used to resolve dependencies.- Service Lifetimes: Control how instances of a service are created and managed.
Registering Services
Services are registered with the DI container in your application's startup configuration, typically in the Startup.cs file within the ConfigureServices method. You use the IServiceCollection instance to add service descriptors.
Service Lifetimes
The lifetime of a service determines how many instances of the service are created and for how long they are kept alive.
Singleton
A singleton service is created only once per container. The same instance is returned for all subsequent requests for that service.
services.AddSingleton<IMyService, MyService>();
Scoped
A scoped service is created once per client request. This is typically used in web applications where you want a single instance of a service to be available throughout the lifecycle of an HTTP request.
services.AddScoped<IMyService, MyService>();
Transient
A transient service is created every time it's requested from the container. This means a new instance is created for each consumer and for each request.
services.AddTransient<IMyService, MyService>();
Consuming Services
Once services are registered, they can be consumed by injecting them into the constructor of your classes. The DI container will automatically resolve and provide the instances when the class is instantiated.
Consider an example with a logger service:
public interface ILoggerService
{
void Log(string message);
}
public class ConsoleLoggerService : ILoggerService
{
public void Log(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
public class MyService
{
private readonly ILoggerService _logger;
public MyService(ILoggerService logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void DoSomething()
{
_logger.Log("Doing something...");
// ... business logic ...
}
}
In your Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ILoggerService, ConsoleLoggerService>(); // Registering the logger
services.AddTransient<MyService>(); // Registering MyService so it can be resolved with its dependencies
}
In a web controller or a background service, you can then request MyService:
public class MyController : Controller
{
private readonly MyService _myService;
public MyController(MyService myService)
{
_myService = myService ?? throw new ArgumentNullException(nameof(myService));
}
public IActionResult Index()
{
_myService.DoSomething();
return View();
}
}
The DI container will see that MyController requires MyService. When resolving MyService, it will see that it requires ILoggerService. It will then resolve the registered ConsoleLoggerService and provide it to MyService. Finally, the resolved MyService instance will be provided to MyController.
Advanced Concepts
The .NET Core DI container supports several advanced features:
IServiceProviderFactory: For integrating with other containers.- Generics: Registering generic types.
IEnumerable<T>: Resolving collections of services.Func<T>factories: For more complex object creation logic.AddOptions()andConfigureOptions(): For managing configuration and options.
Best Practices
- Program to interfaces, not implementations: Always register and inject interfaces instead of concrete classes. This maximizes the benefits of DI.
- Keep DI simple: Don't overcomplicate your DI setup. The default container is powerful for most scenarios.
- Be mindful of lifetimes: Choose the appropriate service lifetime to avoid unexpected behavior or memory leaks.
- Avoid constructor circular dependencies: These can lead to runtime errors.
- Use DI for external dependencies: Injecting framework services, repositories, and other external components.