Injection of Dependencies in.NET Core
A basic software design technique called Dependency Injection (DI) is used to lessen class coupling and facilitate the testing, maintenance, and expansion of programs.
A class produces the objects on which it depends in a closely connected application. For instance, an Engine object might be created directly by a Car class. The Car class must be changed if the application subsequently requires a new engine implementation.
Dependency Injection allows the Car class to specify that it requires an Engine, and another component is in charge of delivering the necessary implementation.
.NET and ASP.NET Core provide a built-in Dependency Injection container, so developers can register services and let the framework create and provide their dependencies.
This article explains Dependency Injection, its relationship with Inversion of Control (IoC), service lifetimes, injection types, practical implementation, benefits, common anti-patterns, and recommended practices.
What Is Dependency Injection?
Dependency Injection is a design pattern in which an object receives the components it depends on from an external source instead of creating those components itself.
Consider a simple example:
public class Engine
{
public void Start()
{
Console.WriteLine("Engine started.");
}
}
public class Car
{
private readonly Engine _engine = new Engine();
public void Start()
{
_engine.Start();
}
}
The Car class creates its own Engine.
This creates a direct dependency between Car and the concrete Engine implementation. If the application needs a different engine implementation, the Car class has to change.
With Dependency Injection, the dependency can instead be supplied from outside the class.
public interface IEngine
{
void Start();
}
public class Engine : IEngine
{
public void Start()
{
Console.WriteLine("Engine started.");
}
}
public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Start()
{
_engine.Start();
}
}
Now Car depends on the IEngine abstraction rather than directly creating Engine.
This is the basic idea behind Dependency Injection:
The class declares what it needs; another component provides it.
Why Do We Need Dependency Injection?
Without DI, classes often create their own dependencies. This can lead to tight coupling.
For example:
public class EmailService
{
public void Send(string message)
{
Console.WriteLine(message);
}
}
public class OrderService
{
private readonly EmailService _emailService = new EmailService();
public void PlaceOrder()
{
// Place order logic
_emailService.Send("Order placed successfully.");
}
}
OrderService is directly coupled to EmailService.
Suppose the application later needs to use an SMS service instead, or a fake email service during testing. The OrderService implementation must change.
DI separates these responsibilities.
public interface IMessageService
{
void Send(string message);
}
public class EmailService : IMessageService
{
public void Send(string message)
{
Console.WriteLine($"Email: {message}");
}
}
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(IMessageService messageService)
{
_messageService = messageService;
}
public void PlaceOrder()
{
// Place order logic
_messageService.Send("Order placed successfully.");
}
}
The OrderService no longer needs to know which concrete messaging implementation is being used.
This provides several advantages:
- Loose coupling
- Easier unit testing
- Easier replacement of implementations
- Better separation of concerns
- Centralized dependency configuration
- Improved maintainability
Dependency Injection and Inversion of Control
Dependency Injection and Inversion of Control are related concepts, but they are not the same thing.
What Is Inversion of Control?
Inversion of Control (IoC) is a broader architectural principle in which control over certain operations is transferred from application code to an external component or framework.
In traditional code, a class might create and manage everything it needs:
Application
|
+-- Creates Service
|
+-- Creates Repository
|
+-- Creates Database Connection
With IoC, the framework or another external component manages this process.
Application
|
+-- Requests Service
|
+-- DI Container
|
+-- Creates Service
+-- Provides Repository
+-- Provides other dependencies
The framework becomes responsible for resolving the dependency graph.
How Does DI Achieve IoC?
Dependency Injection is one technique for implementing Inversion of Control.
For example, instead of writing:
var service = new OrderService(new EmailService());
the application can register the dependencies:
builder.Services.AddTransient<IMessageService, EmailService>();
builder.Services.AddTransient<OrderService>();
The DI container can then create OrderService and provide its required IMessageService.
Therefore:
- IoC is the broader principle.
- DI is a technique used to achieve IoC.
- DI container is the mechanism that manages dependency registration and resolution.
Built-in Dependency Injection in .NET
Modern .NET applications include a built-in DI container.
In an ASP.NET Core application, services are commonly registered through builder.Services in Program.cs.
The two important abstractions are:
IServiceCollection— used to register services.IServiceProvider— used to resolve services at runtime.
IServiceCollection
IServiceCollection contains the service registrations used by the application.
For example:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IMyService, MyService>();
var app = builder.Build();
app.Run();
The following registration tells the DI container:
When IMyService is requested,
create and provide MyService.
Service registrations also specify the lifetime of the service.
IServiceProvider
IServiceProvider is responsible for resolving registered services.
In normal ASP.NET Core application code, developers generally do not need to call GetService or GetRequiredService manually. Instead, the framework resolves constructor or endpoint parameters automatically.
For example:
public class HomeController : Controller
{
private readonly IMyService _myService;
public HomeController(IMyService myService)
{
_myService = myService;
}
}
ASP.NET Core sees the IMyService dependency and asks the DI container for the registered implementation.
The Composition Root
Service registration is commonly centralized in Program.cs, which acts as the application’s composition root.
For example:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IMyService, MyService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IApplicationCache, ApplicationCache>();
var app = builder.Build();
app.Run();
Keeping dependency configuration in one place makes the application’s dependency graph easier to understand and change.
For larger applications, registrations can also be grouped into extension methods.
Service Lifetimes in .NET
.NET provides three primary service lifetimes:
- Transient
- Scoped
- Singleton
Choosing the correct lifetime is important because it determines how long a service instance remains available.
Transient Lifetime
A transient service creates a new instance each time it is requested from the DI container.
Registration:
builder.Services.AddTransient<IMyService, MyService>();
Transient services are commonly appropriate for lightweight, stateless services.
For example:
public interface IMessageFormatter
{
string Format(string message);
}
public class MessageFormatter : IMessageFormatter
{
public string Format(string message)
{
return $"Message: {message}";
}
}
Registration:
builder.Services.AddTransient<IMessageFormatter, MessageFormatter>();
A new MessageFormatter instance can be created whenever the service is requested.
When to Use Transient
Transient is generally suitable for:
- Stateless business services
- Formatting utilities
- Lightweight calculations
- Data transformation services
The main consideration is object creation overhead if a transient service is requested very frequently.
Scoped Lifetime
A scoped service is created once per DI scope.
In an ASP.NET Core web application, a scope normally corresponds to an HTTP request.
Registration:
builder.Services.AddScoped<IOrderService, OrderService>();
If several components request IOrderService during the same request, they receive the same scoped instance.
A common example is Entity Framework Core’s DbContext, which is normally registered with a scoped lifetime in ASP.NET Core applications.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
When to Use Scoped
Scoped services are commonly used for:
- Database contexts
- Unit of Work implementations
- Request-specific business services
- Services that should share state within one request
A scoped service should not be treated as globally shared application state.
Singleton Lifetime
A singleton service uses one instance for the lifetime of the application’s service provider.
Registration:
builder.Services.AddSingleton<IApplicationCache, ApplicationCache>();
All consumers that resolve the service from the same root provider receive the same instance.
Singleton services are appropriate when the service is:
- Stateless
- Thread-safe
- Expensive to create
- Designed to maintain application-wide state
Singleton and Thread Safety
Because a singleton can be accessed concurrently by multiple requests, mutable state inside a singleton must be designed for concurrent access.
For example, this is potentially unsafe:
public class CounterService
{
public int Count { get; set; }
}
If multiple requests modify Count, the implementation must account for concurrency.
Avoid Captive Dependencies
A longer-lived service should not directly capture a shorter-lived dependency.
For example:
Singleton
|
+-- Scoped Service
This can cause the scoped dependency to be retained beyond the lifetime for which it was designed.
A particularly important example is attempting to inject a scoped DbContext directly into a singleton.
If a singleton genuinely needs to work with a scoped service, the design should create an appropriate scope when performing that operation rather than capturing the scoped dependency in the singleton constructor.
Service Lifetime Comparison
| Lifetime | Instance Behavior | Common Uses | Main Consideration |
|---|---|---|---|
| Transient | New instance when requested | Lightweight, stateless services | More object creation |
| Scoped | One instance per scope | DbContext, request-level services |
Scope boundaries matter |
| Singleton | One instance for the service-provider lifetime | Shared, thread-safe services | Concurrency and state management |
Types of Dependency Injection
Dependency Injection can be implemented in several ways.
The three commonly discussed forms are:
- Constructor injection
- Method injection
- Property injection
Constructor Injection
Constructor injection is the preferred approach for required dependencies in most .NET applications.
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(IMessageService messageService)
{
_messageService = messageService;
}
public void PlaceOrder()
{
_messageService.Send("Order placed.");
}
}
The dependency is explicit because it appears in the constructor.
Constructor injection provides several benefits:
- Dependencies are immediately visible.
- Required dependencies can be enforced when the object is constructed.
- Dependencies can be stored in
readonlyfields. - Unit tests can provide fake or mock implementations.
- The class does not need to resolve dependencies itself.
For these reasons, constructor injection should generally be the default choice.
Method Injection
Method injection supplies a dependency through a method parameter.
This approach is useful when a dependency is required only for a particular operation.
Minimal APIs provide a common example:
app.MapGet("/orders", (IOrderService orderService) =>
{
return orderService.GetOrders();
});
The framework resolves IOrderService and provides it to the endpoint handler.
Method injection can be useful when the dependency is specific to one operation rather than a requirement of the entire class.
Property Injection
Property injection provides a dependency through a property after an object has been created.
It is less common in modern ASP.NET Core application design.
For example:
public class ReportService
{
public ILogger<ReportService>? Logger { get; set; }
}
The major disadvantage is that the dependency is not visible in the constructor.
This can make the class harder to understand and can result in runtime failures if a required property has not been initialized.
Property injection may have limited uses for optional dependencies or integration with systems that require it, but constructor injection is generally preferable for required dependencies.
Practical Dependency Injection Example
Let’s create a simple service and inject it into an ASP.NET Core controller.
Step 1: Create the Service Interface
Create an interface:
public interface IMyService
{
string Greet(string name);
}
The interface defines the contract without specifying how the service performs the operation.
Step 2: Create the Implementation
Create a class that implements the interface:
public class MyService : IMyService
{
public string Greet(string name)
{
return $"Hello, {name} from MyService!";
}
}
Step 3: Register the Service
Register the implementation in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IMyService, MyService>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
The registration connects the abstraction to its implementation:
IMyService
|
+-- MyService
Step 4: Inject the Service into a Controller
The controller can now request IMyService through its constructor:
public class HomeController : Controller
{
private readonly IMyService _myService;
public HomeController(IMyService myService)
{
_myService = myService;
}
public IActionResult Welcome()
{
string message = _myService.Greet("Developer");
return Content(message);
}
}
The controller does not create MyService.
ASP.NET Core resolves the registered implementation and passes it into the constructor.
Step 5: Expected Output
When the Welcome action is called, the response is:
Hello, Developer from MyService!
The important part is not the message itself. The important part is that HomeController does not know how MyService is constructed.
Before and After Dependency Injection
The difference becomes clearer when comparing tightly coupled and loosely coupled implementations.
Without DI
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine(
$"MyDependency.WriteMessage called: {message}");
}
}
public class IndexModel
{
private readonly MyDependency _dependency =
new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}
IndexModel directly creates MyDependency.
If the implementation changes, IndexModel must also change.
With DI
Define an abstraction:
public interface IMyDependency
{
void WriteMessage(string message);
}
Implement it:
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine(
$"MyDependency.WriteMessage called: {message}");
}
}
Register it:
builder.Services.AddTransient<IMyDependency, MyDependency>();
Inject it:
public class IndexModel
{
private readonly IMyDependency _myDependency;
public IndexModel(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage(
"IndexModel.OnGet with DI");
}
}
Now the consuming class depends on the abstraction rather than the concrete implementation.
A different implementation can be registered without changing IndexModel.
Dependency Injection and Unit Testing
One of the biggest practical benefits of DI is easier unit testing.
Suppose OrderService depends on IMessageService:
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(IMessageService messageService)
{
_messageService = messageService;
}
public void PlaceOrder()
{
// Order processing
_messageService.Send("Order placed.");
}
}
A test does not need to use the real email or messaging infrastructure.
A fake implementation can be supplied:
public class FakeMessageService : IMessageService
{
public string? LastMessage { get; private set; }
public void Send(string message)
{
LastMessage = message;
}
}
The test can then construct the service using the fake:
var fakeMessageService = new FakeMessageService();
var orderService =
new OrderService(fakeMessageService);
orderService.PlaceOrder();
Console.WriteLine(fakeMessageService.LastMessage);
Expected output:
Order placed.
The test focuses on OrderService rather than depending on an external messaging system.
Key Benefits of Dependency Injection
Loose Coupling
DI separates a class from the concrete implementations of its dependencies.
Instead of:
OrderService -> EmailService
the design becomes:
OrderService -> IMessageService <- EmailService
This makes implementations easier to replace.
Improved Testability
Dependencies can be replaced with mocks, fakes, or stubs during testing.
This makes it easier to test business logic without requiring databases, APIs, file systems, or other external resources.
Better Maintainability
Dependency configuration is centralized instead of being scattered throughout application classes.
A change to an implementation can often be handled in the composition root without modifying consumers.
Flexibility
An application can support multiple implementations of an abstraction.
For example:
IMessageService
|
+-- EmailMessageService
+-- SmsMessageService
+-- PushNotificationService
The consuming business service does not need to know which implementation is selected.
Support for Clean Architecture
DI works closely with the Dependency Inversion Principle.
High-level business logic can depend on abstractions instead of directly depending on infrastructure implementations.
For example:
Application Layer
|
v
IOrderRepository
^
|
Infrastructure Layer
|
SqlOrderRepository
The business logic does not need to directly construct the database implementation.
Common Dependency Injection Anti-Patterns
DI provides significant benefits, but simply using the DI container does not automatically produce good architecture.
Several common mistakes should be avoided.
Service Locator Anti-Pattern
The Service Locator pattern occurs when a class receives IServiceProvider and resolves its own dependencies.
For example:
public class OrderService
{
private readonly IServiceProvider _serviceProvider;
public OrderService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void ProcessOrder()
{
var repository =
_serviceProvider.GetRequiredService<IOrderRepository>();
repository.Save();
}
}
The problem is that the class’s dependency on IOrderRepository is hidden.
Compare this with constructor injection:
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
The second version clearly communicates the dependency.
As a general rule, use constructor injection for required dependencies and avoid using IServiceProvider as a general-purpose dependency resolver inside application classes.
Too Many Constructor Dependencies
Consider a class with a constructor like this:
public OrderService(
IRepository repository,
IEmailService emailService,
ILogger<OrderService> logger,
IPaymentService paymentService,
IInventoryService inventoryService,
INotificationService notificationService,
IAuditService auditService)
{
}
DI is not necessarily the problem here.
The large constructor may indicate that the class has too many responsibilities.
Instead of hiding those dependencies, use the constructor as a design signal and consider breaking the class into smaller components.
Mixing Service Lifetimes
Be careful when services with different lifetimes depend on one another.
A common problematic relationship is:
Singleton
|
+-- Scoped dependency
For example, a singleton should not directly capture an Entity Framework Core DbContext registered as scoped.
When a shorter-lived service is needed from a longer-lived component, an appropriate scope should be created for the operation.
Creating Interfaces for Everything
DI often involves interfaces, but every class does not necessarily need an interface.
For example, creating:
IStringFormatter
StringFormatter
only because a DI container is being used may add unnecessary abstraction.
Interfaces are particularly useful when they provide meaningful benefits such as:
- Multiple implementations
- Test substitution
- Architectural boundaries
- Decoupling infrastructure from application logic
The goal is useful abstraction, not abstraction for its own sake.
Managing Configuration with IOptions
Applications frequently need configuration values such as:
- SMTP settings
- API URLs
- Feature settings
- Service-specific options
Instead of injecting the complete IConfiguration object into every service, strongly typed options can be used.
Define a configuration class:
public class EmailSettings
{
public string SmtpServer { get; set; } = string.Empty;
public int Port { get; set; }
public string FromAddress { get; set; } = string.Empty;
}
Suppose appsettings.json contains:
{
"EmailSettings": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"FromAddress": "noreply@example.com"
}
}
Register the configuration:
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("EmailSettings"));
Inject the options:
using Microsoft.Extensions.Options;
public class EmailService
{
private readonly EmailSettings _settings;
public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}
public void SendEmail()
{
Console.WriteLine(
$"Sending email from {_settings.FromAddress}");
}
}
This keeps configuration strongly typed and limits each service to the settings it actually needs.
Depending on the configuration behavior required, .NET also provides IOptionsSnapshot<T> and IOptionsMonitor<T>.
Practical DI Workflow
A typical Dependency Injection workflow in an ASP.NET Core application looks like this:
1. Define an abstraction
|
v
2. Implement the abstraction
|
v
3. Register the implementation
|
v
4. Inject the abstraction
|
v
5. ASP.NET Core resolves the dependency
|
v
6. Application uses the service
For example:
IOrderService
|
v
OrderService
|
v
IOrderRepository
|
v
SqlOrderRepository
The registrations in Program.cs connect these components.
Best Practices for Dependency Injection
The following practices help keep DI-based applications maintainable:
- Prefer constructor injection for required dependencies.
- Depend on abstractions where an abstraction provides a real architectural or testing benefit.
- Choose service lifetimes deliberately.
- Avoid capturing scoped services inside singleton services.
- Avoid using
IServiceProvideras a service locator in application code. - Treat large constructors as a possible signal of excessive class responsibilities.
- Keep service registrations organized.
- Use strongly typed options for structured configuration.
- Keep services focused on clear responsibilities.
- Use mocks, fakes, or stubs to isolate units during testing.
Conclusion
Dependency Injection is a fundamental part of modern .NET application development. It separates object creation from object usage and allows classes to depend on abstractions rather than directly creating concrete implementations.
In ASP.NET Core, the built-in DI container provides a convenient way to register services, manage their lifetimes, and inject them into controllers, endpoint handlers, and other application components.
The three primary service lifetimes are Transient, Scoped, and Singleton. Understanding the differences between them is essential for avoiding state, resource, and concurrency problems.
Constructor injection is generally the preferred injection technique because it makes required dependencies explicit and improves testability.
DI is most effective when it is treated as part of the application’s design rather than simply as a framework feature. Combined with appropriate abstractions, clear service boundaries, correct lifetime management, and focused classes, Dependency Injection helps create .NET applications that are easier to test, maintain, and evolve.
ASP.NET Core 10.0 Hosting Recommendation
HostForLIFE.eu
HostForLIFE.eu is a popular recommendation that offers various hosting choices. Starting from shared hosting to dedicated servers, you will find options fit for beginners and popular websites. It offers various hosting choices if you want to scale up. Also, you get flexible billing plans where you can choose to purchase a subscription even for one or six months.
