feat(api, domain, core): Глобальные модели ошибок. ConfigureValidator - валидатор контроллеров. GlobalExceptionHandler - глобальный обработчик ошибок в API.

This commit is contained in:
Mikhail Trubnikov
2026-05-19 16:19:55 +10:00
parent c0148088c1
commit d8af0253b1
10 changed files with 236 additions and 33 deletions

View File

@@ -0,0 +1,51 @@
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Infrastructure.Middleware;
namespace PARR.API.Infrastructure
{
/// <summary>
/// Настройка валидаторов
/// </summary>
public static class ConfigureValidator
{
/// <summary>
/// Подключить конфигурацию валидаторов
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddValidatorServices(this IServiceCollection services)
{
services.AddExceptionHandler<GlobalExceptionHandler>(); // регим GlobalExceptionHandler
services.AddProblemDetails(); // Генерирует стандартный формат ошибки RFC 7807
// Настройка контроллеров и кастомного формата ошибок валидации
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
// наша стандартная модель ошибок
var errors = context.ModelState.Values
.SelectMany(t => t.Errors)
.Select(t => new ErrorModel { Message= t.ErrorMessage })
.ToList();
return new BadRequestObjectResult( new Response( false, errors));
};
});
// Регистрируем валидаторы API.
// Просканирует всю сборку API (где лежит Program.cs) и найдет там валидаторы
services.AddValidatorsFromAssembly(typeof(Program).Assembly);
//services.AddValidatorsFromAssemblies(Assembly.GetExecutingAssembly());
// Включаем авто-валидацию FluentValidation для контроллеров
services.AddFluentValidationAutoValidation();
return services;
}
}
}

View File

@@ -1,9 +1,73 @@
using Microsoft.AspNetCore.Diagnostics;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.Domain.Exceptions;
namespace PARR.API.Infrastructure.Middleware
{
public class GlobalExceptionHandler//: IExceptionHandler
/// <summary>
/// Глобальная обработка ошибок согласно моих кастомных Exception
/// </summary>
public class GlobalExceptionHandler : IExceptionHandler
{
//todo:
private readonly ILogger<GlobalExceptionHandler> logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
this.logger = logger;
}
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
logger.LogError(exception, "Ошибка во время запроса {TraceId}: {Message}", httpContext.TraceIdentifier, exception.Message);
// определяем статус код, в зависимости от типа исключения
var statusCode = exception switch
{
NotFoundException => StatusCodes.Status404NotFound,
DbErrorException => StatusCodes.Status502BadGateway,
ExternalServiceException => StatusCodes.Status502BadGateway,
AppValidationException => StatusCodes.Status400BadRequest,
FluentValidation.ValidationException => StatusCodes.Status400BadRequest,
AlreadyExistsException => StatusCodes.Status409Conflict,
UnauthorizedException => StatusCodes.Status401Unauthorized,
ForbiddenException => StatusCodes.Status403Forbidden,
//todo: ---------- другие ошибки ----------
_ => StatusCodes.Status500InternalServerError
};
// Для критических серверных ошибок (5xx) пишем детальный лог
if (statusCode >= 500)
{
logger.LogError(exception, "Критическая ошибка сервера: {Message}", exception.Message);
}
//var errorMessage = exception.Message;
var errors = new List<ErrorModel>();
// Если это ошибка FluentValidatioun, вытаскиваем детали по каждому полю
if (exception is FluentValidation.ValidationException fluentEx)
{
// ошибка валидации, вернем по каждому полю
foreach (var error in fluentEx.Errors)
{
errors.Add(new ErrorModel { FieldName = error.PropertyName, Message = error.ErrorMessage });
}
}
else
{
// это не ошибка валидации, вернем сообщение
errors.Add(new ErrorModel { Message = exception.Message });
}
httpContext.Response.StatusCode = statusCode;
var response = new Response<object?>(null, false, errors, exception.Message);
await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
// ошибка обработана
return true;
}
}
}