CodeToClarity Logo
Published on ·.NET

Dependency Injection in ASP.NET Core (For Absolute Beginners)

Kishan KumarKishan Kumar

Learn the fundamentals of Dependency Injection in ASP.NET Core. A beginner-friendly guide to DI with real examples and best practices.

If you’ve ever built a .NET application, you’ve probably heard of Dependency Injection (DI) — one of those concepts that everyone talks about but not everyone truly understands.

The good news? Once you “get” it, DI makes your code cleaner, easier to test, and far more maintainable. It’s not just a pattern — in ASP.NET Core, it’s a built-in feature and the backbone of how the framework works.

Let’s break it down in simple terms 👇


💡 What Is Dependency Injection?

Dependency Injection is a design pattern that provides an object (class) with everything it needs (its dependencies) instead of making it create them on its own.

Think of it like this:

Imagine you’re running a restaurant. Your chef shouldn’t have to build the oven or grow the vegetables — they should just be able to cook using what’s provided.

That’s exactly what DI does for your classes. It provides everything they need so they can just “cook” (do their job).

Without DI, your class ends up creating and managing its own dependencies — making it tightly coupled and hard to test.


❌ Without DI

public class NotificationService
{
    private readonly EmailService _emailService = new();

    public void Send() => _emailService.SendEmail();
}

Problems:

  • Hard to test (you can’t replace EmailService easily)
  • Tightly coupled (you can’t switch implementations without editing the class)
  • Violates clean architecture principles

✅ With DI

public class NotificationService(IEmailService emailService)
{
    public void Send() => emailService.SendEmail();
}

Benefits:

  • Depends on an interface, not a specific class
  • The DI container automatically injects the right implementation
  • Easy to mock and unit test

⚙️ How ASP.NET Core Handles DI

ASP.NET Core comes with built-in Dependency Injection — no need for external libraries.
When your app starts, it sets up a DI container, and every service you register there can be injected automatically wherever it’s needed.

You register your dependencies in Program.cs like this:

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<INotificationService, EmailNotificationService>();
builder.Services.AddTransient<IPaymentService, StripePaymentService>();

Now, whether you’re using Controllers, Minimal APIs, or Background Services, you can simply ask for what you need — ASP.NET Core takes care of the rest.


🧩 Types of Dependency Injection

ASP.NET Core supports three main types of DI:

Most common and cleanest approach:

public class ReportService(ILogger<ReportService> logger)
{
    public void Generate() => logger.LogInformation("Generating report...");
}

2. Method Injection

Useful in Minimal APIs or endpoint handlers:

app.MapGet("/reports", (IReportService reportService) =>
{
    var report = reportService.Generate();
    return Results.Ok(report);
});

Sets dependencies through public properties — less clean and harder to test.


🏗 Registering Dependencies in Program.cs

All service registrations happen inside the Program.cs file:

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();

To keep things tidy, use extension methods:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        services.AddScoped<IOrderService, OrderService>();
        services.AddScoped<ICustomerService, CustomerService>();
        return services;
    }
}

And then in your Program.cs:

builder.Services.AddApplicationServices();

🧱 Framework Services vs. Application Services

ASP.NET Core’s DI container treats both the same way.

Framework Services (already available)

  • ILogger<T>
  • IConfiguration
  • IOptions<T>
  • IWebHostEnvironment

You can inject them directly:

public class AuditService(ILogger<AuditService> logger, IConfiguration config)
{
    public void Log() => logger.LogInformation($"Env: {config["ASPNETCORE_ENVIRONMENT"]}");
}

Application Services (your custom logic)

You define these yourself:

builder.Services.AddScoped<IUserService, UserService>();

And use them in controllers:

public class UserController(IUserService userService)
{
    [HttpGet("{id}")]
    public IActionResult GetUser(int id) => Ok(userService.GetById(id));
}

🔄 Dependency Inversion in Clean Architecture

One of the SOLID principles — the Dependency Inversion Principle — says:

“High-level modules should not depend on low-level modules. Both should depend on abstractions.”

This means your core logic should depend on interfaces, not implementations.

For example:

public interface IEmailService
{
    void Send(string to, string subject, string body);
}

public class NotificationService(IEmailService emailService)
{
    public void SendWelcomeEmail(string to)
    {
        emailService.Send(to, "Welcome!", "Thanks for signing up!");
    }
}

Want to switch from SMTP to SendGrid?
Just change one line in Program.cs:

builder.Services.AddScoped<IEmailService, SendGridEmailService>();

No code changes needed elsewhere — that’s the beauty of DI.


⚠️ Common DI Mistakes to Avoid

  1. Using IServiceProvider directly

    var repo = provider.GetService<IRepository>(); // ❌ Avoid this
    

    → Hides dependencies and makes testing harder.

  2. Too many constructor parameters If your class needs 8 services, break it into smaller ones.

  3. Forgetting to register services You’ll get runtime errors like:
    InvalidOperationException: Unable to resolve service for type 'IEmailService'

  4. Mixing lifetimes improperly Don’t inject a scoped service into a singleton (covered in next article).

  5. Over-abstracting Not everything needs an interface — use them where flexibility or testability matters.


🧠 When NOT to Use DI

Sometimes, skipping DI is perfectly fine:

  • Static helpers (e.g., SlugGenerator.Generate())
  • Startup logic (builder.Environment, builder.Configuration)
  • Short-lived or value-type objects (like models)
  • Very small apps where abstraction adds more complexity than value

Use DI where it adds clarity, not clutter.


🏁 Wrap-Up

Dependency Injection isn’t just another pattern — it’s the heart of ASP.NET Core.

It helps you:

  • Write clean, testable, and modular code
  • Keep components loosely coupled
  • Swap implementations with zero refactoring
  • Scale your app effortlessly

Whether you’re building APIs, web apps, or background services — embracing DI is how you build software that lasts.