Dependency Injection
Dependency Injection (DI) Interview Preparation for .NET Core Developers (3+ Years Experience)
This guide covers key Dependency Injection concepts, best practices, and common interview questions for experienced .NET Core developers. Focus on practical understanding and real-world application scenarios.
Understanding DI Containers
Built-in DI Container
.NET Core provides a built-in DI container through Microsoft.Extensions.DependencyInjection. It's lightweight and suitable for most applications.
Key Features:
- Service registration via
IServiceCollection - Automatic resolution of dependencies
- Support for service lifetimes
- Integration with ASP.NET Core
Basic Usage:
// In Startup.cs or Program.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMyService, MyService>();
services.AddScoped<IRepository, Repository>();
services.AddSingleton<ILogger, Logger>();
}
Third-Party Containers (e.g., Autofac)
For complex scenarios, third-party containers like Autofac offer advanced features:
- Property injection
- Method injection
- Module-based registration
- Better performance in large applications
Autofac Example:
// In Startup.cs
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
builder.RegisterType<Repository>().As<IRepository>().InstancePerRequest();
}
Interview Questions:
- When would you choose Autofac over the built-in container?
- How does Autofac's module system improve maintainability?
- Explain the performance differences between built-in DI and Autofac.
Service Lifetimes
Transient
- New instance created each time the service is requested
- Shortest lifetime, suitable for lightweight, stateless services
- Use for services that don't maintain state between requests
Example:
services.AddTransient<IMyService, MyService>();
Scoped
- Instance created once per request/scope
- Shared within the same request but not across requests
- Ideal for services that need to maintain state within a single operation
Example:
services.AddScoped<IRepository, Repository>();
Singleton
- Single instance created for the entire application lifetime
- Shared across all requests and users
- Use for thread-safe, stateless services or shared resources
Example:
services.AddSingleton<ILogger, Logger>();
Common Pitfalls:
- Captive dependencies (longer-lived service depending on shorter-lived service)
- Thread safety issues with singletons
- Memory leaks from holding references
Interview Questions:
- What happens if a Singleton service depends on a Scoped service?
- How do you handle thread safety in Singleton services?
- When should you use each lifetime type in a web application?
Best Practices for DI in Large Applications
Service Registration Organization
- Group related services in extension methods
- Use assembly scanning for automatic registration
- Implement proper naming conventions
Extension Method Example:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddTransient<IMyService, MyService>();
services.AddScoped<IRepository, Repository>();
return services;
}
}
Dependency Inversion Principle
- Depend on abstractions, not concretions
- Use interfaces for all injectable services
- Avoid concrete class dependencies in constructors
Avoiding Anti-Patterns
- Don't use DI for data transfer objects (DTOs)
- Avoid service locator pattern
- Don't inject too many dependencies (more than 5-7 per class)
Testing Considerations
- Use DI to enable easy mocking in unit tests
- Register test doubles in test startup configurations
- Ensure services are designed for testability
Interview Questions:
- How do you organize service registrations in a large enterprise application?
- What are the signs of DI abuse in a codebase?
- How does DI facilitate unit testing?
Custom Service Registration and Resolution
Custom Registration Patterns
- Factory methods for complex instantiation logic
- Conditional registration based on configuration
- Named registrations for multiple implementations
Factory Method Example:
services.AddTransient<IMyService>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
if (config.GetValue<bool>("UseAdvancedService"))
{
return new AdvancedService();
}
return new BasicService();
});
Custom Resolution
- Using
IServiceProviderfor manual resolution (avoid when possible) - Service locator pattern (use sparingly)
- Keyed services for runtime selection
Keyed Services Example (in .NET 8+):
services.AddKeyedTransient<IMyService>("basic", (provider, key) => new BasicService());
services.AddKeyedTransient<IMyService>("advanced", (provider, key) => new AdvancedService());
public class Consumer
{
public Consumer([FromKeyedServices("advanced")] IMyService service)
{
// Uses advanced service
}
}
Advanced Scenarios
- Open generics registration
- Decorators for cross-cutting concerns
- Lazy initialization
Open Generics Example:
services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
Decorator Pattern:
services.AddTransient<IMyService, LoggingDecorator>();
services.AddTransient<IMyService, CachingDecorator>();
services.AddTransient<IMyService, MyService>();
Interview Questions:
- How would you implement a factory pattern using DI?
- Explain the decorator pattern with DI containers.
- When should you use manual service resolution vs constructor injection?
Middleware and ASP.NET Core DI Integration
Middleware in ASP.NET Core is integral to the request pipeline and heavily relies on Dependency Injection for service resolution.
Built-in Middleware Examples
Static Files Middleware (UseStaticFiles): Serves static files like CSS, JS, images. Uses DI to access configuration and file providers.
// Legacy .NET Core 3.1
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles(); // Basic static file serving
}
// Latest .NET 8+ (Program.cs)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseStaticFiles(); // Still basic, but can be configured
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")),
RequestPath = "/static"
});
Routing Middleware (UseRouting): Enables endpoint routing. Must be called before UseEndpoints.
// Legacy .NET Core 3.1
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
// Latest .NET 8+ (Program.cs)
var app = builder.Build();
app.UseRouting();
app.MapControllers(); // Simplified endpoint mapping
Authentication Middleware (UseAuthentication): Authenticates requests using configured authentication schemes. Uses DI to resolve authentication services.
// Legacy .NET Core 3.1
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* config */ });
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseAuthorization();
}
// Latest .NET 8+ (Program.cs)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* config */ });
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Custom Middleware with DI
Custom middleware can inject services through constructor injection.
Example: Logging Middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger; // Injected via DI
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation($"Request: {context.Request.Path}");
await _next(context);
}
}
// Extension method for registration
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// Usage in Program.cs
app.UseRequestLogging();
Interview Questions on Middleware:
- Explain the order of middleware execution and why it matters.
- How does DI work in custom middleware?
- What are the differences between UseStaticFiles, UseRouting, and UseAuthentication?
- How do you handle exceptions in middleware using DI?
Other Important DI-Related Components
Logging
ASP.NET Core's logging framework uses DI extensively.
// Service registration
builder.Services.AddLogging(config =>
{
config.AddConsole();
config.AddDebug();
});
// Injection
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
}
Configuration and Options Pattern
Strongly-typed configuration using IOptions.
// Configuration class
public class AppSettings
{
public string ConnectionString { get; set; }
public int MaxRetries { get; set; }
}
// Registration
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
// Injection
public class MyService
{
private readonly AppSettings _settings;
public MyService(IOptions<AppSettings> options)
{
_settings = options.Value;
}
}
Health Checks
Built-in health check services using DI.
builder.Services.AddHealthChecks()
.AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
.AddCheck<CustomHealthCheck>("custom");
public class CustomHealthCheck : IHealthCheck
{
// Injected dependencies
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken token)
{
// Health check logic
}
}
Background Services (Hosted Services)
For long-running background tasks.
public class BackgroundWorker : BackgroundService
{
private readonly ILogger<BackgroundWorker> _logger;
public BackgroundWorker(ILogger<BackgroundWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
// Registration
builder.Services.AddHostedService<BackgroundWorker>();
HttpClient Factory
For managing HttpClient instances with DI.
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
// Injection
public class ApiService
{
private readonly IHttpClientFactory _clientFactory;
public ApiService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetDataAsync()
{
var client = _clientFactory.CreateClient("api");
return await client.GetStringAsync("/data");
}
}
Interview Questions on DI-Related Components:
- How do you configure and use the Options pattern?
- Explain the benefits of HttpClientFactory over creating HttpClient instances directly.
- How do background services integrate with the DI container?
- What are health checks and how do they use DI?
Common Interview Scenarios and Code Challenges
Scenario 1: Circular Dependencies
Question: How do you detect and resolve circular dependencies?
Answer: Use proper abstraction layers, refactor to use events or mediator pattern, or use property injection carefully.
Scenario 2: Performance Optimization
Question: How do you optimize DI container performance in high-traffic applications?
Answer: Use singleton lifetimes where appropriate, avoid reflection-heavy operations, consider third-party containers for complex scenarios.
Scenario 3: Testing with DI
Question: How do you write unit tests for classes with many dependencies?
Answer: Use mocking frameworks, create test-specific registrations, consider facade pattern to reduce dependencies.
Interview Questions
Basic Level (1-2 years exp)
Q1. What is Dependency Injection and why do we need it?
Answer: Dependency Injection (DI) is a design pattern where dependencies are provided to a class rather than the class creating them itself. It promotes loose coupling, testability, and maintainability.
Why we need it:
- Loose Coupling: Classes don't create their own dependencies
- Testability: Easy to mock dependencies in unit tests
- Maintainability: Changes to dependencies don't affect consuming classes
- Reusability: Dependencies can be reused across different classes
- Configuration: Dependencies can be configured externally
Q2. What are the different service lifetimes in .NET Core DI?
Answer:
- Transient: New instance created each time requested
- Scoped: Instance created once per request/scope
- Singleton: Single instance for entire application lifetime
Q3. What is the difference between Transient and Scoped lifetimes?
Answer:
- Transient: Always creates new instance, suitable for lightweight stateless services
- Scoped: Same instance within a request, different across requests, good for services needing state within a single operation
- Singleton: Same instance for entire app, thread-safe stateless services
Q4. How do you register services in .NET Core?
Answer:
// In Startup.cs or Program.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMyService, MyService>();
services.AddScoped<IRepository, Repository>();
services.AddSingleton<ILogger, Logger>();
}
Q5. What is IServiceCollection and IServiceProvider?
Answer:
- IServiceCollection: Interface for registering services, used in ConfigureServices
- IServiceProvider: Interface for resolving services, used at runtime
Q6. How do you inject dependencies in a controller?
Answer:
public class HomeController : Controller
{
private readonly IMyService _myService;
public HomeController(IMyService myService)
{
_myService = myService;
}
}
Q7. What happens if you try to resolve a service that isn't registered?
Answer: You get an InvalidOperationException at runtime when trying to resolve the service.
Q8. What is the built-in DI container called?
Answer: Microsoft.Extensions.DependencyInjection
Intermediate Level (2-3 Years Experience)
Q9. Explain the concept of captive dependencies and how to avoid them.
Answer: Captive dependencies occur when a longer-lived service depends on a shorter-lived service, causing the shorter-lived service to live longer than intended.
Example of captive dependency:
services.AddSingleton<IServiceA, ServiceA>(); // Longer-lived
services.AddTransient<IServiceB, ServiceB>(); // Shorter-lived
public class ServiceA : IServiceA
{
public ServiceA(IServiceB serviceB) { } // IServiceB lives as long as IServiceA
}
How to avoid:
- Ensure dependency lifetimes are compatible (shorter-lived can depend on longer-lived, but not vice versa)
- Use factories or service locators if necessary
- Review dependency chains regularly
Q10. What are the benefits of third-party DI containers like Autofac?
Answer:
- Property Injection: Inject dependencies into properties
- Method Injection: Inject dependencies into methods
- Advanced Lifetime Management: More complex lifetime scenarios
- Module-based Registration: Organize registrations in modules
- Better Performance: Optimized for complex scenarios
- Convention-based Registration: Auto-register based on naming conventions
Q11. How do you implement property injection in Autofac?
Answer:
builder.RegisterType<MyService>()
.As<IMyService>()
.PropertiesAutowired(); // Enable property injection
public class MyService : IMyService
{
public ILogger Logger { get; set; } // Will be injected
}
Q12. What is the Options pattern and how does it work with DI?
Answer: The Options pattern provides strongly-typed access to configuration settings.
// Configuration class
public class AppSettings
{
public string ConnectionString { get; set; }
}
// Registration
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
// Injection
public class MyService
{
private readonly AppSettings _settings;
public MyService(IOptions<AppSettings> options)
{
_settings = options.Value;
}
}
Q13. How do you handle configuration validation with the Options pattern?
Answer:
builder.Services.AddOptions<AppSettings>()
.Bind(builder.Configuration.GetSection("AppSettings"))
.ValidateDataAnnotations()
.Validate(settings =>
{
// Custom validation logic
return !string.IsNullOrEmpty(settings.ConnectionString);
});
Q14. What is HttpClientFactory and why should you use it?
Answer: HttpClientFactory manages HttpClient instances and provides benefits like:
- Automatic handler management: Manages handler lifetime
- Named clients: Configure different clients for different purposes
- Typed clients: Inject strongly-typed clients
- Polly integration: Built-in resilience policies
// Named client
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
// Typed client
builder.Services.AddHttpClient<IMyApiClient, MyApiClient>();
public class MyApiClient : IMyApiClient
{
private readonly HttpClient _client;
public MyApiClient(HttpClient client)
{
_client = client;
}
}
Q15. How do you implement background services with DI?
Answer:
public class BackgroundWorker : BackgroundService
{
private readonly ILogger<BackgroundWorker> _logger;
public BackgroundWorker(ILogger<BackgroundWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running");
await Task.Delay(1000, stoppingToken);
}
}
}
// Registration
builder.Services.AddHostedService<BackgroundWorker>();
Q16. What are health checks and how do they integrate with DI?
Answer: Health checks monitor application health and dependencies.
builder.Services.AddHealthChecks()
.AddSqlServer(builder.Configuration.GetConnectionString("Default"))
.AddCheck<CustomHealthCheck>("custom");
public class CustomHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken token)
{
// Health check logic
return Task.FromResult(HealthCheckResult.Healthy());
}
}
Q17. How do you organize service registrations in a large application?
Answer:
- Extension Methods: Group related services
- Assembly Scanning: Auto-register based on conventions
- Modules: Use third-party containers with modules
- Separate Registration Classes: Dedicated classes for registration logic
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddTransient<IMyService, MyService>();
services.AddScoped<IRepository, Repository>();
return services;
}
}
Q18. What is the difference between constructor injection and property injection?
Answer:
- Constructor Injection: Dependencies provided through constructor, preferred approach
- Property Injection: Dependencies set on properties after construction, less common in .NET Core
Constructor Injection (Recommended):
public class MyService
{
public MyService(ILogger logger) { }
}
Property Injection:
public class MyService
{
public ILogger Logger { get; set; }
}
Q19. How do you handle circular dependencies?
Answer:
- Refactor: Break circular dependencies by introducing interfaces or events
- Property Injection: Use property injection for one of the dependencies
- Service Locator: Use IServiceProvider to resolve dependencies manually (avoid if possible)
- Mediator Pattern: Use events or mediator to break direct dependencies
Q20. What is the IServiceScope and when would you use it?
Answer: IServiceScope creates a scope for resolving scoped services outside of a request context.
using (var scope = serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
// Use scoped service
}
Use cases:
- Background tasks
- Console applications
- Testing scenarios
Advance Level (3+ year Exp)
Q21. How do you implement custom middleware with DI?
Answer:
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation($"Request: {context.Request.Path}");
await _next(context);
}
}
// Extension method
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
Q22. Explain the middleware pipeline and how DI fits in.
Answer: Middleware components form a pipeline that processes HTTP requests. Each middleware can:
- Handle the request completely
- Pass to next middleware
- Modify request/response
DI provides services to middleware through constructor injection. The service provider is available during middleware construction.
Q23. What are the key differences between UseStaticFiles, UseRouting, and UseAuthentication?
Answer:
- UseStaticFiles: Serves static files, uses file providers (DI injected)
- UseRouting: Enables endpoint routing, must come before UseEndpoints
- UseAuthentication: Authenticates requests using configured schemes, uses authentication services (DI injected)
Q24. How do you implement factory patterns with DI?
Answer:
services.AddTransient<IMyService>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
if (config.GetValue<bool>("UseAdvancedService"))
{
return new AdvancedService();
}
return new BasicService();
});
Q25. What are keyed services and how do you use them?
Answer: Keyed services (available in .NET 8+) allow multiple implementations of the same interface.
builder.Services.AddKeyedTransient<IMyService>("basic", (provider, key) => new BasicService());
builder.Services.AddKeyedTransient<IMyService>("advanced", (provider, key) => new AdvancedService());
public class Consumer
{
public Consumer([FromKeyedServices("advanced")] IMyService service) { }
}
Q26. How do you implement the decorator pattern with DI?
Answer:
services.AddTransient<IMyService, LoggingDecorator>();
services.AddTransient<IMyService, CachingDecorator>();
services.AddTransient<IMyService, MyService>();
Q27. How do you handle open generics with DI?
Answer:
services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
services.AddTransient(typeof(IService<>), typeof(Service<>));
// Usage
public class Consumer
{
public Consumer(IRepository<User> userRepo, IService<Product> productService) { }
}
Q28. What is the difference between AddTransient, AddScoped, and AddSingleton?
Answer:
- AddTransient: Registers with Transient lifetime
- AddScoped: Registers with Scoped lifetime
- AddSingleton: Registers with Singleton lifetime
All methods register the service in the container with the specified lifetime.
Q29. How do you implement conditional service registration?
Answer:
if (builder.Environment.IsDevelopment())
{
services.AddTransient<IMyService, DebugService>();
}
else
{
services.AddTransient<IMyService, ProductionService>();
}
Q30. How do you test classes with DI dependencies?
Answer:
public class MyServiceTests
{
[Fact]
public void Test_DoWork()
{
// Arrange
var loggerMock = new Mock<ILogger<MyService>>();
var service = new MyService(loggerMock.Object);
// Act
service.DoWork();
// Assert
// Verify behavior
}
}
Additional Interview Tips
Q31. What are some common DI anti-patterns?
Answer:
- Service Locator: Using IServiceProvider directly in classes
- Concrete Dependencies: Injecting concrete classes instead of interfaces
- Over-injection: Too many dependencies in a single class
- Circular Dependencies: Classes depending on each other
- Lifetime Mismatches: Wrong lifetime combinations
Q32. How do you handle cross-cutting concerns with DI?
Answer:
- Decorators: Wrap services with cross-cutting functionality
- Interceptors: Use dynamic proxies (with third-party containers)
- Middleware: For HTTP-related concerns
- Scrutor: For decoration in .NET Core
Q33. What is Scrutor and how does it help with DI?
Answer: Scrutor is a library that adds assembly scanning and decoration capabilities to the built-in DI container.
services.Scan(scan => scan
.FromAssemblyOf<IMyService>()
.AddClasses(classes => classes.AssignableTo<IMyService>())
.AsImplementedInterfaces()
.WithTransientLifetime());
Best Practices
- Always depend on abstractions: Use interfaces, not concrete classes
- Use appropriate lifetimes: Choose the right lifetime for each service
- Avoid service locator pattern: Don't inject IServiceProvider unless necessary
- Keep constructors clean: Limit to 3-5 dependencies per class
- Use extension methods: Organize registrations logically
- Validate configuration: Use Options validation
- Test with DI: Design for testability
- Monitor for captive dependencies: Review lifetime compatibility
- Use HttpClientFactory: For HTTP client management
- Consider third-party containers: For complex scenarios
Common Pitfalls
- Singleton with scoped dependencies: Causes captive dependencies
- Not disposing services: Memory leaks with disposable services
- Circular references: Classes depending on each other
- Overusing singletons: Thread safety issues
- Ignoring async disposal: Services implementing IDisposable
- Hardcoding registrations: Instead of configuration-driven
- Not testing DI configuration: Integration tests for container setup
- Mixing lifetimes incorrectly: Shorter-lived depending on longer-lived
- Service locator in constructors: Instead of proper injection
- Ignoring performance: Reflection overhead in complex scenarios
Performance Tips
- Use singletons appropriately: For thread-safe stateless services
- Avoid reflection-heavy operations: In hot paths
- Consider third-party containers: For performance-critical apps
- Use factory methods: For complex instantiation logic
- Minimize service resolution: In tight loops
- Profile DI performance: Use diagnostic tools
- Use keyed services: To avoid conditional logic
- Implement proper disposal: For services with resources
- Cache resolved services: When appropriate
- Monitor memory usage: Watch for memory leaks
Key Takeaways for Interviews
Demonstrate Practical Knowledge: Be ready to explain real-world usage and trade-offs.
Understand Trade-offs: Know when to use built-in vs third-party containers.
Focus on Maintainability: Emphasize how DI improves code organization and testability.
Know the Pitfalls: Be able to discuss common mistakes and how to avoid them.
Stay Updated: Mention newer features like keyed services in .NET 8+.
Remember to prepare code examples and be ready to discuss architectural decisions related to DI in your past projects.
Comments
Post a Comment