ASP.NET Core dependency injection is one of the core building blocks of modern .NET application architecture. Used correctly, dependency injection reduces coupling, makes business workflows easier to test and allows infrastructure components such as databases, APIs and file services to evolve without forcing changes throughout the application.
The most useful way to understand dependency injection is through
a real enterprise system. The Enerlux Door & Window ERP
codebase uses a large static DAL with Entity Framework 6,
SqlConnection, SqlCommand,
DataTable and many application-specific query
methods. That code is valuable because it shows exactly where
modern dependency injection can improve separation of concerns
during an ASP.NET Core modernization.
This article does not suggest that every static DAL method must immediately be converted into an interface. Instead, it shows where dependency injection creates real architectural value: database access, reporting services, business workflows, external integrations and testable application services.
- Use dependency injection to control coupling between application layers.
- Understand Singleton, Scoped and Transient service lifetimes.
- Prefer constructor injection for required dependencies.
- Keep infrastructure details outside business workflows.
- Use interfaces where they protect a meaningful application boundary.
- Design dependencies so enterprise applications remain testable and maintainable.
1. What Is Dependency Injection in ASP.NET Core?
Dependency injection means that a component receives the objects it needs instead of constructing those objects internally.
Without Dependency Injection
ReportService
↓
new DatabaseRepository()
↓
new SqlConnection()
With Dependency Injection
ReportService
↓
IReportRepository
↓
Configured implementation
The second approach gives the application composition root control over the concrete implementation.
2. Why Dependency Injection Matters in Enterprise .NET Applications
In a small application, constructing dependencies directly can seem harmless. In a large ERP, POS or shipping platform, however, the same dependency may be required by dozens of workflows.
The Enerlux DAL contains many responsibilities: dealers, quotes, orders, inventory, reports, appointments, installations, warranty and user administration. Keeping all of those concerns in a static data-access class makes the dependency structure implicit.
Dependency injection makes those relationships explicit at the point where a service is constructed.
3. Prefer Constructor Injection for Required Dependencies
Constructor injection is usually the clearest way to express that a service cannot operate without a dependency.
public sealed class QuoteService
{
private readonly IQuoteRepository _repository;
public QuoteService(
IQuoteRepository repository)
{
_repository = repository;
}
public Task<IReadOnlyList<QuoteSummary>>
GetQuotesAsync(
QuoteFilter filter,
CancellationToken cancellationToken)
{
return _repository.GetQuotesAsync(
filter,
cancellationToken);
}
}
The dependency is visible in the class definition and can be replaced during testing without changing the business logic.
4. Understand Singleton, Scoped and Transient Lifetimes
ASP.NET Core's built-in container supports three common service lifetimes.
- Transient — a new instance is created each time it is requested.
- Scoped — one instance is normally used within a web request scope.
- Singleton — one instance is shared for the lifetime of the application.
Choosing a lifetime is an architectural decision. A database context, for example, should not casually be registered as a singleton simply because reusing objects appears efficient.
builder.Services.AddTransient<IReportMapper, ReportMapper>();
builder.Services.AddScoped<IQuoteService, QuoteService>();
builder.Services.AddSingleton<IApplicationClock, ApplicationClock>();
5. Use Scoped Services for Request-Level Database Work
Enterprise ASP.NET Core applications commonly use scoped services for database-related application workflows. The exact lifetime depends on the persistence technology and design, but request-scoped coordination is often a useful default for application services.
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
The goal is to make the lifetime of related dependencies explicit rather than allowing database objects to be created randomly throughout the application.
6. Where the Enerlux Static DAL Creates Tight Coupling
The Enerlux DAL contains methods such as
FetchQuotesAndOrders that create an Entity
Framework context inside a static method.
using (var ctxt = new Us_EnerluxEntities())
{
// Query application data
}
This design works, but the caller cannot supply a different context, test double or alternative data-access implementation. The dependency is hidden inside the method.
During modernization, the first useful step is often to move the database dependency behind a service or repository that can be injected.
7. Introduce an Explicit Repository Boundary Where It Helps
A repository can encapsulate persistence without exposing every database detail to the application layer.
public interface IQuoteRepository
{
Task<IReadOnlyList<QuoteSummary>>
GetQuotesAsync(
QuoteFilter filter,
CancellationToken cancellationToken);
}
The implementation can then use Entity Framework Core, Dapper or another persistence strategy without changing the application service contract.
8. Inject Business Services Instead of Calling Static DAL Methods
Static calls hide dependencies. An application service can make those dependencies explicit and coordinate the actual business workflow.
public sealed class OrderService : IOrderService
{
private readonly IOrderRepository _orders;
private readonly IInventoryService _inventory;
public OrderService(
IOrderRepository orders,
IInventoryService inventory)
{
_orders = orders;
_inventory = inventory;
}
}
This becomes particularly useful when an ERP workflow combines orders, inventory, installation or warranty data.
9. Use Dependency Injection for ERP Reporting Services
The Enerlux DAL contains multiple report-oriented operations such as quote and order summaries, commercial versus residential reporting and generated reports with filters.
public sealed class ReportService : IReportService
{
private readonly IReportRepository _repository;
public ReportService(
IReportRepository repository)
{
_repository = repository;
}
public Task<ReportResult> GenerateAsync(
ReportFilter filter,
CancellationToken cancellationToken)
{
return _repository.GenerateAsync(
filter,
cancellationToken);
}
}
The reporting service can then remain focused on the application workflow while SQL construction stays within the persistence layer.
10. Inject Configuration Instead of Reading It Everywhere
Enterprise applications often depend on connection strings, service endpoints and other environment-specific settings. Keep configuration at the composition boundary.
builder.Services.Configure<DatabaseOptions>(
configuration.GetSection("Database"));
builder.Services.Configure<ReportOptions>(
configuration.GetSection("Reports"));
Application services can then receive typed options rather than repeatedly accessing raw configuration values.
11. Inject External API Clients Through HttpClientFactory
If an ASP.NET Core modernization introduces external services for ERP, document, shipping or customer workflows, HTTP clients should also be composed through dependency injection.
builder.Services.AddHttpClient<ShippingClient>(
client =>
{
client.BaseAddress =
new Uri(configuration["Shipping:BaseUrl"]!);
});
This avoids scattering new HttpClient()
throughout business services.
12. Avoid Captive Dependency Lifetime Problems
A common dependency-injection mistake is injecting a shorter-lived service into a longer-lived service.
Singleton Service
↓
Scoped DbContext
Potential lifetime mismatch
A singleton retaining a request-scoped dependency can create incorrect state sharing or runtime failures. Review the lifetime of the complete dependency graph.
13. Let the Container Manage Disposable Dependencies
One advantage of DI is that the container can manage the lifetime of services it creates. Avoid manually disposing a dependency that the container is responsible for managing.
At the same time, manually created resources such as explicitly constructed ADO.NET connections still require proper disposal.
await using var connection =
new SqlConnection(connectionString);
await connection.OpenAsync(
cancellationToken);
14. Do Not Create Interfaces Just for the Sake of Interfaces
Dependency injection does not require every class to have an interface. Create an abstraction where it protects a meaningful boundary or improves substitution and testing.
For example, a repository that hides SQL Server access or an external API client is often a useful boundary. A private formatter with one implementation may not need one.
15. Dependency Injection Makes Enterprise Code Easier to Test
The most practical benefit of dependency injection is often testability. An application service can receive a test implementation instead of creating a real database dependency.
var repository =
new FakeQuoteRepository();
var service =
new QuoteService(repository);
var result =
await service.GetQuotesAsync(
filter,
CancellationToken.None);
The test can focus on application behavior without requiring a live production database.
16. Use Test Doubles at the Boundary That Matters
A mock or fake is most useful when it replaces an expensive or external dependency. Examples include SQL Server, external APIs, cloud storage and messaging services.
var carrier =
new FakeCarrierClient
{
Result = new ShipmentResult
{
Success = true
}
};
Do not mock every internal class simply because the DI container makes it possible. Test at meaningful architectural boundaries.
17. Make Multiple Data Access Technologies Explicit
Enterprise applications sometimes use more than one data access technology. The Enerlux DAL is a practical example: Entity Framework is used in many workflows while direct ADO.NET is used for report-oriented database access.
Application Service
│
├── EF Core Repository
│
├── Dapper Repository
│
└── ADO.NET Reporting Repository
Dependency injection lets the composition layer choose the implementation without forcing business services to know how data is persisted.
18. Use Dependency Injection as Part of Legacy .NET Modernization
The Enerlux code is a good illustration of incremental modernization. A large static DAL does not need to be rewritten in one step.
Legacy Static DAL
↓
Application Boundary
↓
Interface
↓
Injected Implementation
↓
New ASP.NET Core Application
This approach allows teams to modernize one business area at a time while keeping existing functionality operational.
19. Migrate One Business Capability at a Time
Instead of converting an entire ERP DAL simultaneously, select a business capability such as reporting, inventory, orders or appointments.
Existing Quote DAL
↓
IQuoteRepository
↓
New Implementation
↓
ASP.NET Core Quote Service
↓
New API
This makes modernization easier to test, deploy and roll back.
20. Use Generic Dependencies Carefully
Generic repositories and generic services can be useful, but over-generalizing business operations can hide important domain differences.
A quotation workflow, inventory workflow and installation workflow may all access SQL Server, but their business behavior is different. Keep abstractions aligned with the responsibilities they actually protect.
21. Use Factories When the Implementation Depends on Runtime Data
Dependency injection chooses services during application composition. Some enterprise workflows also need a runtime selection, such as choosing a carrier, document processor or report generator.
public interface IDocumentProcessor
{
string Type { get; }
Task ProcessAsync(
DocumentRequest request,
CancellationToken cancellationToken);
}
A resolver or factory can select the registered implementation based on a known business value.
22. Add Cross-Cutting Behavior Without Polluting Business Code
Logging, metrics, caching or authorization-related behavior can often be applied around a service boundary rather than copied into every method.
Application Service
↓
Logging / Metrics Decorator
↓
Concrete Service
The exact implementation can vary, but the architectural principle is to keep cross-cutting concerns separate from business rules.
23. Create Scopes for Background Processing
Background services can run outside a normal HTTP request. If they need scoped services, create an explicit scope for each unit of work.
public sealed class ReportWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public ReportWorker(
IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
using var scope =
_scopeFactory.CreateScope();
var service =
scope.ServiceProvider
.GetRequiredService<IReportService>();
await service.ProcessPendingAsync(
stoppingToken);
}
}
The scope lifetime should match the unit of work rather than the lifetime of the entire application.
24. Keep Dependency Registration in the Composition Root
Service registration should be centralized enough that developers can understand how the application is composed.
builder.Services.AddScoped<IQuoteService, QuoteService>();
builder.Services.AddScoped<IQuoteRepository, QuoteRepository>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
builder.Services.AddScoped<IReportService, ReportService>();
This makes application composition visible instead of scattering service creation throughout business code.
25. Dependency Injection Anti-Patterns to Avoid
- Using service location everywhere instead of constructor injection.
- Registering database contexts as singletons without a valid reason.
- Creating hidden service dependencies inside business classes.
- Adding interfaces to every class without a meaningful boundary.
- Building an oversized service locator that resolves everything dynamically.
- Using dependency injection to hide poor application architecture.
26. Applying Dependency Injection to the Enerlux ERP
A modernization path for the Enerlux application could start by grouping the large DAL into business capabilities instead of exposing one static class to every part of the system.
ASP.NET Core Application
│
├── Quote Service
│ └── IQuoteRepository
│
├── Order Service
│ └── IOrderRepository
│
├── Inventory Service
│ └── IInventoryRepository
│
├── Appointment Service
│ └── IAppointmentRepository
│
└── Report Service
└── IReportRepository
The existing EF6 and ADO.NET implementations can initially sit behind these boundaries. Later, selected implementations can be moved to EF Core, Dapper or other data-access strategies without changing the higher-level workflow contracts.
27. The Same Pattern Fits WinPOS
WinPOS has similarly distinct business responsibilities: sales, customers, wallet transactions, customer debt, inventory and synchronization.
POS Application
│
├── ISaleService
├── ICustomerService
├── IWalletService
├── ISaleDebtService
└── ISynchronizationService
Dependency injection can make those relationships explicit while allowing Dapper, SQL Server and local database implementations to remain behind the appropriate boundary.
28. The Pattern Also Fits ShippingPortal
ShippingPortal can use the same architecture for shipment, tracking and carrier integration workflows.
Shipping API
↓
IShipmentService
↓
IShipmentRepository
│
├── SQL Server / Dapper
│
└── Carrier Client
├── DHL
├── UPS
├── DPD
└── Asendia
The API does not need to know which concrete carrier client or database implementation is being used.
ASP.NET Core Dependency Injection Checklist
- Are required dependencies supplied through constructors?
- Are service lifetimes appropriate for the dependency graph?
- Are database dependencies scoped correctly?
- Are external API clients configured through HttpClientFactory?
- Are abstractions introduced only where they protect meaningful boundaries?
- Can application services be tested without real infrastructure?
- Are background workers creating scopes for scoped dependencies?
- Are business services separated from persistence details?
- Can legacy DAL implementations be replaced without changing business workflows?
- Is dependency registration understandable from the composition root?
Conclusion
ASP.NET Core dependency injection is most valuable when it
supports a clear architecture. The goal is not simply to
register hundreds of services in Program.cs.
The goal is to make important dependencies explicit and
keep business workflows independent from infrastructure
details.
The Enerlux Door & Window ERP code provides a realistic example of where this separation can help. Its large DAL combines Entity Framework 6 and direct ADO.NET across quotations, orders, inventory, reporting, appointments, installation and warranty workflows.
During modernization, those capabilities can be introduced behind focused application services and injected repository or infrastructure boundaries. The same approach can then be applied to WinPOS and ShippingPortal, where SQL Server, Dapper, local databases and external integrations form the infrastructure behind business operations.