feat(api, domain, core): Глобальные модели ошибок. ConfigureValidator - валидатор контроллеров. GlobalExceptionHandler - глобальный обработчик ошибок в API.
This commit is contained in:
@@ -4,7 +4,6 @@ FROM 10.99.253.167:8090/dotnet/runtime:9.0 AS base
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM 10.99.253.167:8090/dotnet/sdk:9.0 AS build
|
FROM 10.99.253.167:8090/dotnet/sdk:9.0 AS build
|
||||||
USER root
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
USER root
|
USER root
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|||||||
@@ -84,27 +84,11 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
InitiatorComment = "Отправлен запрос из API на формирование отчета о загруженности"
|
InitiatorComment = "Отправлен запрос из API на формирование отчета о загруженности"
|
||||||
};
|
};
|
||||||
|
|
||||||
//todo: избавиться от try/catch
|
var queueSettings = taskMqSettingsProvider.GetSettings(TaskTypeEnum.Workload);
|
||||||
|
|
||||||
try
|
var taskId = await taskManagementService.CreateTaskAsync<object?>(TaskTypeEnum.Workload, default, initiator, queueSettings);
|
||||||
{
|
|
||||||
//mqSettings.Tasks.TryGetValue(TaskTypeEnum.Workload, out var queueSettings);
|
|
||||||
|
|
||||||
//if (queueSettings == null)
|
return Ok(new Response<string?>(null, true, null!, $"Создана задача {taskId} на формирование отчета"));
|
||||||
//{
|
|
||||||
// logger.LogError("Не удалось получить настройки очереди для TaskType: {TaskType}", TaskTypeEnum.Workload);
|
|
||||||
// throw new InvalidOperationException($"Не удалось получить настройки очереди для TaskTypeEnum.Workload");
|
|
||||||
//}
|
|
||||||
var queueSettings = taskMqSettingsProvider.GetSettings(TaskTypeEnum.Workload);
|
|
||||||
|
|
||||||
var taskId = await taskManagementService.CreateTaskAsync<object?>(TaskTypeEnum.Workload, default, initiator, queueSettings);
|
|
||||||
|
|
||||||
return Ok(new Response<string?>(null, true, null!, $"Создана задача {taskId} на формирование отчета"));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response(false, new List<ErrorModel> { new ErrorModel { Message = ex.Message } }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -123,7 +107,6 @@ namespace PARR.API.Controllers.V1.Statistics
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Универсальный метод получения отчетности
|
/// Универсальный метод получения отчетности
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
51
PARR.API/Infrastructure/ConfigureValidator.cs
Normal file
51
PARR.API/Infrastructure/ConfigureValidator.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,73 @@
|
|||||||
using Microsoft.AspNetCore.Diagnostics;
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
|
using PARR.API.Contracts.V1.Responses.Base;
|
||||||
|
using PARR.Domain.Exceptions;
|
||||||
|
|
||||||
namespace PARR.API.Infrastructure.Middleware
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||||
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.19.0" />
|
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.19.0" />
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.5.2" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.16">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.16">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Elastic.CommonSchema.Serilog;
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using PARR.API.Authentication;
|
using PARR.API.Authentication;
|
||||||
|
using PARR.API.Infrastructure;
|
||||||
using PARR.API.Installers;
|
using PARR.API.Installers;
|
||||||
using PARR.API.Settings;
|
using PARR.API.Settings;
|
||||||
using PARR.Core;
|
using PARR.Core;
|
||||||
@@ -68,10 +69,11 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddValidatorServices();
|
||||||
|
//builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
//builder.Services.AddControllers();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
@@ -82,6 +84,9 @@ builder.Services.InstallCorsServices();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// подключаем обработку моих кастомных Exceptions (GlobalExceptionHandler)
|
||||||
|
app.UseExceptionHandler();
|
||||||
|
|
||||||
// При использовании балансировщика (haproxy, перенаправлять заголовки)
|
// При использовании балансировщика (haproxy, перенаправлять заголовки)
|
||||||
app.UseForwardedHeaders();
|
app.UseForwardedHeaders();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.5.2" />
|
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||||
<PackageReference Include="InfluxDB.Client" Version="4.17.0" />
|
<PackageReference Include="InfluxDB.Client" Version="4.17.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.16" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.16" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.16" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.16" />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using PARR.Domain.Common.Rabbit.Messages;
|
|||||||
using PARR.Domain.DTOs.TaskDTO;
|
using PARR.Domain.DTOs.TaskDTO;
|
||||||
using PARR.Domain.Entities.Base.History;
|
using PARR.Domain.Entities.Base.History;
|
||||||
using PARR.Domain.Enums;
|
using PARR.Domain.Enums;
|
||||||
|
using PARR.Domain.Exceptions;
|
||||||
using PARR.Domain.Settings;
|
using PARR.Domain.Settings;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -50,15 +51,19 @@ namespace PARR.Core.Services.TaskServices.Implementations
|
|||||||
throw new InvalidOperationException($"Тип задачи typeCode не найден в БД");
|
throw new InvalidOperationException($"Тип задачи typeCode не найден в БД");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка IsSingleton: если задача уже активна — возвращаем её
|
// Проверка IsSingleton: если задача уже есть и она активна, генерим ошибку, что нельзя создать новую
|
||||||
if (taskType.IsSingleton)
|
if (taskType.IsSingleton)
|
||||||
{
|
{
|
||||||
var existingTask = await GetActiveSingletonTaskAsync(typeCode);
|
var existingTask = await GetActiveSingletonTaskAsync(typeCode);
|
||||||
|
|
||||||
if (existingTask != null)
|
if (existingTask != null)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Задача типа {TypeCode} уже активна (id: {ExistingId}). Возвращаем существующую.", typeCode, existingTask.Id);
|
//logger.LogInformation("Задача типа {TypeCode} уже активна (id: {ExistingId}). Возвращаем существующую.", typeCode, existingTask.Id);
|
||||||
return existingTask.Id;
|
//return existingTask.Id;
|
||||||
|
|
||||||
|
logger.LogInformation("Задача типа {TypeCode} уже активна (id: {ExistingId}).", typeCode, existingTask.Id);
|
||||||
|
|
||||||
|
throw new AlreadyExistsException($"Уже существует активная задача (id: {existingTask.Id}). Нельзя создать новую пока не выполнится существующая.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ namespace PARR.Core.Services.TaskServices.Implementations
|
|||||||
if (!await taskRepository.CreateAsync(task) || !await taskRepository.CommitAsync(initiator))
|
if (!await taskRepository.CreateAsync(task) || !await taskRepository.CommitAsync(initiator))
|
||||||
{
|
{
|
||||||
logger.LogError("Ошибка при сохранении задачи в БД");
|
logger.LogError("Ошибка при сохранении задачи в БД");
|
||||||
throw new DbUpdateException("Ошибка при сохранении задачи в БД");
|
throw new DbErrorException("Ошибка при сохранении задачи в БД");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("Задача {TaskId} типа {TypeCode} сохранена в БД со статусом Pending", task.Id, typeCode);
|
logger.LogInformation("Задача {TaskId} типа {TypeCode} сохранена в БД со статусом Pending", task.Id, typeCode);
|
||||||
|
|||||||
96
PARR.Domain/Exceptions/CustomExceptions.cs
Normal file
96
PARR.Domain/Exceptions/CustomExceptions.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
namespace PARR.Domain.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Базовый класс для всех ошибок
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseException : Exception
|
||||||
|
{
|
||||||
|
protected BaseException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибка БД. 502
|
||||||
|
/// </summary>
|
||||||
|
public class DbErrorException : BaseException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибка БД.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public DbErrorException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Объект не найден. 404
|
||||||
|
/// </summary>
|
||||||
|
public class NotFoundException : BaseException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Объект не найден.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public NotFoundException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибка валидации. 400.
|
||||||
|
/// Использовать эту ошибку, если не подходит валидация от Fluent (ValidationException)
|
||||||
|
/// </summary>
|
||||||
|
public class AppValidationException : BaseException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибка валидации.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public AppValidationException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Объект уже существует. 409
|
||||||
|
/// </summary>
|
||||||
|
public class AlreadyExistsException : BaseException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Объект уже существует.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public AlreadyExistsException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибка при работе с внешним сервисом. 502
|
||||||
|
/// </summary>
|
||||||
|
public class ExternalServiceException : BaseException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибка при работе с внешним сервисом.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public ExternalServiceException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пользователь не аутентифицирован. 401
|
||||||
|
/// </summary>
|
||||||
|
public class UnauthorizedException : BaseException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Пользователь не аутентифицирован.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public UnauthorizedException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Доступ запрещен. 403.
|
||||||
|
/// Пользователь вошел в систему (мы знаем, кто он), но у него нет прав на конкретное действие или объект.
|
||||||
|
/// </summary>
|
||||||
|
public class ForbiddenException : BaseException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Доступ запрещен.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public ForbiddenException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Common\Rabbit\Events\" />
|
<Folder Include="Common\Rabbit\Events\" />
|
||||||
<Folder Include="Exceptions\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user