← Home
.NETDependency injectionReported by candidates

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
Make it yours · Navistar Financial payoff quotes (invented example)
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 .
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 repo Example 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

1/10Before: makes its own repository with , inside the class.

In plain words

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.

1/9A customer's app calls GET /api/loans/7. Routing picks to answer it.

The story

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.
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.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

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
  1. 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.
  2. 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.
  3. , 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.
  4. 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.

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
  1. dotnet new webapi -n NewLab. Add a class Clock with a Name set in its constructor, and register AddSingleton(new Clock("registered")).
  2. 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.
  3. 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."

Sources Sourced: 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

Traps

What should you say instead? Tap to check.

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.

Reported at PwC (.NET developer): "Write DI syntax on notepad"
Stuck? Hints
  1. Start with the interface: one method, no body.
  2. The registration is one line in Program.cs: interface first, class second.
  3. 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.

Question index: the CRUD API live task (Must, medium) checks this card; same steps as Microsoft Learn's DI guide
You are given
Lending.sln · where this file sits
  • src/Application/Servicing/Loans/
Stuck? Hints
  1. Pull an interface out of with just the method uses.
  2. Move the out: the constructor takes the interface instead.
  3. 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 .
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.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked