La Dependency Injection (DI) est devenue un pilier fondamental du développement moderne en .NET. Si les concepts de base sont largement connus, les aspects avancés de cette technique méritent une attention particulière pour construire des applications robustes et maintenables. Dans cet article, nous explorerons les concepts avancés de la DI et leur mise en œuvre pratique dans l'écosystème .NET moderne.
Concepts théoriques avancés
Au-delà de l'injection de dépendances basique, plusieurs concepts avancés méritent notre attention :
- Durée de vie des services (Scoped, Singleton, Transient)
- Injection de dépendances conditionnelles
- Décorateurs et chaînes de responsabilité
- Factory patterns avec DI
Implémentation des durées de vie avancées
public interface IService { }
// Service de base
public class MyService : IService { }
// Configuration dans Program.cs
services.AddSingleton(); // Une seule instance pour toute l'application
services.AddScoped(); // Une instance par scope (ex: par requête HTTP)
services.AddTransient(); // Nouvelle instance à chaque injection
Injection conditionnelle
L'injection conditionnelle permet de choisir dynamiquement quelle implémentation utiliser selon le contexte :
public class ServiceResolver
{
private readonly IServiceProvider _serviceProvider;
public ServiceResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IService ResolveService(ServiceType type)
{
return type switch
{
ServiceType.Production => _serviceProvider.GetService(),
ServiceType.Development => _serviceProvider.GetService(),
_ => throw new ArgumentException("Invalid service type")
};
}
}
Patterns avancés de DI
Decorator Pattern avec DI
public interface INotificationService
{
Task SendNotificationAsync(string message);
}
public class LoggingDecorator : INotificationService
{
private readonly INotificationService _inner;
private readonly ILogger _logger;
public LoggingDecorator(
INotificationService inner,
ILogger logger)
{
_inner = inner;
_logger = logger;
}
public async Task SendNotificationAsync(string message)
{
_logger.LogInformation("Sending notification: {Message}", message);
await _inner.SendNotificationAsync(message);
_logger.LogInformation("Notification sent successfully");
}
}
Gestion des erreurs et performance
La gestion appropriée des erreurs dans un système utilisant DI est cruciale :
public class ServiceCollection
{
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton();
// Gestion des erreurs avec circuit breaker
services.AddTransient(sp =>
{
try
{
var service = sp.GetRequiredService();
return new CircuitBreakerDecorator(service);
}
catch (Exception ex)
{
// Fallback service
return new FallbackService();
}
});
}
}
Tests unitaires avec DI
Les tests unitaires doivent prendre en compte la DI :
public class NotificationServiceTests
{
[Fact]
public async Task SendNotification_ShouldLogAndSendMessage()
{
// Arrange
var mockLogger = new Mock>();
var mockInnerService = new Mock();
var decorator = new LoggingDecorator(
mockInnerService.Object,
mockLogger.Object);
// Act
await decorator.SendNotificationAsync("Test message");
// Assert
mockInnerService.Verify(
x => x.SendNotificationAsync("Test message"),
Times.Once);
mockLogger.Verify(
x => x.LogInformation(It.IsAny(), "Test message"),
Times.Once);
}
}
Bonnes pratiques
- Éviter la Service Locator Pattern en faveur de l'injection de constructeur
- Utiliser des interfaces pour découpler les composants
- Respecter le principe de responsabilité unique
- Documenter les durées de vie des services
- Éviter les dépendances circulaires
Cas d'usage réels
Voici un exemple complet d'une architecture utilisant la DI avancée :
public interface IUserService
{
Task GetUserAsync(int id);
}
public class CachingUserService : IUserService
{
private readonly IUserService _inner;
private readonly ICache _cache;
private readonly ILogger _logger;
public CachingUserService(
IUserService inner,
ICache cache,
ILogger logger)
{
_inner = inner;
_cache = cache;
_logger = logger;
}
public async Task GetUserAsync(int id)
{
var cacheKey = $"user_{id}";
if (_cache.TryGet(cacheKey, out User user))
{
_logger.LogInformation("Cache hit for user {Id}", id);
return user;
}
user = await _inner.GetUserAsync(id);
await _cache.SetAsync(cacheKey, user);
return user;
}
}
// Configuration
services.AddScoped();
services.Decorate();
Conclusion
La maîtrise des concepts avancés de la Dependency Injection permet de construire des applications plus modulaires, testables et maintenables. Les patterns et pratiques présentés ici constituent une base solide pour implémenter la DI de manière efficace dans vos projets .NET. N'oubliez pas que la DI n'est qu'un outil parmi d'autres et doit être utilisée judicieusement en fonction des besoins spécifiques de votre application.