Dependency Injection Best Practices (ASP.NET Core and Modern .NET)

Dependency Injection Best Practices (ASP.NET Core and Modern .NET)

Dependency Injection (DI) is a core design principle in modern .NET applications and a fundamental feature of ASP.NET Core. When used correctly, it improves testability, maintainability, and modularity. When misused, it can lead to hidden complexity, memory leaks, and fragile architectures. This guide focuses on practical, production-ready best practices that apply across ASP.NET Core, Minimal APIs, and layered architectures.

1. Prefer Constructor Injection (Default Approach)

Constructor injection is the most recommended and safest form of DI in .NET.

Why it is preferred:

• Makes dependencies explicit
• Ensures required services are available at object creation
• Improves testability
• Prevents runtime null dependencies

Example:

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

Avoid:

• Property injection (hidden dependencies)
• Service locator pattern (IServiceProvider.GetService inside classes)

2. Follow the Single Responsibility Principle

Each service should do one thing and depend only on what it needs.

Bad example:

A service that handles:

• Database access
• Logging
• Email sending
• Business logic
• Better approach:

Split into:

• Repository layer (data access)
• Domain service (business logic)
• Infrastructure services (email, logging)

This reduces coupling and improves testability.

3. Use the Correct Service Lifetime

.NET DI supports three main lifetimes:

1. Transient

Created every time requested.

• Use for lightweight, stateless services

2. Scoped

One instance per request.

• Ideal for web applications
• Common for database contexts

3. Singleton

One instance for the entire application lifecycle.

• Use for stateless shared services

Common mistake:

• Registering stateful services as singletons → leads to threading issues.

4. Avoid Captive Dependencies

A "captive dependency" occurs when a longer-lived service depends on a shorter-lived one.

Example problem: Singleton depends on Scoped service

This can cause:

• Memory leaks
• Invalid state usage
• Thread safety issues

Rule: A service should never depend on a shorter lifetime service.

5. Keep the Constructor Clean

Constructors should only:

• Assign dependencies
• Not execute logic

Avoid:

• File I/O
• Database calls
• Network operations

These belong in methods, not constructors.

6. Prefer Interfaces Over Concrete Classes

Always depend on abstractions, not implementations.

Good:

public class PaymentService
{
    private readonly IPaymentGateway _gateway;
}

Bad:

public class PaymentService
{
    private readonly StripeGateway _gateway;
}

Benefits:

• Easier mocking in tests
• Swappable implementations
• Better separation of concerns

7. Keep Service Registration Organized

As applications grow, Program.cs can become messy.

Best practice: Use extension methods

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        services.AddScoped<IOrderService, OrderService>();
        return services;
    }
}

Then in Program.cs:

builder.Services.AddApplicationServices();

8. Avoid Service Locator Pattern

Do NOT do this:

var service = provider.GetService();

Why it is bad:

• Hidden dependencies
• Harder to test
• Breaks design transparency

Instead, inject dependencies directly.

9. Be Careful with Scoped Services in Background Tasks

Scoped services (like DbContext) are not safe to use in long-running background services unless properly handled.

Correct approach:

Use IServiceScopeFactory:

using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();

This ensures proper lifetime management.

10. Optimize for Testability

Good DI design naturally improves testing.

Best practices:

• Use interfaces
• Avoid static dependencies
• Keep services small and focused

Testing becomes easier with frameworks like xUnit.

11. Avoid Over-Injection (God Constructors)

If a class has too many dependencies (6+ is a warning sign), it usually means:

• The class is doing too much
• Responsibilities are not properly separated

Fix:

• Split the service
• Introduce facades or coordinators

12. Use Options Pattern for Configuration

Instead of injecting raw configuration strings:

Bad:

public MyService(IConfiguration config)

Good:

Use strongly typed options:

public class EmailSettings
{
    public string Host { get; set; }
}

Register:

services.Configure(configuration.GetSection("Email"));

Inject:

IOptions

13. Avoid Capturing Services in Static Fields

Never store DI services in static variables.

Why:

• Breaks lifetime management
• Causes memory leaks
• Introduces hidden state

14. Design for Minimal API and ASP.NET Core Equally

In Minimal APIs, DI is often passed directly into endpoint handlers:

app.MapGet("/orders", (IOrderService service) =>
{
    return service.GetAll();
});

In ASP.NET Core controllers, DI is constructor-based.

Both approaches follow the same DI container rules—only usage differs.

15. Understand the Role of DI in the Request Pipeline

In ASP.NET Core, DI is deeply integrated into:

• Middleware pipeline
• Controllers / endpoints
• Filters
• Hosted services

This makes it more than just a pattern—it is part of the runtime architecture.

Conclusion

Dependency Injection in .NET is most effective when it is:

• Simple
• Explicit
• Well-scoped
• Interface-driven
• Lifetime-safe

The goal is not to use DI everywhere, but to use it where it improves clarity and testability without introducing unnecessary complexity.

Contents related to 'Dependency Injection Best Practices (ASP.NET Core and Modern .NET)'

Entity Framework (EF)
Entity Framework (EF)
xUnit, xUnit.net
xUnit, xUnit.net
Serilog
Serilog