Initialisation et Configuration d'une API Web .NET 6+ avec Injection Automatique

La mise en place d'un projet API Web avec .NET 6 et les versions ultérieures repose sur le modèle d'hébergement minimal. Voici comment structurer l'initialisation du pipeline et automatiser l'enregistrement des dépendances via la réflexion.

Configuration du point d'entrée (Program.cs)

Le fichier principal orchestre l'ajout des services et la configuration du middleware. Dans cet exemple, nous configurons les contrôleurs, la gestion des sessions, la protection des données et notre propre mécanisme d'injection de dépendances.


public class Program
{
    public static void Main(string[] arguments)
    {
        var webBuilder = WebApplication.CreateBuilder(arguments);

        // Enregistrement des services de base
        webBuilder.Services.AddControllers();
        webBuilder.Services.AddEndpointsApiExplorer();
        
        // Configuration de l'accès au contexte HTTP et des sessions
        webBuilder.Services.AddHttpContextAccessor();
        webBuilder.Services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromMinutes(30);
        });

        // Protection des données pour éviter les avertissements de cookies
        webBuilder.Services.AddDataProtection()
            .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(AppContext.BaseDirectory, "KeyRings")));

        // Liaison des configurations personnalisées
        webBuilder.Services.Configure<ApplicationSettings>(webBuilder.Configuration.GetSection("AppConfig"));

        // Appel de notre extension pour l'injection automatique
        webBuilder.Services.RegisterApplicationServices();

        // Configuration de la base de données
        DatabaseConfigurator.Initialize(webBuilder.Configuration);

        // Configuration de Swagger
        webBuilder.Services.AddSwaggerDocumentation();

        var application = webBuilder.Build();
        GlobalServiceProvider.Container = application.Services;

        application.UseSwagger();
        application.UseSwaggerUI();

        if (!application.Environment.IsDevelopment())
        {
            application.UseExceptionHandler("/error/global");
        }

        application.UseStaticFiles();
        application.UseRouting();
        application.UseSession();
        application.UseAuthorization();
        application.UseResponseCaching();
        
        // Middleware personnalisé pour la gestion des erreurs
        application.UseMiddleware<GlobalErrorHandlingMiddleware>();

        application.MapControllers();
        
        application.Run();
    }
}

Automatisation de l'injection de dépendances

Pour éviter d'enregistrer manuellement chaque service, nous créons une classe d'extension qui utilise la réflexion pour analyser les assemblys et enregistrer les classes marquées par un attribut personnalisé.


public static class DependencyInjectionExtensions
{
    /// <summary>
    /// Enregistre tous les services marqués avec l'attribut AutoInject dans les assemblys spécifiés.
    /// </summary>
    public static void RegisterApplicationServices(this IServiceCollection serviceCollection)
    {
        string[] targetAssemblies = { "MyApp.Infrastructure", "MyApp.Application" };
        
        foreach (var assemblyName in targetAssemblies)
        {
            var assembly = Assembly.Load(assemblyName);
            ScanAndRegister(serviceCollection, assembly);
        }
    }

    private static void ScanAndRegister(IServiceCollection serviceCollection, Assembly assembly)
    {
        var types = assembly.GetTypes();

        foreach (var implementationType in types)
        {
            var injectAttribute = implementationType.GetCustomAttribute<AutoInjectAttribute>();

            if (injectAttribute != null)
            {
                Type contractType = injectAttribute.ContractType;

                // Si aucun contrat n'est spécifié mais que la résolution d'interface est activée
                if (contractType == null && injectAttribute.ResolveInterface)
                {
                    contractType = implementationType.GetInterfaces().FirstOrDefault();
                }

                // Fallback sur le type d'implémentation lui-même
                if (contractType == null)
                {
                    contractType = implementationType;
                }

                // Enregistrement basé sur le cycle de vie défini
                switch (injectAttribute.Scope)
                {
                    case InjectionScope.Singleton:
                        serviceCollection.AddSingleton(contractType, implementationType);
                        break;
                    case InjectionScope.Scoped:
                        serviceCollection.AddScoped(contractType, implementationType);
                        break;
                    case InjectionScope.Transient:
                        serviceCollection.AddTransient(contractType, implementationType);
                        break;
                    default:
                        serviceCollection.AddTransient(contractType, implementationType);
                        break;
                }
            }
        }
    }
}

