C# Dependency Injection Explained With Real-World Examples
If you have worked with ASP.NET Core, you have probably seen code like this:
builder.Services.AddScoped<IOrderService, OrderService>();
and then:
public class OrdersController
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
}
It looks simple.
But what exactly is happening here?
Why should we use IOrderService instead of simply creating new OrderService()?
And why has Dependency Injection become such an important part of modern C# and .NET development?
Let's understand Dependency Injection from the ground up using real-world examples, not just definitions.
What Is Dependency Injection?
Dependency Injection, commonly called DI, is a design technique where an object receives the dependencies it needs from an external source instead of creating those dependencies itself.
That sounds complicated, so let's simplify it.
Imagine you have an OrderService that needs to send an email.
A simple implementation might look like this:
public class OrderService
{
public void PlaceOrder()
{
var emailService = new EmailService();
emailService.SendEmail(
"customer@example.com",
"Order placed successfully");
}
}
This code works.
But OrderService is now responsible for two things:
Placing orders
Creating and managing an email service
That creates a strong dependency between the two classes.
Instead, we can inject the dependency:
public class OrderService
{
private readonly IEmailService _emailService;
public OrderService(IEmailService emailService)
{
_emailService = emailService;
}
public void PlaceOrder()
{
_emailService.SendEmail(
"customer@example.com",
"Order placed successfully");
}
}
Now OrderService doesn't care how emails are sent.
It only knows that it has an IEmailService.
That's the core idea behind Dependency Injection.
A Real-World Analogy
Think about a restaurant.
A restaurant's chef needs ingredients.
The chef doesn't manufacture tomatoes, grow wheat, produce salt, and raise chickens.
Instead, those dependencies are supplied to the kitchen.
The chef simply says:
"I need tomatoes."
The supplier provides them.
Similarly, a C# class can say:
"I need an email service."
The Dependency Injection container provides it.
The class doesn't need to know how the dependency was created.
Without Dependency Injection
Let's start with a typical example.
Suppose we are building an e-commerce application.
We have:
public class OrderService
{
public void PlaceOrder(Order order)
{
var repository = new OrderRepository();
repository.Save(order);
}
}
At first glance, this looks perfectly reasonable.
But imagine that OrderRepository requires a database connection:
public class OrderRepository
{
private readonly SqlConnection _connection;
public OrderRepository()
{
_connection = new SqlConnection(
"Server=localhost;Database=Orders;");
}
public void Save(Order order)
{
// Save order
}
}
Now OrderService indirectly depends on:
OrderRepositorySQL Server
Connection configuration
This makes the code difficult to change and test.
The Problem With new
One of the biggest warning signs is excessive use of new inside business logic.
For example:
public class PaymentService
{
public void ProcessPayment(decimal amount)
{
var logger = new FileLogger();
var paymentGateway = new StripePaymentGateway();
logger.Log("Processing payment");
paymentGateway.Charge(amount);
}
}
What happens if we want to switch to another payment provider?
Maybe tomorrow we want:
Stripe
↓
PayPal
↓
Adyen
We would have to modify PaymentService.
That's the problem.
The business logic knows too much about infrastructure.
Introducing Interfaces
We can introduce abstractions.
public interface IPaymentGateway
{
void Charge(decimal amount);
}
Then create an implementation:
public class StripePaymentGateway : IPaymentGateway
{
public void Charge(decimal amount)
{
Console.WriteLine($"Charging {amount} using Stripe");
}
}
Now the service depends on the abstraction:
public class PaymentService
{
private readonly IPaymentGateway _paymentGateway;
public PaymentService(IPaymentGateway paymentGateway)
{
_paymentGateway = paymentGateway;
}
public void ProcessPayment(decimal amount)
{
_paymentGateway.Charge(amount);
}
}
Notice what changed.
PaymentService no longer knows about Stripe.
It only knows about:
IPaymentGateway
That's a huge improvement.
What Is Actually Being Injected?
In this constructor:
public PaymentService(IPaymentGateway paymentGateway)
{
_paymentGateway = paymentGateway;
}
IPaymentGateway is the dependency.
The PaymentService depends on it.
Instead of doing:
var paymentGateway = new StripePaymentGateway();
inside the class, we provide it from outside.
That's why it's called Dependency Injection.
The dependency is injected into the object.
Constructor Injection
Constructor injection is the most common form of Dependency Injection in modern C# applications.
Example:
public class CustomerService
{
private readonly ICustomerRepository _repository;
public CustomerService(ICustomerRepository repository)
{
_repository = repository;
}
public Customer GetCustomer(int id)
{
return _repository.GetById(id);
}
}
The dependency is provided through the constructor.
This has an important advantage:
The object cannot be created without satisfying its required dependency.
For example:
var service = new CustomerService(repository);
The dependency is explicit.
ASP.NET Core and Dependency Injection
This is where Dependency Injection becomes especially useful.
ASP.NET Core has a built-in Dependency Injection container.
For example:
builder.Services.AddScoped<IOrderService, OrderService>();
This tells ASP.NET Core:
Whenever something asks for
IOrderService, provide anOrderService.
Then a controller can simply request it:
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public IActionResult Create(Order order)
{
_orderService.PlaceOrder(order);
return Ok();
}
}
You never wrote:
new OrderService()
ASP.NET Core creates it for you.
How the Dependency Injection Container Works
Think of the DI container as a factory.
You register:
builder.Services.AddScoped<IOrderService, OrderService>();
Later, ASP.NET Core sees:
public OrdersController(IOrderService orderService)
It looks at the registration:
IOrderService
↓
OrderService
Then it creates:
OrderService
and supplies it to the controller.
The flow looks like this:
HTTP Request
↓
OrdersController
↓
IOrderService
↓
OrderService
↓
IOrderRepository
↓
Database
The DI container is responsible for wiring these dependencies together.
Registering Multiple Dependencies
Real applications rarely have only one dependency.
Consider:
public class OrderService : IOrderService
{
private readonly IOrderRepository _repository;
private readonly IPaymentGateway _paymentGateway;
private readonly IEmailService _emailService;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository repository,
IPaymentGateway paymentGateway,
IEmailService emailService,
ILogger<OrderService> logger)
{
_repository = repository;
_paymentGateway = paymentGateway;
_emailService = emailService;
_logger = logger;
}
}
The service has four dependencies.
We can register them:
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<IEmailService, EmailService>();
ASP.NET Core builds the dependency graph automatically.
Dependency Injection Lifetimes
One of the most important concepts in ASP.NET Core DI is service lifetime.
There are three commonly used lifetimes:
Transient
Scoped
Singleton
Let's understand them.
1. Transient
Register a transient service:
builder.Services.AddTransient<IEmailService, EmailService>();
A new instance is created each time the service is requested.
Think of it as:
Request A → EmailService #1
Request B → EmailService #2
Request C → EmailService #3
Transient is useful for lightweight, stateless services.
2. Scoped
Register:
builder.Services.AddScoped<IOrderService, OrderService>();
A scoped service generally has one instance per scope.
In an ASP.NET Core web application, a scope commonly corresponds to an HTTP request.
For example:
HTTP Request 1
↓
OrderService #1
HTTP Request 2
↓
OrderService #2
Scoped is commonly used for application services and database-related components.
For example:
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
3. Singleton
Register:
builder.Services.AddSingleton<ICacheService, CacheService>();
Only one instance is created for the application's lifetime.
Conceptually:
Application
↓
CacheService #1
↓
Used by many requests
Singletons can be useful for carefully designed shared, thread-safe, stateless services or application-wide caches.
But singleton services require careful design because the same instance can be accessed concurrently.
A Common DI Mistake
Consider:
builder.Services.AddSingleton<MyService>();
and:
public class MyService
{
private readonly DbContext _context;
public MyService(DbContext context)
{
_context = context;
}
}
This can create a lifetime mismatch if DbContext is registered as scoped.
A singleton should not directly depend on a scoped service.
The general rule is:
Don't make a longer-lived service depend directly on a shorter-lived service.
This is one of the most important DI concepts to understand in ASP.NET Core.
Dependency Injection Makes Testing Easier
This is one of the biggest practical benefits.
Suppose our service depends on:
IPaymentGateway
During production, we use:
StripePaymentGateway
During testing, we can use:
FakePaymentGateway
For example:
public class FakePaymentGateway : IPaymentGateway
{
public bool PaymentProcessed { get; private set; }
public void Charge(decimal amount)
{
PaymentProcessed = true;
}
}
Now we can test:
var gateway = new FakePaymentGateway();
var service = new PaymentService(gateway);
service.ProcessPayment(100);
Assert.True(gateway.PaymentProcessed);
No real payment provider.
No network call.
No credit card.
No external dependency.
That's the power of Dependency Injection.
Dependency Injection and Mocking
DI also works nicely with mocking frameworks.
For example, a test could provide a mocked repository:
var repository = new Mock<IOrderRepository>();
repository
.Setup(x => x.GetById(10))
.Returns(new Order
{
Id = 10
});
Then:
var service = new OrderService(repository.Object);
The service doesn't know whether it received:
Real Repository
OR
Mock Repository
OR
Fake Repository
It only knows about:
IOrderRepository
Dependency Injection Is More Than Just Using Interfaces
This is an important distinction.
You might see code like:
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
}
and conclude:
"DI means using interfaces."
Not exactly.
Interfaces help create abstractions, but Dependency Injection is fundamentally about providing dependencies from outside rather than constructing them internally.
You could technically inject a concrete class:
public OrderService(OrderRepository repository)
{
_repository = repository;
}
That is still dependency injection.
However, depending on abstractions can provide additional flexibility and can support cleaner architectural boundaries.
Real-World Example: Notification Service
Imagine an application that sends notifications.
Today we use email.
Tomorrow we may need:
Email
SMS
Push notifications
WhatsApp
Instead of tightly coupling our business logic to one provider, define:
public interface INotificationService
{
Task SendAsync(string recipient, string message);
}
Email implementation:
public class EmailNotificationService : INotificationService
{
public Task SendAsync(string recipient, string message)
{
Console.WriteLine($"Sending email to {recipient}");
return Task.CompletedTask;
}
}
SMS implementation:
public class SmsNotificationService : INotificationService
{
public Task SendAsync(string recipient, string message)
{
Console.WriteLine($"Sending SMS to {recipient}");
return Task.CompletedTask;
}
}
Our business service doesn't care which implementation is being used:
public class AccountService
{
private readonly INotificationService _notificationService;
public AccountService(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task CreateAccountAsync(string email)
{
// Create account...
await _notificationService.SendAsync(
email,
"Your account has been created.");
}
}
Now we can change the notification mechanism without changing AccountService.
That's loose coupling.
The Dependency Inversion Principle
Dependency Injection is closely related to the Dependency Inversion Principle, one of the SOLID principles.
The basic idea is:
High-level business logic should not depend directly on low-level implementation details.
Instead:
Business Logic
↓
Abstraction
↓
Infrastructure
For example:
OrderService
↓
IOrderRepository
↓
SqlOrderRepository
Rather than:
OrderService
↓
SqlOrderRepository
The first design makes it easier to replace infrastructure without rewriting business logic.
DI Helps You Change Technology
This becomes extremely valuable in real projects.
Suppose your application initially uses:
SQL Server
Later, part of the application needs:
PostgreSQL
If your business logic directly creates SQL-specific classes everywhere, the migration becomes painful.
With abstractions:
public interface ICustomerRepository
{
Task<Customer?> GetAsync(int id);
}
you can have:
SqlCustomerRepository
or:
PostgresCustomerRepository
The application service can remain unchanged.
DI Can Also Improve Architecture
A well-designed application might look like:
API
│
▼
Application Services
│
▼
Interfaces
│
├── Database
├── Payment Provider
├── Email Provider
└── External APIs
The business logic sits in the middle.
Infrastructure details stay at the edges.
This is one reason Dependency Injection is commonly used in architectures such as:
Clean Architecture
Hexagonal Architecture
Onion Architecture
Domain-Driven Design applications
When Should You Use Dependency Injection?
DI is particularly useful when a class depends on:
Database repositories
External APIs
Payment providers
Email services
File storage
Logging
Caching
Message queues
Configuration
Time providers
Authentication services
For example:
public class ReportService
{
private readonly IReportRepository _repository;
private readonly IFileStorage _storage;
private readonly INotificationService _notification;
public ReportService(
IReportRepository repository,
IFileStorage storage,
INotificationService notification)
{
_repository = repository;
_storage = storage;
_notification = notification;
}
}
Each dependency has a clear responsibility.
When Should You NOT Use DI?
Not every class needs to be registered in the DI container.
For example:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
There is no meaningful external dependency here.
You don't necessarily need:
builder.Services.AddTransient<Calculator>();
You could simply use:
var calculator = new Calculator();
DI is not a goal by itself.
The goal is to create code that is easier to maintain, test, and evolve.
A Practical Rule
When you see:
var dependency = new Something();
inside a business service, ask yourself:
"Does this class really need to create this dependency?"
If the answer is no, consider injecting it.
For example:
public class ReportService
{
public void Generate()
{
var repository = new SqlReportRepository();
var email = new EmailService();
// ...
}
}
could become:
public class ReportService
{
private readonly IReportRepository _repository;
private readonly IEmailService _emailService;
public ReportService(
IReportRepository repository,
IEmailService emailService)
{
_repository = repository;
_emailService = emailService;
}
public void Generate()
{
// Business logic
}
}
The second version is easier to test and change.
Dependency Injection in One Diagram
The entire concept can be summarized like this:
DI Container
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
OrderService EmailService Repository
│
▼
IOrderRepository
│
▼
SqlOrderRepository
The important part is that the application doesn't have to manually construct the entire object graph.
The container does the wiring.
Final Example
Let's put everything together.
Interfaces
public interface IOrderRepository
{
Task SaveAsync(Order order);
}
public interface INotificationService
{
Task SendAsync(string recipient, string message);
}
Implementations
public class OrderRepository : IOrderRepository
{
public Task SaveAsync(Order order)
{
// Save to database
return Task.CompletedTask;
}
}
public class EmailNotificationService : INotificationService
{
public Task SendAsync(string recipient, string message)
{
// Send email
return Task.CompletedTask;
}
}
Business Service
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly INotificationService _notification;
public OrderService(
IOrderRepository repository,
INotificationService notification)
{
_repository = repository;
_notification = notification;
}
public async Task PlaceOrderAsync(Order order)
{
await _repository.SaveAsync(order);
await _notification.SendAsync(
order.CustomerEmail,
"Your order has been placed.");
}
}
Registration
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
builder.Services.AddScoped<OrderService>();
Now the application can construct the dependency graph:
OrderService
│
├── IOrderRepository
│ └── OrderRepository
│
└── INotificationService
└── EmailNotificationService
The OrderService doesn't create any of these dependencies itself.
That's Dependency Injection.
Final Thoughts
Dependency Injection isn't just an ASP.NET Core feature.
It is a design approach that helps you build software where classes have clear responsibilities and don't become tightly coupled to implementation details.
The key idea is simple:
Without DI:
Class
├── creates dependency
└── uses dependency
With DI:
DI Container
│
▼
Dependency
│
▼
Class
When used correctly, DI gives you:
Loose coupling
Better testability
Easier replacement of implementations
Cleaner architecture
Better separation of concerns
More maintainable code
If you're building ASP.NET Core applications, understanding Dependency Injection isn't optional—it is one of the fundamental concepts behind how the framework itself is designed.
And the next time you see:
builder.Services.AddScoped<IOrderService, OrderService>();
you'll know exactly what is happening behind the scenes.
Comments
Post a Comment