Dependency Injection in C#: .NET Core DI Container & Service Lifetimes
1. Dependency Injection: Concept and Benefits
Q: What is Dependency Injection (DI) in C#?
Dependency Injection (DI) is a design pattern in which a class’s dependencies (e.g., services, objects) are provided externally rather than created internally. In C#, DI is commonly used to pass dependencies via constructors, methods, or properties, promoting loose coupling and testable code. .NET Core provides a built-in DI container to manage dependency registration and resolution.
Q: What are the key components of DI?
- Dependency: A service or object a class needs to function (e.g., a logger, database context).
- Injection: Providing the dependency to the class (typically via constructor).
- Container: A system (e.g., .NET Core’s DI container) that manages dependency registration and lifecycle.
- Service Lifetime: Defines how long a dependency lives (e.g., transient, scoped, singleton).
Q: What are the benefits of Dependency Injection?
- Loose Coupling: Classes depend on abstractions (e.g., interfaces), not concrete implementations, making code flexible.
- Testability: Dependencies can be mocked or stubbed for unit testing.
- Maintainability: Easier to swap or update dependencies without changing dependent classes.
- Scalability: Simplifies managing complex dependency graphs in large applications.
- Reusability: Promotes reusable services across the application.
Q: How does DI in C#/.NET Core differ from C/C++?
- C#/.NET Core: Provides a built-in DI container (
IServiceProvider) in .NET Core, type-safe, managed, with automatic lifecycle management. - C/C++: No native DI support; developers use manual dependency passing or third-party libraries (e.g., Boost.DI in C++). Requires manual memory management.
- C# Advantage: Integrated, type-safe, with automatic disposal and lifecycle management.
2. Using Built-in DI in .NET Core
Q: What is the built-in DI container in .NET Core?
.NET Core includes a lightweight DI container (IServiceProvider) integrated into the framework, configured in the Startup class or Program.cs (in .NET 6+). It supports registering services, resolving dependencies, and managing service lifetimes:
- Transient: New instance per request.
- Scoped: Same instance within a scope (e.g., HTTP request).
- Singleton: Same instance for the application’s lifetime.
Q: How do you configure DI in .NET Core?
- Register services in
Program.cs(orStartup.csin older versions) usingIServiceCollection. - Inject dependencies via constructor injection in classes (e.g., controllers, services).
- Use interfaces to define contracts for dependencies.
Syntax (in Program.cs):
builder.Services.AddTransient<IService, Service>();
Q: What are the common DI methods in .NET Core?
AddTransient<TService, TImplementation>: Creates a new instance each time the service is requested.AddScoped<TService, TImplementation>: Creates one instance per scope (e.g., per HTTP request).AddSingleton<TService, TImplementation>: Creates a single instance for the application.AddSingleton<TService>(instance): Registers an existing instance.
Q: Can you give an example of using built-in DI in .NET Core?
Below is an example of a .NET Core console application demonstrating DI with constructor injection, service registration, and different lifetimes.
using System;
using Microsoft.Extensions.DependencyInjection;
// Interface for dependency
public interface ILogger
{
void Log(string message);
}
// Concrete implementation
public class ConsoleLogger : ILogger
{
private readonly string _instanceId;
public ConsoleLogger()
{
_instanceId = Guid.NewGuid().ToString();
}
public void Log(string message)
{
Console.WriteLine($"[{_instanceId}] {message}");
}
}
// Service using dependency
public class UserService
{
private readonly ILogger _logger;
public UserService(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void ProcessUser(string userName)
{
_logger.Log($"Processing user: {userName}");
}
}
class Program
{
static void Main(string[] args)
{
// Set up DI container
var serviceProvider = new ServiceCollection()
.AddSingleton<ILogger, ConsoleLogger>() // Singleton: same instance
.AddTransient<UserService>() // Transient: new instance each time
.BuildServiceProvider();
// Resolve and use services
try
{
// First instance
var userService1 = serviceProvider.GetService<UserService>();
userService1.ProcessUser("Krishna");
// Second instance
var userService2 = serviceProvider.GetService<UserService>();
userService2.ProcessUser("Kristal");
// Note: Same logger instance (singleton) used by both services
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
// Dispose services
(serviceProvider as IDisposable)?.Dispose();
}
}
}
Output (example):
[123e4567-e89b-12d3-a456-426614174000] Processing user: Krishna
[123e4567-e89b-12d3-a456-426614174000] Processing user: Kristal
Note: The GUID (_instanceId) will be the same for both calls, indicating the ConsoleLogger is a singleton.
Q: How do you use DI in an ASP.NET Core application?
In ASP.NET Core, DI is configured in Program.cs (or Startup.cs in older versions), and dependencies are injected into controllers, services, or middleware. Below is an example of DI in an ASP.NET Core minimal API.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
// Interface and implementation (same as above)
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
private readonly string _instanceId;
public ConsoleLogger()
{
_instanceId = Guid.NewGuid().ToString();
}
public void Log(string message)
{
Console.WriteLine($"[{_instanceId}] {message}");
}
}
public class UserService
{
private readonly ILogger _logger;
public UserService(ILogger logger)
{
_logger = logger;
}
public void ProcessUser(string userName)
{
_logger.Log($"Processing user: {userName}");
}
}
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddScoped<ILogger, ConsoleLogger>(); // Scoped lifetime
builder.Services.AddScoped<UserService>();
var app = builder.Build();
// Minimal API endpoint
app.MapGet("/process/{userName}", (string userName, UserService userService) =>
{
userService.ProcessUser(userName);
return $"Processed {userName}";
});
app.Run();
Usage: Run the app and navigate to http://localhost:5000/process/Krishna.
Output (example in console):
[456f789a-b12c-34d5-e678-901234567890] Processing user: Krishna
Note: The GUID will differ per HTTP request (scoped lifetime).
Q: How does DI in .NET Core differ from C/C++?
- .NET Core: Built-in DI container, type-safe, supports lifetimes (transient, scoped, singleton), integrated with ASP.NET Core.
- C/C++: No native DI; developers implement manual injection or use libraries (e.g., Boost.DI). Requires manual memory management.
- C# Advantage: Seamless integration, automatic disposal, and lifecycle management.
3. Common Mistakes & Best Practices
Q: Common mistakes?
Concept/Implementation:
- Creating dependencies inside classes instead of injecting them.
- Overusing concrete types instead of interfaces, reducing flexibility.
.NET Core DI:
- Choosing incorrect lifetimes (e.g., singleton for scoped services, causing state issues).
- Not disposing services properly, leading to memory leaks.
- Resolving services manually (
GetService) instead of using constructor injection. - Registering services multiple times, causing unexpected behavior.
General:
- Forgetting to handle null dependencies in constructors.
- Overcomplicating DI setup with unnecessary customizations.
Q: Best practices?
Concept/Implementation:
- Use interfaces for dependencies to ensure loose coupling.
- Prefer constructor injection over property or method injection.
- Keep service interfaces focused and minimal (single responsibility).
.NET Core DI:
- Choose appropriate lifetimes:
Transientfor stateless,Scopedfor per-request,Singletonfor shared state. - Register services in
Program.csclearly, grouping related services. - Use
usingorIDisposablefor services that manage resources (e.g., database connections). - Validate service resolution at startup with
ValidateOnBuild(ASP.NET Core).
General:
- Use dependency injection for testable, maintainable code.
- Avoid service locator pattern (manual
GetServicecalls) in favor of constructor injection. - Document service dependencies with XML comments.
- Test DI setup with unit tests, mocking dependencies as needed.
- Leverage modern C# features (e.g.,
requiredproperties for mandatory dependencies).