ASP.NET Core  

Dependency Injection (DI) Built-In in ASP.NET Core

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:

  1. Transient – Created each time they are requested.

  2. Scoped – Created once per request.

  3. 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.

LifetimeDescriptionExample Use Case
TransientCreated every time requestedLightweight, stateless services like helpers
ScopedOne instance per HTTP requestDatabase contexts or unit of work
SingletonOne instance for the whole appConfiguration 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

  1. 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!");
          
        
  2. Loose Coupling
    Classes depend on abstractions, not concrete implementations.

  3. Centralized Configuration
    All services are registered in one place ( Program.cs ).

  4. Extensibility
    You can easily switch implementations without modifying dependent code.

  5. 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.