What is dependency injection, what problem does it solve, and how do you register and inject a service?
The most reported .NET API question in the index: Wipro, Infosys, Capgemini, PwC, Synechron, Deloitte, Disney and Neudesic.
Say this first · about 30 seconds
Dependency injection means a class doesn't build the objects it needs, its dependencies. It lists them in its , and something outside passes them in.
The problem it solves: if writes new LoanRepository() inside itself, I can't test it without a real database, and I can't swap the repository without editing the class.
Register: in Program.cs, AddScoped<ILoanRepository, LoanRepository>() tells the : when someone asks for this , build this class. Scoped means one per request; the has the rest.
Inject: takes in its constructor and keeps it in a field. For each request the container builds the chain from the bottom: the database context, then the repository, the service and the controller.
What I get: tests with fakes, one-line swaps, and classes that depend on interfaces, not on each other. That is loose coupling.
Make it yours · Navistar Financial payoff quotes (invented example)
Situation: at Navistar Financial, the dealer portal shows what it costs to pay off a truck loan today. gets the amount from FIS, then adds our fee. It used to build its FIS client with inside, so every test called FIS's test system and failed whenever that system was down.
What I did: now takes in its constructor. Program.cs registers the real client; the unit test passes a fake that returns a fixed amount.
Result: the fee test runs in milliseconds with no network, and an FIS test outage no longer turns our build red.
What I'd say: DI let me test our own rule, the fee, without FIS. The class asks for what it needs; the app and the test each decide what to give it.
The code, simplified
Invented to show the shape, in C#. Tap Next: first the app wires the real client, then the test passes a fake.
Navistar.sln · where these 4 files sit
src/
Api/
Application/Payoffs/
tests/Payoffs/
Diagram · step 1In the app, Program.cs registers the real FIS client and .
Appcontainer
Testby hand
PayoffService
FIS clientreal/fake
register 2 services
Program.cs · line 4 · 1 of 2Registers the real FIS client. is the registration for a class that calls another system over HTTP: the container builds it and passes it a ready .
Fix later in your repoExample written for this card: invented, not from a real codebase.
Say it out loud, then check
Show the 30-second answer
Dependency injection means a class doesn't build the objects it needs, its dependencies. It lists them in its , and something outside passes them in.
The problem it solves: if writes new LoanRepository() inside itself, I can't test it without a real database, and I can't swap the repository without editing the class.
Register: in Program.cs, AddScoped<ILoanRepository, LoanRepository>() tells the : when someone asks for this , build this class. Scoped means one per request; the has the rest.
Inject: takes in its constructor and keeps it in a field. For each request the container builds the chain from the bottom: the database context, then the repository, the service and the controller.
What I get: tests with fakes, one-line swaps, and classes that depend on interfaces, not on each other. That is loose coupling.
Check your answer: tick what you said
You said 0 of 8
Then add your example: Navistar Financial payoff quotes (invented example)
Situation: at Navistar Financial, the dealer portal shows what it costs to pay off a truck loan today. gets the amount from FIS, then adds our fee. It used to build its FIS client with inside, so every test called FIS's test system and failed whenever that system was down.
What I did: now takes in its constructor. Program.cs registers the real client; the unit test passes a fake that returns a fixed amount.
Result: the fee test runs in milliseconds with no network, and an FIS test outage no longer turns our build red.
What I'd say: DI let me test our own rule, the fee, without FIS. The class asks for what it needs; the app and the test each decide what to give it.
See it
LoansController
LoanService
LoanRepository
1/10Before: makes its own repository with , inside the class.And makes its own , which talks to the real SQL database.A unit test wants to check alone. It can't: every call reaches the real database.A fake repository? There is nowhere to hand one in. To change it you must edit itself.Fix: only lists in its . No .Program.cs it once: when asked for , build .A request comes in. The builds the parts first: the context, then the repository.Then it passes them in: the repository into , the service into the controller.In a test you pass a into the constructor yourself. No database.Constructors list needs. Program.cs says what to build. The container builds and passes it in.
In plain words
A like needs other objects to do its job, such as a repository that loads loans. Those are its dependencies.
Without DI, it builds them itself with . It is like a car with the engine welded in: it runs, but you can't test it with a test engine or fit a better one.
With DI, the class only says what it needs, in its . The builds the parts and fits them in, like a mechanic.
Program.cs is the mechanic's list: which part goes where. Change one line there, and every class that needs that part gets the new one.
The senior answer · about 2 minutes
What it is
Dependency injection means a class gets the objects it depends on from outside, usually through its constructor, instead of creating them itself with .
It is one form of inversion of control. My class no longer controls how its parts are built; the that comes with ASP.NET Core does that.
In our BMW FS lending API, takes an in its constructor. It doesn't know whether that is the SQL repository or a test fake, and it doesn't need to.
Why we use it
First, testing. Because the repository comes in through the constructor, a unit test passes a fake and checks the service's own logic in milliseconds, with no database.
Second, change. When BMW FS moved our database login to a , a login with no password, we changed one registration line at start-up, and every class that receives the database context got the new connection.
Third, the container owns lifetimes and clean-up. It decides when to reuse an object and disposes it when the request ends. The covers that.
How I register and inject
In Program.cs I map each interface to a class with , or , and use for EF Core. Registering builds nothing; it only writes the list.
Classes take what they need as constructor parameters and keep them in fields, so nothing can replace them halfway through.
For controllers, ASP.NET Core builds a new one for every request, after routing picks the action, and fills its constructor from that request's .
In production
I turn on , so a missing registration stops the app at start-up instead of failing on a customer's first request. It only checks registered services, so we also have a test that starts the app with and calls each controller once.
In code review I look for classes created by hand that are also registered with the container, and for business classes that take the container itself, , and fetch things from it. Both hide a dependency, and the first one caused a real outage for us: the incident below.
If a constructor grows past five or six parameters, the class is doing too much. I split it rather than hide the dependencies.
One request, start to end
Every idea on this card in one flow: routing, the scope, registrations, building bottom-up, injecting, running, and clean-up.
ASP.NET Corerouting, scope
Containerregistrations
Our classescontroller → SQL
GET /api/loans/7
1/9A customer's app calls GET /api/loans/7. Routing picks to answer it.ASP.NET Core opens a for this request and asks the container for a .The controller's constructor needs . The registration list says: build . needs . That needs . The context needs nothing more.Now it builds from the bottom up. First the context, with the connection string from Program.cs.Then the repository gets that context, the service gets the repository, the controller the service.The graph is complete. The container hands the controller back to ASP.NET Core. runs: controller → service → repository → database. 200 with the loan.Response sent. The scope ends and the context. The next request builds a fresh set.
The story
Before
In older code, and in many .NET Framework apps, each class built what it needed. held new LoanRepository(), and the repository held new ServicingDbContext(). Each class quietly chose its own parts.
Why it had to change
Nobody could test without a real database. Changing the repository, or even its connection string, meant finding and editing every , and missing one meant a bug.
How it works now
Now each class lists its needs in its constructor, and the container builds the whole chain for every request. The storyboard follows one request from start to end.
What happens next
Program.cs is the one place that says which class fills each need. The controller, the service and the repository never call on each other.
What we gained
Unit tests with fakes. One-line swaps. Needs you can read from the constructor. Lifetimes and disposal handled by the container. And checks the wiring before the app takes traffic.
In the lending API
after the fix. It never builds its repository; it receives one in its .
Only the key lines are shown; Deep walks the whole file.
Lending.sln · where this file sits
src/Application/Servicing/Loans/
⋯ 1 line in Deep⋯ 4 lines in Deep
Service · After · LoanService.cs · line 5 · 1 of 4A field of the interface type, not the class. means only the constructor can set it, so the repository can't be swapped or set to null halfway through.
The code, file by file
Slice 1 of the lending API: one request for one loan, from Program.cs down to the repository, then the unit test DI makes possible. Before tabs first, then the fixed files.
Diagram · step 1Before: makes its own repository with , inside the class.
LoansController
LoanService
LoanRepository
Service · Before · LoanService.cs · line 6 · 1 of 1The problem line. builds its own repository when it is created. Nobody outside can choose a different one, not even a test.
What they ask next
The follow-ups interviews really ask. Answer out loud, then tap to check.
ReportedHow do you implement DI in a controller?
Constructor injection. The controller takes in its constructor and keeps it in a field. Program.cs registers , and turns on controllers. I never write for the controller or the service.
public LoansController(ILoanService loans)
=> _loans = loans;
ReportedWhy is it better to inject dependencies instead of creating them?
Four reasons. I can unit test a class by passing in fakes. I can swap an implementation by changing one line in Program.cs. A class's needs are visible in its constructor. And the container handles lifetimes and disposal, so no class has to.
ReportedWhy do we register services in Program.cs?
Program.cs runs once at start-up and is the one place where the app is wired together. People call it the composition root. Registering only writes the list; builder.Build() turns it into the container, and objects are built later, when a request asks for them.
Answer, then open the next level. That is the order an interviewer digs in.
Reporteda candidate was asked Intervieweran interviewer asks it
ReportedHow do you implement DI in a controller?
Constructor injection. The controller takes in its constructor and keeps it in a field. Program.cs registers , and turns on controllers. I never write for the controller or the service.
public LoansController(ILoanService loans)
=> _loans = loans;
Once per request, after routing has picked the action. ASP.NET Core builds it with that request's , so it gets that request's scoped services, and it is thrown away when the request ends. Controllers are not in the container's list by default; puts them there. The storyboard shows each step.
ReportedWhat are the different ways to inject a service?
Constructor injection is the default. Method injection: one action asks for a service with [FromServices], or a minimal API handler simply lists it as a parameter. Middleware gets per-request services as parameters. You can also ask by hand, but that hides the need, so it is a last resort. The built-in container has no property injection; Autofac adds it.
ReportedWhy is it better to inject dependencies instead of creating them?
Four reasons. I can unit test a class by passing in fakes. I can swap an implementation by changing one line in Program.cs. A class's needs are visible in its constructor. And the container handles lifetimes and disposal, so no class has to.
ReportedWhat is IoC, and how is it different from DI?
Inversion of control is the general idea: your class stops controlling something, here how its parts are built, and a framework does it instead. DI is one way to do that. The Dependency Inversion Principle, the D in SOLID, is a design rule: high-level code like depends on an abstraction like , not on the SQL class. DI is how that abstraction gets supplied.
Isn't passing in and asking it for things the same?
No. That is the service locator pattern: the constructor no longer shows what the class needs, and a missing registration fails deep inside a method. The shows that bug.
Is DI inheritance or composition?
Composition. has a repository; it is not a kind of repository. DI is composition where someone else picks and passes in the part: see .
ReportedWhy do we register services in Program.cs?
Program.cs runs once at start-up and is the one place where the app is wired together. People call it the composition root. Registering only writes the list; builder.Build() turns it into the container, and objects are built later, when a request asks for them.
It still compiles, then fails at run time with "Unable to resolve service for type…". See the first production bug below; moves the failure to start-up.
A constructor needs too many dependencies. Which SOLID principle is broken, and why is preferred?
Too many parameters, say eight, usually means the class has several jobs: that breaks the Single Responsibility Principle: one class, one job. I split it by job, for example payments and statements into their own services. DI didn't cause the problem; it made it visible. means the field can only be set in the constructor, so nobody can replace or null the dependency later.
ASP.NET MVC 5 and Web API 2 had no container built in. Teams added Autofac, Unity or Ninject and plugged it in through . ASP.NET Core has one built in from the start, and the framework itself (logging, options, EF Core) uses it. You can still plug Autofac in when you need extras like property injection or modules.
Production bugs, and how to tell them
Each one: what you see, why, the fix in code, and the line to say.
Unable to resolve service: a registration line is missing
$ curl https://localhost:5001/api/loans/7
HTTP/1.1 500 Internal Server Error
System.InvalidOperationException: Unable to resolve service for type 'Lending.Application.Servicing.Loans.ILoanRepository' while attempting to activate 'Lending.Application.Servicing.Loans.LoanService'.
Cause
asks for in its constructor, but Program.cs has no line for it. It compiles, because the list is only read when the app runs, never by the compiler.
Fix
Add AddScoped<ILoanRepository, LoanRepository>(), and turn on so the next missing line stops the app at start-up instead of on a customer's request.
Before · Program.cs · line 7 · 1 of 2 is registered, but what it needs is not. Nothing complains until a request asks for it.
Say it
The container only knows what Program.cs tells it. A missing registration compiles and fails at run time, on the first request that needs it. We switch on ValidateOnBuild, which checks registered services at start-up, and a test starts the app with WebApplicationFactory, so it fails in the pipeline, not in production.
read inside the method. Time was a dependency the class never listed, so the test couldn't control it.
Fix
Inject . The app registers ; the test passes a set to a fixed date.
Diagram · step 2 reads the real clock inside the method. The test can't set the date.
Test: not overdue
Oct: pass
LoanServiceIsOverdue
DateTime.UtcNowthe real clock
Before · LoanService.cs · line 3 · 1 of 1The real clock, read inside the method. Nothing outside can change it.
Say it
Anything a class reads from outside is a dependency, including the clock. When a test depends on today's date, I inject , so the test sets the time and gives the same result every day.
Production incident: Statements broke when the old database login was switched off
P2 · serious, has a workaroundBMW FSa wrong settingLending API: statements endpoint
Composed, not a real event at BMW FS. Every mechanism is real: a class that builds its own DbContext with new never gets the connection that Program.cs registers.
The story in 5 lines
BMW FS was moving the lending API from a SQL password login to a . Our release two weeks earlier changed the line in Program.cs, and everything kept working.
Then the BMW FS disabled the old password login, as planned. Within minutes, every monthly statement download failed with "Login failed". Every other endpoint was fine.
, written years before, built its own context with new ServicingDbContext(). It never came from the container, so it never got the new connection. It still used the old login.
The re-enabled the old login (the change's step). We injected the context, deleted the constructor that allowed , and added a test that fails if any class still uses the old connection.
Who's who (5 people)
You
.NET developer on the vendor team (L3). Finds the cause, writes the fix.
Tech lead
Vendor senior. Runs the bridge call, reviews every change.
L2 pod
Vendor support in Navi Mumbai. First look, runbooks, alerts.
BMW FS DBA
Owns Azure SQL logins. Ran the planned change that set this off.
BMW FS IT manager
Approves changes and the rollback of theirs.
Tap a step. The map lights the place where it happens.
Customer portalBMW FS, downloads PDFs
Lending APIApp Service, our .NET code
StatementsStatementService: new DbContext
Azure SQLlending database
Key Vaultold password secret
BMW FS DBAowns logins
Alert → L2 podNavi Mumbai
App Insightsrequests, KQL
You + tech leadL3: our code
Azure DevOpstests, release, slots
1/8 Detect
Statement downloads start failing. An alert on the failure rate pages our .
Tue 14 Oct, 08:12 ET = 17:42 ISTAzure Monitor alert, then L2
At 08:00 ET the BMW FS disables the old SQL login svc_lending, step 3 of their managed-identity . Nobody tells us it is today; the change was theirs.
Customers on the BMW FS portal get "Statement unavailable". At 08:12 the alert on the statements endpoint's failure rate pages the .
follows the : no deploy today, so they pull you in as L3.
The alert (Azure Monitor)
Sev 2 · lending-api-prod
Failed requests on GET /api/statements/*
> 5% over 5 minutes (now: 100%)
Fired 08:12 ET · 312 failures
In theory
A change to logins that our app uses comes with a heads-up and a check of who still uses the old login.
In practice
The listed the API, but the check only asked "is the new connection in Program.cs?" It was. Nobody looked for code that built its own connection.
Try it yourself · 15 minutes, on your Mac
dotnet new webapi -n NewLab. Add a class Clock with a Name set in its constructor, and register AddSingleton(new Clock("registered")).
Add a second class that takes Clock in its constructor, and a third that does new Clock("hand-made") itself. Return both names from one endpoint.
Change the registration to "registered v2" and run again. Only the injected one changes. That is this incident in miniature.
You should see
The registration change reaches every injected class and none of the hand-built ones.
The senior move
"DI gives me one place to change wiring, but only if nothing builds its own parts. When we change a connection or swap an implementation, I first search for anyone creating that class by hand with the new keyword, and I delete constructors that let people skip the container."
SourcesSourced: the SqlClient login-failed error and managed-identity connection strings (Microsoft.Data.SqlClient docs), and (EF Core DbContext configuration docs), sys.dm_exec_sessions (SQL Server docs), ( docs). Composed: BMW FS's systems, every date, number and name, and the (standard columns).
Traps
Don't sayDI means using a container.SayDI is passing a class its dependencies from outside. The container only automates it: new LoanService(fakeRepo) in a test is DI too.
Don't sayYou need an interface for everything you inject.SayThe container injects classes too, like or . I add an interface where I want to swap the implementation.
Don't sayRegistering a service in Program.cs creates it.SayRegistering writes one row in a list. The object is built later, when something asks for it.
Don't sayI inject and get what I need.SayThat is a service locator: it hides the class's needs and fails late. Every need goes in the constructor.
Don't sayDI is only for testing.SayTesting is one gain. The others are one place to swap implementations and the container managing lifetimes and disposal.
Traps
What should you say instead? Tap to check.
Don't sayDI means using a container.
Say instead
DI is passing a class its dependencies from outside. The container only automates it: new LoanService(fakeRepo) in a test is DI too.
Don't sayYou need an interface for everything you inject.
Say instead
The container injects classes too, like or . I add an interface where I want to swap the implementation.
Don't sayRegistering a service in Program.cs creates it.
Say instead
Registering writes one row in a list. The object is built later, when something asks for it.
Don't sayI inject and get what I need.
Say instead
That is a service locator: it hides the class's needs and fails late. Every need goes in the constructor.
Don't sayDI is only for testing.
Say instead
Testing is one gain. The others are one place to swap implementations and the container managing lifetimes and disposal.
Live coding
Easy first, then harder. Set a timer and type it before you look.
Task 1
Reportedeasy · 10 minutes
On a notepad, with no editor help: write an interface, a class that implements it, the line that registers it, and a controller that receives it through its constructor.
The registration is one line in Program.cs: interface first, class second.
The controller's constructor takes the interface, not the class.
Show a solution
Lending.sln · where these 4 files sit
src/
Application/
Api/
Controllers/
What the interviewer checks
The controller depends on the interface, not the class.
The registration maps interface to class: AddScoped<ILoanService, LoanService>().
The dependency is stored in a private readonly field.
No for the service anywhere in the controller.
You can say which lifetime you chose and why, in one line.
Task 2
Question index: Mustmedium · 20 minutes
You are given a that builds its own repository with . Refactor it to constructor injection, register it, and write a unit test that runs with no database.
Pull an interface out of with just the method uses.
Move the out: the constructor takes the interface instead.
In the test, write a tiny fake that implements the interface with a list.
Show a solution
Lending.sln · where these 4 files sit
src/
Application/Servicing/Loans/
Api/
tests/Lending.Tests/
Diagram · step 5Fix: only lists in its . No .
LoansController
LoanService
LoanRepository
ILoanRepository.cs · line 4 · 1 of 1Only what calls. Small interfaces are easy to fake.
What the interviewer checks
has only what uses.
takes it in the constructor; no left inside.
Program.cs registers both and .
The test builds with and a fake. No container, no database.
The test checks 's own rule (unknown id throws), not the repository.
Senior extra: switch on , and say how you'd find every other new LoanRepository() (search, then delete a parameterless constructor so stops compiling, as in the incident).
With AI tools
Copilot and similar tools often write new SomethingClient() inside a service, or pass the container itself () around to fetch things, a service locator, because it compiles. When I review AI-written code I check three things: every dependency is in the constructor, every one is registered, and the app still starts with on. DI also makes AI-written tests useful: the tool can generate a test with a fake repository only because the class takes an interface.