L'injection de dépendances (DI) est devenue un pilier fondamental du développement moderne en .NET. Cette pratique, fortement encouragée par Microsoft, permet de créer des applications modulaires, testables et maintenables. Dans cet article, nous allons explorer en détail comment architecturer efficacement l'injection de dépendances dans vos projets .NET.
Les fondamentaux de l'injection de dépendances en .NET
L'injection de dépendances est un pattern d'inversion de contrôle (IoC) qui permet de découpler les composants d'une application. En .NET, le conteneur DI natif fournit trois durées de vie principales pour les services :
- Transient : Une nouvelle instance est créée à chaque injection
- Scoped : Une instance par scope (typiquement une requête HTTP)
- Singleton : Une seule instance partagée pour toute l'application
Configuration de base
public void ConfigureServices(IServiceCollection services)
{
// Services de base
services.AddTransient();
services.AddScoped();
services.AddSingleton();
}
Architecture en couches avec DI
Une architecture bien pensée sépare les préoccupations en couches distinctes. Voici un exemple d'organisation type :
// Interface de repository
public interface ICustomerRepository
{
Task GetByIdAsync(int id);
}
// Implémentation du repository
public class CustomerRepository : ICustomerRepository
{
private readonly DbContext _context;
public CustomerRepository(DbContext context)
{
_context = context;
}
public async Task GetByIdAsync(int id)
{
return await _context.Customers.FindAsync(id);
}
}
// Service métier
public class CustomerService
{
private readonly ICustomerRepository _repository;
private readonly ILogger _logger;
public CustomerService(
ICustomerRepository repository,
ILogger logger)
{
_repository = repository;
_logger = logger;
}
}
Bonnes pratiques et patterns avancés
Utilisation du pattern Factory
public interface IServiceFactory
{
T Create(string type);
}
public class PaymentServiceFactory : IServiceFactory
{
private readonly IServiceProvider _serviceProvider;
public PaymentServiceFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IPaymentService Create(string type)
{
return type switch
{
"stripe" => _serviceProvider.GetService(),
"paypal" => _serviceProvider.GetService(),
_ => throw new ArgumentException("Type de paiement non supporté")
};
}
}
Modules d'enregistrement
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructureServices(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddScoped();
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
return services;
}
}
Gestion des erreurs et validation
public class ServiceResult
{
public bool Success { get; set; }
public T Data { get; set; }
public string Error { get; set; }
}
public class CustomerService
{
private readonly ICustomerRepository _repository;
private readonly IValidator _validator;
public async Task> CreateCustomerAsync(Customer customer)
{
var validationResult = await _validator.ValidateAsync(customer);
if (!validationResult.IsValid)
{
return new ServiceResult
{
Success = false,
Error = string.Join(", ", validationResult.Errors)
};
}
try
{
var result = await _repository.CreateAsync(customer);
return new ServiceResult
{
Success = true,
Data = result
};
}
catch (Exception ex)
{
// Logging...
return new ServiceResult
{
Success = false,
Error = "Une erreur est survenue lors de la création du client"
};
}
}
}
Tests unitaires
public class CustomerServiceTests
{
private readonly Mock _repositoryMock;
private readonly Mock> _validatorMock;
private readonly CustomerService _service;
public CustomerServiceTests()
{
_repositoryMock = new Mock();
_validatorMock = new Mock>();
_service = new CustomerService(_repositoryMock.Object, _validatorMock.Object);
}
[Fact]
public async Task CreateCustomer_WithValidData_ReturnsSuccess()
{
// Arrange
var customer = new Customer { Name = "Test" };
_validatorMock.Setup(v => v.ValidateAsync(It.IsAny()))
.ReturnsAsync(new ValidationResult());
_repositoryMock.Setup(r => r.CreateAsync(It.IsAny()))
.ReturnsAsync(customer);
// Act
var result = await _service.CreateCustomerAsync(customer);
// Assert
Assert.True(result.Success);
Assert.Equal(customer, result.Data);
}
}
Considérations de performance
Pour optimiser les performances de votre application utilisant l'injection de dépendances :
- Évitez l'injection de dépendances en cascade excessive
- Utilisez le lifetime approprié pour chaque service
- Implémentez IDisposable pour libérer les ressources correctement
- Considérez l'utilisation de Lazy
pour le chargement différé
public class ExpensiveService : IDisposable
{
private bool _disposed;
private readonly IDbConnection _connection;
public ExpensiveService(IDbConnection connection)
{
_connection = connection;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_connection?.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Conclusion
L'injection de dépendances est un outil puissant qui, bien utilisé, permet de créer des applications .NET robustes et maintenables. Les points clés à retenir sont :
- Choisir les bons lifetimes pour vos services
- Organiser votre code en modules cohérents
- Utiliser des patterns appropriés comme Factory quand nécessaire
- Implémenter une gestion d'erreurs robuste
- Écrire des tests unitaires pour valider votre architecture
En suivant ces principes et bonnes pratiques, vous pourrez créer des applications .NET modernes, évolutives et faciles à maintenir.