Définition des attributs et des couches de données

Nous définissons un attribut personnalisé pour contrôler le cycle de vie des services, ainsi que les classes de base pour le dépôt (Repository) et le service.


/// <summary>
/// Attribut utilisé pour marquer les classes nécessitant une injection automatique.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class AutoInjectAttribute : Attribute
{
    public InjectionScope Scope { get; set; } = InjectionScope.Scoped;
    public Type ContractType { get; set; }
    public bool ResolveInterface { get; set; }
}

public enum InjectionScope
{
    Transient, // Une nouvelle instance à chaque demande
    Scoped,    // Une instance par requête web
    Singleton  // Une instance unique pour toute l'application
}

// Interfaces et classes de base pour l'accès aux données
public interface IGenericRepository<TEntity> where TEntity : class, new()
{
    // Méthodes CRUD et gestion des transactions
}

public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class, new()
{
    private readonly IDbContext _dbContext;

    public GenericRepository(IDbContext dbContext)
    {
        _dbContext = dbContext;
        // Initialisation du contexte et configuration multi-tenant
    }
}

// Interfaces et classes de base pour la logique métier
public interface IGenericService<TEntity> : IGenericRepository<TEntity> where TEntity : class, new() { }

public class GenericService<TEntity> : GenericRepository<TEntity>, IGenericService<TEntity> where TEntity : class, new()
{
    public GenericService(IDbContext dbContext) : base(dbContext) { }
}

Implémentation concrète et consommation

Voici comment appliquer l'attribut sur un dépôt et un service spécifiques, puis les injecter dans un contrôleur d'API.


// Entité et DTO
public class AuditLog { }
public class AuditLogFilter { public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } }
public class PagedResult<T> { }
public class PaginationMetadata { }

// Dépôt spécifique
[AutoInject(Scope = InjectionScope.Transient)]
public class AuditLogRepository : GenericRepository<AuditLog>
{
    public AuditLogRepository(IDbContext context) : base(context) { }

    public PagedResult<AuditLog> FetchLogs(AuditLogFilter filter, PaginationMetadata paging)
    {
        // Logique de requête spécifique
        return new PagedResult<AuditLog>();
    }
}

// Interface du service
public interface IAuditLogService
{
    void RecordAction(AuditLog logEntry);
    PagedResult<AuditLog> RetrieveLogs(AuditLogFilter filter, PaginationMetadata paging);
}

// Implémentation du service
[AutoInject(ContractType = typeof(IAuditLogService), Scope = InjectionScope.Transient)]
public class AuditLogService : GenericService<AuditLog>, IAuditLogService
{
    private readonly AuditLogRepository _auditRepository;

    public AuditLogService(AuditLogRepository repository, IDbContext context) : base(context)
    {
        _auditRepository = repository;
    }

    public void RecordAction(AuditLog logEntry)
    {
        // Enregistrement en base
    }

    public PagedResult<AuditLog> RetrieveLogs(AuditLogFilter filter, PaginationMetadata paging)
    {
        bool isDemoEnvironment = AppConfig.GetValue<bool>("IsDemo", false);
        if (isDemoEnvironment)
        {
            return new PagedResult<AuditLog>();
        }

        // Ajustement des dates
        filter.StartDate = DateUtility.ShiftDate(filter.StartDate, -1);
        filter.EndDate = DateUtility.ShiftDate(filter.EndDate, 1);

        return _auditRepository.FetchLogs(filter, paging);
    }
}

// Contrôleur API
[ApiController]
[Route("api/[controller]")]
public class AuditController : ControllerBase
{
    private readonly IAuditLogService _auditService;

    public AuditController(IAuditLogService auditService)
    {
        _auditService = auditService;
    }

    [HttpGet]
    public IActionResult GetLogs([FromQuery] AuditLogFilter filter, [FromQuery] PaginationMetadata paging)
    {
        var logs = _auditService.RetrieveLogs(filter, paging);
        return Ok(logs);
    }

    [HttpPost]
    public IActionResult PostLog([FromBody] AuditLog logEntry)
    {
        _auditService.RecordAction(logEntry);
        return Created();
    }
}

Étiquettes: dotnet-6 Web-API dependency-injection Reflection CSharp

Publié le 27 juillet à 05h24