Gestion des erreurs en ASP.NET Core

Tutoriel complet sur Gestion des erreurs en ASP.NET Core : concepts, implémentation et optimisation pour développeurs.

Olivier Dupuy
21 juillet 2025

12

Vues

0

Commentaires

4

Min de lecture

Architecture moderne

Une architecture bien conçue est la clé du succès d'une application. Cet article explore Gestion des erreurs en ASP.NET Core et les principes d'architecture essentiels pour construire des applications maintenables et scalables.

Clean Architecture

L'architecture Clean sépare les préoccupations en couches distinctes :

Structure du projet

Solution/
├── src/
│   ├── Core/
│   │   ├── Domain/           # Entités métier
│   │   └── Application/      # Cas d'usage
│   ├── Infrastructure/       # Accès aux données
│   └── Presentation/         # API/Web
└── tests/
    ├── UnitTests/
    └── IntegrationTests/

Couche Domain

// Entité métier
public class Article : Entity
{
    public string Title { get; private set; }
    public string Content { get; private set; }
    public DateTime PublishedAt { get; private set; }
    public ArticleStatus Status { get; private set; }
    
    private Article() { } // EF Core
    
    public Article(string title, string content, string authorId)
    {
        Title = Guard.Against.NullOrEmpty(title, nameof(title));
        Content = Guard.Against.NullOrEmpty(content, nameof(content));
        AuthorId = Guard.Against.NullOrEmpty(authorId, nameof(authorId));
        Status = ArticleStatus.Draft;
        CreatedAt = DateTime.UtcNow;
        
        AddDomainEvent(new ArticleCreatedEvent(Id, title, authorId));
    }
    
    public void Publish()
    {
        if (Status == ArticleStatus.Published)
            throw new DomainException("Article is already published");
            
        Status = ArticleStatus.Published;
        PublishedAt = DateTime.UtcNow;
        
        AddDomainEvent(new ArticlePublishedEvent(Id, Title));
    }
    
    public void UpdateContent(string title, string content)
    {
        if (Status == ArticleStatus.Published)
            throw new DomainException("Cannot modify published article");
            
        Title = Guard.Against.NullOrEmpty(title, nameof(title));
        Content = Guard.Against.NullOrEmpty(content, nameof(content));
        UpdatedAt = DateTime.UtcNow;
    }
}

// Value Object
public record ArticleSlug
{
    public string Value { get; }
    
    public ArticleSlug(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Slug cannot be empty");
            
        if (!Regex.IsMatch(value, @"^[a-z0-9-]+$"))
            throw new ArgumentException("Invalid slug format");
            
        Value = value;
    }
    
    public static implicit operator string(ArticleSlug slug) => slug.Value;
    public static implicit operator ArticleSlug(string value) => new(value);
}

Couche Application

// Interface repository
public interface IArticleRepository
{
    Task<Article?> GetByIdAsync(int id);
    Task<List<Article>> GetPublishedAsync(int page, int pageSize);
    Task AddAsync(Article article);
    Task UpdateAsync(Article article);
    Task DeleteAsync(Article article);
}

// Cas d'usage avec CQRS
public record CreateArticleCommand(string Title, string Content, string AuthorId) : IRequest<int>;

public class CreateArticleHandler : IRequestHandler<CreateArticleCommand, int>
{
    private readonly IArticleRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMediator _mediator;
    
    public CreateArticleHandler(
        IArticleRepository repository, 
        IUnitOfWork unitOfWork,
        IMediator mediator)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _mediator = mediator;
    }
    
    public async Task<int> Handle(CreateArticleCommand request, CancellationToken cancellationToken)
    {
        var article = new Article(request.Title, request.Content, request.AuthorId);
        
        await _repository.AddAsync(article);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        
        // Publier les événements de domaine
        foreach (var domainEvent in article.GetDomainEvents())
        {
            await _mediator.Publish(domainEvent, cancellationToken);
        }
        
        return article.Id;
    }
}

// Query avec CQRS
public record GetArticlesQuery(int Page, int PageSize) : IRequest<PagedResult<ArticleDto>>;

public class GetArticlesHandler : IRequestHandler<GetArticlesQuery, PagedResult<ArticleDto>>
{
    private readonly IReadOnlyRepository<ArticleDto> _readRepository;
    
    public GetArticlesHandler(IReadOnlyRepository<ArticleDto> readRepository)
    {
        _readRepository = readRepository;
    }
    
    public async Task<PagedResult<ArticleDto>> Handle(GetArticlesQuery request, CancellationToken cancellationToken)
    {
        return await _readRepository.GetPagedAsync(
            a => a.Status == ArticleStatus.Published,
            a => a.OrderByDescending(x => x.PublishedAt),
            request.Page,
            request.PageSize);
    }
}

Couche Infrastructure

// Repository implementation
public class ArticleRepository : IArticleRepository
{
    private readonly ApplicationDbContext _context;
    
