Dependency Injection in ASP.NET Core is one of the framework’s most powerful and foundational features. For enterprise application development, startups, and scalable SaaS platforms, DI is essential to building loosely coupled, testable, and high-performance software. 

However, without a clear understanding of DI patterns, service lifetimes, and potential pitfalls, you risk performance degradation, tight coupling, and memory leaks. 

In this blog, we’ll dive deep into the mechanics of Dependency Injection in ASP.NET Core, explore real-world usage patterns, highlight common mistakes, and provide practical performance tips to help you build scalable and maintainable applications. 

1. What Is Dependency Injection? 

Dependency Injection (DI) is a design pattern that supplies a class with its required dependencies instead of letting it create them internally. 

❌ Without DI: 

csharp 

CopyEdit 

public class OrderService 

   private readonly EmailSender _emailSender = new EmailSender(); 

 

✅ With DI: 

csharp 

CopyEdit 

public class OrderService 

   private readonly IEmailSender _emailSender; 
 
   public OrderService(IEmailSender emailSender) 
   { 
       _emailSender = emailSender; 
   } 

 

This allows better decoupling and testability—crucial for scalable and maintainable enterprise software. 

2. Built-in DI Container in ASP.NET Core 

ASP.NET Core includes a lightweight but powerful DI container: Microsoft.Extensions.DependencyInjection. 

How to Register Services: 

csharp 

CopyEdit 

services.AddTransient<IEmailSender, SmtpEmailSender>(); 
 

Service Lifetimes: 

  • Transient – A new instance every time (ideal for stateless services) 
  • Scoped – One instance per HTTP request (perfect for DbContext) 
  • Singleton – One instance for the app lifetime (must be thread-safe) 

Proper configuration of service lifetimes prevents issues like data inconsistency and runtime errors. 

3. Real-World DI Patterns in ASP.NET Core 

✅ Constructor Injection (Most Recommended) 

csharp 

CopyEdit 

public class PaymentService 

   private readonly ITransactionLogger _logger; 
 
   public PaymentService(ITransactionLogger logger) 
   { 
       _logger = logger; 
   } 

 

Benefits

  • Compile-time safety 
  • Simplified unit testing 
  • Clear dependency contracts 

✅ Options Pattern for Configurable Dependencies 

csharp 

CopyEdit 

services.Configure<SmtpSettings>(Configuration.GetSection(“Smtp”)); 
 

csharp 

CopyEdit 

public class SmtpEmailSender : IEmailSender 

   private readonly SmtpSettings _settings; 
 
   public SmtpEmailSender(IOptions<SmtpSettings> options) 
   { 
       _settings = options.Value; 
   } 

 

Ideal for injecting app configurations and environment-specific settings. 

✅ Factory-Based Injection for Runtime Resolution 

csharp 

CopyEdit 

services.AddTransient<Func<string, IParser>>(provider => type => 

   return type == “csv” ? new CsvParser() : new JsonParser(); 
}); 
 

Useful when the correct implementation depends on runtime input. 

⚠️ Service Locator Pattern (Use Sparingly) 

csharp 

CopyEdit 

var emailSender = serviceProvider.GetRequiredService<IEmailSender>(); 
 

While flexible, it couples your class to the DI container—avoid unless in middlewares or dynamic scenarios. 

4. Common Pitfalls and How to Avoid Them 

❌ Injecting Scoped Services into Singleton 

This leads to runtime issues and unpredictable behavior. 

Fix: Don’t inject scoped services like DbContext into singletons. Refactor your services or align the lifetimes correctly. 

❌ Over-Injection (God Classes) 

csharp 

CopyEdit 

public class InvoiceService( 
   ICustomerRepo c, IProductRepo p, IDiscountCalculator d, 
   IValidator v, IEmailSender e, ICache c2, ILogger l) { … } 
 

Too many constructor dependencies indicate a violation of the Single Responsibility Principle

Fix: Break into smaller, focused services or use aggregators when logical. 

❌ Memory Leaks from Captured Scoped Services 

Holding onto scoped services (like IDbContext) in background services can lead to memory leaks. 

Fix

csharp 

CopyEdit 

using var scope = _scopeFactory.CreateScope(); 
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>(); 
 

Use IServiceScopeFactory to handle scoped dependencies in background tasks. 

5. Performance Optimization Tips for ASP.NET Core DI 

  • Constructor injection is fastest—avoid property injection. 
  • Limit use of reflection—scan assemblies at startup only. 
  • Avoid creating scopes in hot paths—e.g., middlewares or controllers. 
  • Use TryAdd in libraries to prevent clashing registrations: 

csharp 

CopyEdit 

services.TryAddScoped<IMyService, MyService>(); 
 

Efficient service registration leads to faster startup and request processing times. 

6. Testing with Dependency Injection 

✅ Unit Testing with Mocks: 

csharp 

CopyEdit 

var mockLogger = new Mock<ITransactionLogger>(); 
var service = new PaymentService(mockLogger.Object); 
 

✅ Integration Testing with WebApplicationFactory: 

csharp 

CopyEdit 

factory.WithWebHostBuilder(builder => 

   builder.ConfigureServices(services => 
   { 
       services.Remove(…); 
       services.AddScoped<IDummyService, MockDummyService>(); 
   }); 
}); 
 

Proper DI configuration helps simulate real environments effectively in tests. 

7. Replacing the Default DI Container 

Need advanced features like decorators, modular containers, or interceptors? Replace ASP.NET Core’s DI container with: 

  • Autofac 
  • StructureMap 
  • SimpleInjector 

csharp 

CopyEdit 

.UseServiceProviderFactory(new AutofacServiceProviderFactory()) 
 

Do this only when the built-in container can’t meet your design requirements. 

8. Use Case: Scalable Multi-Tenant SaaS Platform 

We partnered with a growing SaaS startup that needed full tenant isolation—including database, cache, and configuration. 

Solution: 

  • Created a custom ITenantProvider 
  • Injected tenant-aware services using Scoped lifetimes 
  • Used middleware to resolve tenant context before controller execution 

Result: A clean, DI-driven architecture that scaled to 1000+ tenants with zero performance compromise. 

Conclusion: Mastering Dependency Injection in ASP.NET Core 

Dependency Injection in ASP.NET Core is not just a technique—it’s an architectural mindset that encourages clean, testable, and high-performance systems. But like any tool, it must be used carefully. 

✅ TL;DR: 

  • Prefer constructor injection over other methods. 
  • Always align service lifetimes correctly (Scoped vs Singleton). 
  • Use Options pattern and factory injection for flexibility. 
  • Avoid over-injection and tight coupling
  • Monitor DI performance and memory for scalability. 

Additional Resources: