Introduction
ASP.NET Core is designed around the principle of modularity and maintainability. One of the key architectural patterns that makes this possible is Dependency Injection (DI) . DI is a technique where dependencies (services or components required by a class) are provided to it, rather than the class creating them itself.
ASP.NET Core provides a built-in dependency injection container that allows developers to register, configure, and inject dependencies easily, promoting loose coupling, testability, and maintainability in applications.
![DI]()
What is Dependency Injection?
Dependency Injection is a design pattern that enables an object to receive its dependencies from an external source rather than creating them directly.
This pattern helps:
Reduce tight coupling between classes.
Promote code reusability and testability.
Centralize configuration and service management.
Example without Dependency Injection
public class EmailService
{
public void Send(string to, string message)
{
Console.WriteLine($"Sending Email to {to}: {message}");
}
}
public class NotificationController
{
private EmailService _emailService = new EmailService();
public void Notify(string user, string message)
{
_emailService.Send(user, message);
}
}
In this case, NotificationController
is tightly coupled with EmailService
. If you want to replace EmailService
with a SmsService
, you would have to modify the controller’s code — which violates the Open/Closed Principle of SOLID design.
Dependency Injection in ASP.NET Core
ASP.NET Core solves this problem by building DI directly into the framework . You can register services and manage their lifetimes in the service container provided by the framework.
Built-In Service Container
The built-in container is lightweight yet powerful. It supports three main lifetimes for services:
Transient – Created each time they are requested.
Scoped – Created once per request.
Singleton – Created once for the lifetime of the application.
How Dependency Injection Works in ASP.NET Core
Step 1. Register Services in Program.cs
You define which classes should be available for injection inside the service container using the IServiceCollection
interface.
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddScoped<ILogService, LogService>();
builder.Services.AddSingleton<IConfigService, ConfigService>();
var app = builder.Build();
app.MapControllers();
app.Run();
Here:
AddTransient()
creates a new instance every time.
AddScoped()
creates one instance per web request.
AddSingleton()
creates one instance throughout the app lifecycle.
Step 2. Inject Dependencies into Controllers or Classes
You can inject these services directly into your controllers or other classes via constructor injection.
public interface IEmailService
{
void Send(string to, string message);
}
public class EmailService : IEmailService
{
public void Send(string to, string message)
{
Console.WriteLine($"Email sent to {to}: {message}");
}
}
public class NotificationController : ControllerBase
{
private readonly IEmailService _emailService;
// Constructor Injection
public NotificationController(IEmailService emailService)
{
_emailService = emailService;
}
[HttpGet("notify")]
public IActionResult Notify(string user, string message)
{
_emailService.Send(user, message);
return Ok("Notification Sent!");
}
}
ASP.NET Core automatically resolves dependencies from the service container when creating controller instances.
Step 3. Using Dependency Injection in Non-Controller Classes
DI isn’t limited to controllers. You can inject dependencies anywhere, such as in custom middleware or background services.
Example: Using DI in Middleware
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogService _logService;
public LoggingMiddleware(RequestDelegate next, ILogService logService)
{
_next = next;
_logService = logService;
}
public async Task InvokeAsync(HttpContext context)
{
_logService.Log("Processing request: " + context.Request.Path);
await _next(context);
_logService.Log("Finished processing request.");
}
}
// Register middlewarepublic static class MiddlewareExtensions
{
public static IApplicationBuilder UseLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggingMiddleware>();
}
}
Step 4. Using Service Locator Pattern (Cautiously)
Sometimes you may need to manually resolve a service instance using IServiceProvider
— for example, in legacy code or background tasks.
var emailService = app.Services.GetService<IEmailService>();
emailService.Send("[email protected]", "App Started!");
However, this approach should be used sparingly, as it reduces the benefits of DI by hiding dependencies.
Service Lifetimes Explained
Choosing the right service lifetime is crucial for performance and correctness.
Lifetime | Description | Example Use Case |
---|
Transient | Created every time requested | Lightweight, stateless services like helpers |
Scoped | One instance per HTTP request | Database contexts or unit of work |
Singleton | One instance for the whole app | Configuration or caching services |
Example demonstrating all lifetimes:
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddScoped<IDbService, DbService>();
builder.Services.AddSingleton<IAppCache, AppCache>();
ASP.NET Core manages object creation and disposal automatically based on their lifetime.
Replacing the Built-In DI Container
While ASP.NET Core comes with a built-in DI container, it’s also designed to be flexible. You can replace it with popular third-party containers like:
Autofac
StructureMap
Castle Windsor
Example: Using Autofac
public class Startup
{
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType<EmailService>().As<IEmailService>();
}
}
This flexibility allows developers to use advanced DI features such as property injection, interception, and module scanning if needed.
Benefits of Built-In Dependency Injection
Improved Testability
You can easily mock dependencies for unit testing.
var mockEmailService = new Mock<IEmailService>();
mockEmailService.Setup(x => x.Send(It.IsAny<string>(), It.IsAny<string>()));
var controller = new NotificationController(mockEmailService.Object);
controller.Notify("[email protected]", "Hello!");
Loose Coupling
Classes depend on abstractions, not concrete implementations.
Centralized Configuration
All services are registered in one place ( Program.cs
).
Extensibility
You can easily switch implementations without modifying dependent code.
Built-In Framework Integration
ASP.NET Core’s features like logging, configuration, and middleware natively support DI.
Common Mistakes in DI
Overusing Singleton: Injecting scoped services into singletons can cause runtime errors.
Circular Dependencies: Two services depending on each other cause DI failures.
Service Locator Abuse: Avoid fetching services manually everywhere; use constructor injection instead.
Visual Overview
Below is the conceptual flow of how DI works in ASP.NET Core:
[Service Registration] --> [Service Container] --> [Constructor Injection] --> [Service Resolution]
Conclusion
Dependency Injection is at the heart of ASP.NET Core’s architecture. It's built-in DI framework simplifies service registration, promotes modularity, and reduces tight coupling between components.
By mastering DI, developers can write cleaner, testable, and more maintainable ASP.NET Core applications — leading to better software design and scalability.