    public ArticleRepository(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<Article?> GetByIdAsync(int id)
    {
        return await _context.Articles
            .Include(a => a.Category)
            .FirstOrDefaultAsync(a => a.Id == id);
    }
    
    public async Task<List<Article>> GetPublishedAsync(int page, int pageSize)
    {
        return await _context.Articles
            .Where(a => a.Status == ArticleStatus.Published)
            .OrderByDescending(a => a.PublishedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
    }
    
    public async Task AddAsync(Article article)
    {
        await _context.Articles.AddAsync(article);
    }
    
    public Task UpdateAsync(Article article)
    {
        _context.Articles.Update(article);
        return Task.CompletedTask;
    }
    
    public Task DeleteAsync(Article article)
    {
        _context.Articles.Remove(article);
        return Task.CompletedTask;
    }
}

// Unit of Work
public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;
    private readonly IMediator _mediator;
    
    public UnitOfWork(ApplicationDbContext context, IMediator mediator)
    {
        _context = context;
        _mediator = mediator;
    }
    
    public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // Récupérer les événements avant de sauvegarder
        var domainEvents = _context.ChangeTracker
            .Entries<Entity>()
            .SelectMany(x => x.Entity.GetDomainEvents())
            .ToList();
        
        var result = await _context.SaveChangesAsync(cancellationToken);
        
        // Publier les événements après la sauvegarde
        foreach (var domainEvent in domainEvents)
        {
            await _mediator.Publish(domainEvent, cancellationToken);
        }
        
        return result;
    }
}

Microservices avec API Gateway

// Configuration Ocelot API Gateway
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/articles/{everything}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ],
      "UpstreamPathTemplate": "/api/articles/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer",
        "AllowedScopes": []
      },
      "RateLimitOptions": {
        "ClientIdHeader": "ClientId",
        "QuotaExceededMessage": "Customize quota exceeded message",
        "RateLimitCounterPrefix": "ocelot",
        "DisableRateLimitHeaders": false,
        "HttpStatusCode": 429
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:5000"
  }
}

// Service découverte avec Consul
builder.Services.AddConsul(options =>
{
    options.Address = new Uri("http://localhost:8500");
})
.AddConsulServiceDiscovery();

// Health checks
builder.Services.AddHealthChecks()
    .AddDbContext<ApplicationDbContext>()
    .AddRedis(builder.Configuration.GetConnectionString("Redis"))
    .AddConsul(options =>
    {
        options.HostName = "localhost";
        options.Port = 8500;
    });

Event Sourcing et CQRS avancé

// Event Store
public class EventStore : IEventStore
{
    private readonly ApplicationDbContext _context;
    
    public EventStore(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task SaveEventsAsync(Guid aggregateId, IEnumerable<DomainEvent> events, int expectedVersion)
    {
        var currentVersion = await GetVersionAsync(aggregateId);
        
        if (currentVersion != expectedVersion)
            throw new ConcurrencyException($"Expected version {expectedVersion}, but was {currentVersion}");
        
        var eventVersion = expectedVersion;
        foreach (var @event in events)
        {
            eventVersion++;
            var eventData = new StoredEvent
            {
                AggregateId = aggregateId,
                EventType = @event.GetType().Name,
                Data = JsonSerializer.Serialize(@event),
                Version = eventVersion,
                Timestamp = DateTime.UtcNow
            };
            
            await _context.StoredEvents.AddAsync(eventData);
        }
        
        await _context.SaveChangesAsync();
    }
    
    public async Task<List<DomainEvent>> GetEventsAsync(Guid aggregateId)
    {
        var storedEvents = await _context.StoredEvents
            .Where(e => e.AggregateId == aggregateId)
            .OrderBy(e => e.Version)
            .ToListAsync();
        
        var events = new List<DomainEvent>();
        foreach (var storedEvent in storedEvents)
        {
            var eventType = Type.GetType(storedEvent.EventType);
            var @event = JsonSerializer.Deserialize(storedEvent.Data, eventType) as DomainEvent;
            events.Add(@event!);
        }
        
        return events;
    }
}

// Projection Builder
public class ArticleProjectionBuilder : IProjectionBuilder<ArticleReadModel>
{
    public ArticleReadModel Build(IEnumerable<DomainEvent> events)
    {
        var readModel = new ArticleReadModel();
        
        foreach (var @event in events)
        {
            Apply(readModel, @event);
        }
        
        return readModel;
    }
    
    private void Apply(ArticleReadModel readModel, DomainEvent @event)
    {
        switch (@event)
        {
            case ArticleCreatedEvent created:
                readModel.Id = created.ArticleId;
                readModel.Title = created.Title;
                readModel.AuthorId = created.AuthorId;
                break;
                
            case ArticlePublishedEvent published:
                readModel.IsPublished = true;
                readModel.PublishedAt = published.PublishedAt;
                break;
                
            case ArticleContentUpdatedEvent updated:
                readModel.Title = updated.Title;
                readModel.Content = updated.Content;
                break;
        }
    }
}

Conclusion

Une architecture robuste avec Gestion des erreurs en ASP.NET Core nécessite une planification soignée et l'application de principes éprouvés. La Clean Architecture, CQRS, Event Sourcing et les microservices offrent flexibilité et scalabilité. Choisissez l'approche adaptée à la complexité et aux besoins de votre projet.

Partager cet article
42
12

Commentaires (0)

Rejoignez la discussion

Connectez-vous pour partager votre avis et échanger avec la communauté

Première discussion

Soyez le premier à partager votre avis sur cet article !

À propos de l'auteur
Olivier Dupuy

Développeur passionné et créateur de contenu technique. Expert en développement web moderne avec ASP.NET Core, JavaScript, et technologies cloud.

Profil
Articles similaires
API versioning strategies
02 août 2025 0
C# & .NET
Cryptographie post-quantique
02 août 2025 0
C# & .NET
Géolocalisation et cartes interactives
02 août 2025 0
C# & .NET
Navigation rapide
Commentaires (0)