Expert · Web APIs with ASP.NET Core and EF Core · topic 33 of 43 · 30 min
Dependency injection and service lifetimes
Explanation
The problem DI solves
If OrderService writes var sms = new SmsSender(); inside itself, it is welded to that one class. You cannot switch providers, and in tests you cannot replace it with a fake that does not send real SMS. With DI, OrderService just says "I need an ISmsSender" in its constructor, and whoever creates OrderService decides which one to give. You already saw this idea with interfaces in module 5.
Registering and receiving services
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPriceCalculator, GstPriceCalculator>();
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
app.MapPost("/orders", (CreateOrder req, IOrderService orders) =>
Results.Ok(orders.Place(req)));
app.Run();
public interface IPriceCalculator { decimal WithTax(decimal amount); }
public class GstPriceCalculator : IPriceCalculator
{
public decimal WithTax(decimal amount) => amount * 1.18m;
}
public interface IOrderService { object Place(CreateOrder req); }
public class OrderService : IOrderService
{
private readonly IPriceCalculator _calc;
public OrderService(IPriceCalculator calc) => _calc = calc; // constructor injection
public object Place(CreateOrder req) => new { req.Item, Total = _calc.WithTax(req.Amount) };
}
public record CreateOrder(string Item, decimal Amount);The three lifetimes
| Lifetime | One instance per... | Typical use |
|---|---|---|
| AddSingleton | Whole application, for as long as it runs | Stateless helpers, caches, configuration objects; must be thread-safe |
| AddScoped | HTTP request (shared within that request) | DbContext, unit-of-work, per-request services |
| AddTransient | Every time it is asked for | Light, stateless services |
builder.Services.AddSingleton<Counter>();
builder.Services.AddScoped<RequestInfo>();
builder.Services.AddTransient<Stamp>();
public class Counter { private int _n; public int Next() => Interlocked.Increment(ref _n); }
public class RequestInfo { public Guid Id { get; } = Guid.NewGuid(); }
public class Stamp { public DateTime Created { get; } = DateTime.UtcNow; }The captive dependency trap
A service must not depend on a service with a SHORTER lifetime. If a singleton takes a scoped DbContext in its constructor, that one DbContext is captured and reused by every request forever, which causes wrong data and threading errors. In the Development environment, ASP.NET Core checks this and throws an error at startup saying it cannot consume a scoped service from a singleton. Fix it by making the outer service scoped too.
| Outer service | May depend on |
|---|---|
| Singleton | Singleton only |
| Scoped | Scoped or Singleton |
| Transient | Anything |
Controllers and primary constructors
[ApiController]
[Route("api/orders")]
public class OrdersController(IOrderService orders, ILogger<OrdersController> logger) : ControllerBase
{
[HttpPost]
public IActionResult Place(CreateOrder req)
{
logger.LogInformation("Placing order for {Item}", req.Item);
return Ok(orders.Place(req));
}
}Common mistakes and fixes
- "Unable to resolve service for type IOrderService": you forgot to register it. Add builder.Services.AddScoped<IOrderService, OrderService>();.
- Registering after builder.Build(): registrations must happen before Build.
- Using new for services inside other services: you lose DI benefits. Ask for them in the constructor.
- Storing per-user data in a singleton: all users share it. Use scoped services or a database.
- Huge constructors with ten dependencies: a sign the class does too much. Split it.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Dependency injection and service lifetimes · 1/7
In this lesson you will let ASP.NET Core create and hand out your services: dependency injection.
Prefer a video by a teacher? Find free videos on “ASP.NET Core dependency injection singleton scoped transient explained” on YouTube ↗
In real life: Swapping a real SMS gateway for a fake in development
A clinic booking API sends appointment SMS through a paid gateway. In development, nobody wants real SMS going to patients, so Program.cs registers a fake sender that just logs. Production registers the real one. The booking code is identical in both.
if (builder.Environment.IsDevelopment())
builder.Services.AddSingleton<ISmsSender, LogOnlySmsSender>();
else
builder.Services.AddSingleton<ISmsSender, GatewaySmsSender>();
builder.Services.AddScoped<IBookingService, BookingService>();
public interface ISmsSender { Task SendAsync(string phone, string text); }
public class LogOnlySmsSender(ILogger<LogOnlySmsSender> log) : ISmsSender
{
public Task SendAsync(string phone, string text)
{
log.LogInformation("FAKE SMS to {Phone}: {Text}", phone, text);
return Task.CompletedTask;
}
}Practice (0/4 done)
Which lifetime gives one instance per HTTP request?
A singleton service takes a scoped DbContext in its constructor. What is the problem?
You get "Unable to resolve service for type IEmailSender". What is the most likely fix?
In a webapi project, create an IGreetingService with string Greet(string name), and two implementations: HindiGreeting ("Namaste, name") and EnglishGreeting ("Hello, name"). Register one in Program.cs, inject it into GET /greet/{name}, then switch the registration and confirm the endpoint changes without touching the endpoint code. Also register a singleton counter and return how many greetings were served.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Configuration, secrets and the middleware pipelineWeb APIs with ASP.NET Core and EF Core · same module
- Spring and dependency injectionJava Programming (Core to Spring Boot) · Spring Boot basics
- Dependency injection with HiltKotlin and Android App Development · Navigation and app architecture
- The C# and .NET interview questions asked most oftenInterviews, portfolio and careers
- LifetimesRust Programming · Borrowing, slices and lifetimes
- Your portfolio project: a tested Web API with EF CoreInterviews, portfolio and careers
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is dependency injection?
Answer: Giving a class its dependencies from outside instead of it creating them
Dependencies are supplied, usually via the constructor.
Which lifetime is usual for an EF Core DbContext in ASP.NET Core?
Answer: Scoped
AddDbContext registers it as scoped: one per request.
Why must singleton services be thread-safe?
Answer: Many requests use the same instance at the same time
One shared instance serves concurrent requests.
A main benefit of DI for testing is...
Answer: You can pass fake implementations of interfaces into the class under test
Fakes replace real email, SMS or database services.
Which can a scoped service safely depend on?
Answer: Scoped or singleton services
A dependency should live at least as long as the service using it.
Which injection style is recommended?Related
Answer: Constructor injection
Constructor injection gives final fields and easy tests.
What does @Inject constructor do?Related
Answer: Tells Hilt how to create the class
Hilt calls it with the needed dependencies.
const vs readonly: which can be set from a value calculated at run time in the constructor?Related
Answer: readonly
const must be a compile-time literal.
Since which version does a new ASP.NET Core app start from Program.cs without Startup.cs?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Web APIs with ASP.NET Core and EF Core” before its exam: Configuration, secrets and the middleware pipeline, Entity Framework Core: databases with C#.
- Module 8 exam: Web APIs with ASP.NET Core and EF Core · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Testing, tooling and Unity. 43 topics left in the course before the final exam.
← Previous: Routing, model binding, validation and status codes