All articles

How to Implement Dependency Injection in .NET Core

Learn dependency injection in .NET with the built-in IoC container — register services in Program.cs, inject them through constructors, choose lifetimes, and swap implementations with one line.

0 · log in to like, save & follow Share on LinkedIn Share on X

Dependency injection is built into .NET — there is no extra package to install and no third-party container to learn. This tutorial shows why it matters and how to use it, by building a small products API where swapping a whole implementation takes exactly one line.

Dependency injection in .NET with the built-in IoC container

Why should you use dependency injection?

Assume you own GeekShop, an online store. Your website calls an ASP.NET Core Web API; the API controller uses a ProductService that returns product information.

Without dependency injection, the controller creates the service itself:

[HttpGet]
public List<Product> GetProducts()
{
    ProductService products = new ProductService();
    return products.GetProducts();
}

During the holiday season you run a three-day Prime Day sale. You write a new PrimeDayProductService with discounted prices — and now you have to edit the controller to use it, and edit it again when the sale ends. Every class that news up ProductService needs the same treatment. That is tight coupling: the controller depends on one concrete class and cannot work with any other.

Dependency injection turns this around. The controller declares what it needs (an interface), and the .NET IoC container decides which implementation it gets. Switching to the Prime Day service becomes a one-line change in Program.cs.

The IoC container

The Inversion of Control (IoC) container manages object creation, lifetime, and wiring. It has three jobs:

  • Registration — you map an interface to a class (IProductServiceProductService), so the container knows what to build.
  • Resolution — when a controller (or any registered class) asks for IProductService in its constructor, the container creates the right instance and passes it in.
  • Disposal — the container disposes instances according to the lifetime you chose, so you rarely write Dispose calls yourself.

Implement dependency injection step by step

Create the Web API project

dotnet new webapi --use-controllers -n GeeksShop.DI

In Visual Studio: File → New → Project → ASP.NET Core Web API, project name GeeksShop.DI. Delete the sample WeatherForecast files — we won't use them.

Add the Product model

Create a Models folder with Product.cs:

namespace GeeksShop.DI.Models;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal ListPrice { get; set; }
}

Define the contract — IProductService

The interface is what makes the swap possible: the controller will depend on this contract, never on a concrete class. Create Services/Interfaces/IProductService.cs:

using GeeksShop.DI.Models;

namespace GeeksShop.DI.Services.Interfaces;

public interface IProductService
{
    List<Product> GetProducts();
}

Implement the regular service

Services/ProductService.cs returns the everyday catalog:

using GeeksShop.DI.Models;
using GeeksShop.DI.Services.Interfaces;

namespace GeeksShop.DI.Services;

public class ProductService : IProductService
{
    public List<Product> GetProducts() =>
    [
        new() { Id = 1, Name = "Galaxy A13 Mobile", ListPrice = 100 },
        new() { Id = 2, Name = "Air Pods", ListPrice = 200 },
        new() { Id = 3, Name = "Pen drive", ListPrice = 300 },
    ];
}

Implement the Prime Day service

Services/PrimeDayProductService.cs implements the same interface with sale pricing:

using GeeksShop.DI.Models;
using GeeksShop.DI.Services.Interfaces;

namespace GeeksShop.DI.Services;

public class PrimeDayProductService : IProductService
{
    private const decimal PrimeDayDiscount = 10;

    public List<Product> GetProducts() =>
    [
        new() { Id = 4, Name = "Galaxy A15 Mobile", ListPrice = 400 - PrimeDayDiscount },
        new() { Id = 5, Name = "Laptop", ListPrice = 500 - PrimeDayDiscount },
    ];
}

Inject the service into the controller

The controller asks for IProductService in its constructor and never mentions a concrete class. With C# primary constructors this is one line of ceremony:

using GeeksShop.DI.Models;
using GeeksShop.DI.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace GeeksShop.DI.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ProductsController(IProductService products) : ControllerBase
{
    [HttpGet]
    public List<Product> GetProducts() => products.GetProducts();
}

If you run the app now you will hit:

InvalidOperationException: Unable to resolve service for type 'GeeksShop.DI.Services.Interfaces.IProductService' while attempting to activate 'ProductsController'.

The container does not know about IProductService yet — that is the registration step.

Register the dependency in Program.cs

using GeeksShop.DI.Services;
using GeeksShop.DI.Services.Interfaces;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<IProductService, ProductService>();

var app = builder.Build();

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

Run it and call the endpoint:

curl http://localhost:5000/api/products
[
  { "id": 1, "name": "Galaxy A13 Mobile", "listPrice": 100 },
  { "id": 2, "name": "Air Pods", "listPrice": 200 },
  { "id": 3, "name": "Pen drive", "listPrice": 300 }
]

Prime Day arrives — change one line

builder.Services.AddScoped<IProductService, PrimeDayProductService>();

Same controller, same endpoint, different behavior:

[
  { "id": 4, "name": "Galaxy A15 Mobile", "listPrice": 390 },
  { "id": 5, "name": "Laptop", "listPrice": 490 }
]

When the sale ends, change the line back. No controller edits, nothing else touched — that is the payoff of depending on the interface.

Choosing a service lifetime

AddScoped is one of three lifetimes the container offers:

  • AddTransient — a new instance every time the service is requested. Safest for lightweight, stateless services.
  • AddScoped — one instance per HTTP request. The right choice for services that touch a database (this is why AddDbContext registers your EF Core context as scoped).
  • AddSingleton — one instance for the whole application lifetime. Use for caches and configuration-style services, and make sure they are thread-safe.

The differences are easy to see in code — .NET dependency injection object lifetimes walks through them with examples.

Going further: keyed services

Since .NET 8 you can also register both implementations at once under names and pick one per consumer — handy when regular and sale pricing must coexist:

builder.Services.AddKeyedScoped<IProductService, ProductService>("regular");
builder.Services.AddKeyedScoped<IProductService, PrimeDayProductService>("primeday");
public class ProductsController(
    [FromKeyedServices("primeday")] IProductService products) : ControllerBase

For most applications the plain one-registration approach above is all you need — reach for keyed services only when two implementations genuinely have to live side by side.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.