ASP.NET Core  

Dependency Injection in .NET Core Applications

Introduction

When I started working on real-world ASP.NET Core projects, one mistake I repeatedly made was creating objects directly inside controllers. At first, this felt simple and fast. But as the application grew, the code became tightly coupled, difficult to test, and painful to maintain.

That’s when I truly understood the importance of Dependency Injection (DI).

ASP.NET Core provides built-in support for DI, and once you start using it correctly, it completely changes how you structure applications. In this article, I’ll explain Dependency Injection from a practical developer’s perspective, using a simple logging example that I’ve personally used while debugging APIs and tracking requests.

What is Dependency Injection (In Simple Terms)

Dependency Injection is a design pattern where a class does not create the objects it depends on.
Instead, those objects are provided externally.

Without Dependency Injection

public class HomeController : ControllerBase
{
    private ConsoleLoggerService _logger = new ConsoleLoggerService();
}

Problems I faced with this approach:

  • Controller is tightly coupled to one implementation

  • Hard to replace logger later

  • Unit testing becomes difficult

With Dependency Injection

public class HomeController : ControllerBase
{
    private readonly ILoggerService _logger;

    public HomeController(ILoggerService logger)
    {
        _logger = logger;
    }
}

Now:

  • Controller depends on an interface

  • Implementation can change anytime

  • Code becomes clean and testable

Real-Time Scenario: Logging in an ASP.NET Core Application

In almost every project I’ve worked on, logging is mandatory:

  • API request tracking

  • Error debugging

  • Auditing user actions

Instead of hardcoding logging logic everywhere, DI allows us to centralize and inject it.

Step 1: Define a Logging Interface

I always start with an interface. This gives flexibility from day one.

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

Why interface first?

  • Allows multiple implementations

  • Makes unit testing easier

  • Follows clean architecture principles

Step 2: Implement the Service

For this demo, I’m using a simple console logger.

public class ConsoleLoggerService : ILoggerService
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

In real projects, I later replace this with:

  • File logging

  • Database logging

  • Serilog / NLog

Without touching controller code.

Step 3: Register the Service in Program.cs

This is where ASP.NET Core’s built-in DI container comes into play.

var builder = WebApplication.CreateBuilder(args);

// Register service
builder.Services.AddScoped<ILoggerService, ConsoleLoggerService>();

var app = builder.Build();

app.MapControllers();
app.Run();

Understanding Service Lifetimes (From Experience)

  • AddSingleton
    One instance for entire application
    Useful for configuration or caching

  • AddScoped
    One instance per HTTP request
    Best choice for most services (used here)

  • AddTransient
    New instance every time
    Useful for lightweight, stateless services

I generally start with Scoped unless there’s a clear reason otherwise.

Step 4: Inject the Service into a Controller

[ApiController]
[Route("api/[controller]")]
public class HomeController : ControllerBase
{
    private readonly ILoggerService _logger;

    public HomeController(ILoggerService logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Get()
    {
        _logger.Log("HomeController GET method called");
        return Ok("Hello from Dependency Injection!");
    }
}

Key learning:

  • Controller does not know how logging works

  • It only knows that logging exists

  • DI container handles object creation automatically

Example in Action

When a request hits:

GET /api/home

What happens internally:

  1. ASP.NET Core creates HomeController

  2. DI container injects ConsoleLoggerService

  3. _logger.Log() executes

  4. Message appears in console

Later, I can replace ConsoleLoggerService with FileLoggerService without changing the controller at all.

Why Dependency Injection Matters (From Real Projects)

From my experience, DI gives:

  • Flexibility – Swap implementations easily

  • Testability – Mock services in unit tests

  • Maintainability – Clean, readable code

  • Scalability – Easier to grow applications

Once I adopted DI properly, debugging and refactoring became significantly easier.

Conclusion

Dependency Injection is not just a concept — it’s a core foundation of ASP.NET Core applications. By using DI correctly, you write code that is clean, modular, and future-proof.

The logging example shown here is simple, but the same pattern applies to:

  • Database access

  • Authentication

  • Email services

  • External APIs

Understanding and using DI effectively has improved the quality of my projects, and I strongly recommend mastering it early in your ASP.NET Core journey.