Introduction
Modern software applications are built with focus on flexibility, maintainability, and testability. In ASP.NET Core, one of the most powerful features that supports these goals is Dependency Injection (DI). Understanding DI properly will help you write cleaner, modular, and scalable applications.
Many developers know the definition of DI but struggle to understand how it actually works internally, why it is needed, how to use it correctly, and where it fits in a real ASP.NET Core project.
This deep-dive article explains DI in a clear, simple, and practical way:
What Dependency Injection is
Why it matters
Types of dependencies
Problems without DI
How DI works internally in ASP.NET Core
Service lifetime explained
Constructor, Method, and Property Injection
Using DI with interfaces, repositories, services, and controllers
Common mistakes and best practices
Advanced DI patterns
Practical real-world examples
By the end of this guide, you will feel confident using DI in any project.
What Is Dependency Injection?
Simple Definition
Dependency Injection is a design pattern where a class receives the objects it depends on rather than creating them internally.
In simple terms:
Instead of:
We do:
This reduces coupling and increases flexibility.
Why Is It Important?
DI ensures:
Better code structure
Loose coupling between classes
Easier unit testing
Simple implementation replacement
Clean architecture
Improved maintainability
Better separation of concerns
ASP.NET Core uses DI as a built-in feature, unlike older .NET frameworks that required external libraries.
Understanding Dependencies
A dependency is any object that another object needs to perform its work.
Example:
public class OrderService
{
private readonly PaymentService _payment;
public OrderService()
{
_payment = new PaymentService();
}
}
Here:
OrderService depends on PaymentService
OrderService creates PaymentService internally
This is called tight coupling
This design has multiple problems.
Problems Without Dependency Injection
When we do not use DI, we face several issues:
1. Tight Coupling
OrderService becomes tightly tied to PaymentService.
If PaymentService changes, OrderService must also change.
2. Hard to Replace Implementation
What if you want CashPaymentService tomorrow instead of PaymentService?
Without DI, you must edit the code:
_payment = new CashPaymentService();
This breaks Open/Closed Principle.
3. Difficult Unit Testing
Tests require mocks, but tight coupling makes it impossible.
4. Hard to Manage Large Applications
With 20 or more services and repositories, managing dependencies becomes chaotic.
5. Violates SOLID Principles
Especially:
DIP (Dependency Inversion Principle)
OCP (Open/Closed Principle)
SRP (Single Responsibility Principle)
DI Solves These Problems
With DI:
Classes no longer create dependencies.
The framework creates them and passes them in.
Everything depends on interfaces, not classes.
You can swap implementations easily.
Testing becomes simple with mocks.
Let us rewrite the earlier example using DI.
How DI Works in ASP.NET Core
ASP.NET Core provides a built-in IoC (Inversion of Control) container.
The DI flow in ASP.NET Core:
Register dependencies in Program.cs
Framework creates objects automatically
Framework manages lifetime
Framework injects dependencies when controller or service needs them
This means you never manually create objects.
ASP.NET Core handles it internally.
Registering Services in Program.cs
Basic example:
builder.Services.AddScoped<IPaymentService, PaymentService>();
This means:
Constructor Injection
This is the most common DI method in ASP.NET Core and the recommended one.
Example:
public class OrderService
{
private readonly IPaymentService _payment;
public OrderService(IPaymentService payment)
{
_payment = payment;
}
}
ASP.NET Core creates PaymentService and injects it automatically.
Method Injection
You pass dependencies as method arguments:
public void ProcessOrder(IPaymentService payment)
{
payment.Pay(100);
}
Used rarely, mostly in helper methods.
Property Injection
Dependency set using property:
public class ReportService
{
public ILogger Logger { get; set; }
}
Not very common in ASP.NET Core. Not recommended for most cases.
Service Lifetime Explained
The service lifetime defines how long an instance exists.
ASP.NET Core supports three types:
1. Transient (AddTransient)
Example:
builder.Services.AddTransient<IEmailService, EmailService>();
2. Scoped (AddScoped)
Example:
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
3. Singleton (AddSingleton)
One instance for the entire application
Used for configuration, caching, logging
Example:
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
Real-world Usage in a Professional ASP.NET Core Application
Let us build a small sample architecture.
Step 1: Define Interfaces
public interface IProductRepository
{
IEnumerable<Product> GetAll();
}
public interface IPaymentService
{
void Pay(decimal amount);
}
public interface IEmailService
{
void SendEmail(string to, string message);
}
Step 2: Implement the Interfaces
ProductRepository.cs:
public class ProductRepository : IProductRepository
{
public IEnumerable<Product> GetAll()
{
return new List<Product>
{
new Product {Id = 1, Name = "Laptop"},
new Product {Id = 2, Name = "Mobile"}
};
}
}
PaymentService.cs:
public class PaymentService : IPaymentService
{
public void Pay(decimal amount)
{
Console.WriteLine("Payment completed.");
}
}
EmailService.cs:
public class EmailService : IEmailService
{
public void SendEmail(string to, string message)
{
Console.WriteLine("Email sent.");
}
}
Step 3: Register Services in Program.cs
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddTransient<IPaymentService, PaymentService>();
builder.Services.AddSingleton<IEmailService, EmailService>();
Step 4: Inject Into Controller
ProductController.cs:
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly IProductRepository _repo;
private readonly IPaymentService _payment;
private readonly IEmailService _email;
public ProductController(IProductRepository repo,
IPaymentService payment,
IEmailService email)
{
_repo = repo;
_payment = payment;
_email = email;
}
[HttpGet]
public IActionResult GetProducts()
{
var products = _repo.GetAll();
_payment.Pay(500);
_email.SendEmail("[email protected]", "Thanks for shopping.");
return Ok(products);
}
}
This is a complete DI-enabled architecture.
Internals of ASP.NET Core Dependency Injection
ASP.NET Core uses a built-in IoC container.
What happens internally:
When the application starts, it builds a ServiceProvider.
ServiceProvider contains all registered services.
When a controller is created, ASP.NET Core checks its constructor.
It reads required dependencies.
It fetches or creates the required services.
It injects them into the controller.
Developers do not manage object creation manually.
DI with Repository Pattern and EF Core
Typical registration:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
Repository constructor:
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
}
This is the standard way DI is used with Entity Framework Core.
DI with Unit of Work
If your project uses Unit of Work:
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
UnitOfWork constructor:
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public IProductRepository Products { get; }
public UnitOfWork(ApplicationDbContext context, IProductRepository products)
{
_context = context;
Products = products;
}
}
Again, DI manages everything.
Common Mistakes Developers Make with DI
1. Injecting Too Many Services in One Class
If more than 5–6 dependencies are injected, your class has too many responsibilities.
2. Using AddSingleton for EF Core
EF Core DbContext must always be scoped.
Singleton will cause application crash.
3. Using Concrete Class Instead of Interface
Bad:
builder.Services.AddScoped<ProductRepository>();
Good:
builder.Services.AddScoped<IProductRepository, ProductRepository>();
4. Circular Dependencies
A depends on B, and B depends on A.
This must be avoided.
5. Injecting DbContext into other DbContexts
Leads to large messy systems.
Advanced DI Patterns in ASP.NET Core
1. Factory Pattern with DI
Useful when multiple implementations exist.
Example:
UPI payment
Card payment
Wallet payment
Use factory to resolve based on type.
2. Named Services
You can use IServiceProvider to fetch services dynamically.
3. Generics with DI
Example:
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
This allows DI for all repository types.
4. Options Pattern
Used for configuration:
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
Inject like:
public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}
5. Middleware Dependency Injection
Custom middleware can receive DI via constructor.
Benefits of DI in Real ASP.NET Core Projects
Cleaner and modular code
Easier to test using mocks
Supports SOLID principles
Reduces tight coupling
Makes system more maintainable
Helps in scaling the application
Supports better architecture (Repository, Services, UoW, CQRS)
Improves readability
Allows multiple implementations
Improves future maintainability
Summary
Dependency Injection is not just a technical concept; it is a fundamental architectural tool that makes ASP.NET Core applications clean, scalable, flexible, and professional.
In this deep dive, we covered:
What DI is
Why DI is essential
How DI works internally
How services are registered
Constructor, method, and property injection
Service lifetimes
Practical examples
DI with repository and Unit of Work
Advanced patterns
Best practices
Mastering DI will dramatically improve the quality of your ASP.NET Core applications and help you design enterprise-level architectures confidently.