fix(api): Исправлена ошибка асинхронной валидации FluentValidation

- удалён устаревший пакет FluentValidation.AspNetCore, вызывавший конфликт версий (NU1605)
- реализован глобальный фильтр AsyncValidationFilter для поддержки MustAsync-правил
This commit is contained in:
Mikhail Kuznetsov
2026-05-21 10:21:00 +10:00
parent 4b96d1bebe
commit 6fc2fc22ac
5 changed files with 102 additions and 42 deletions

View File

@@ -1,7 +1,7 @@
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Infrastructure.Filters;
using PARR.API.Infrastructure.Middleware;
namespace PARR.API.Infrastructure
@@ -22,7 +22,11 @@ namespace PARR.API.Infrastructure
services.AddProblemDetails(); // Генерирует стандартный формат ошибки RFC 7807
// Настройка контроллеров и кастомного формата ошибок валидации
services.AddControllers()
services.AddControllers(options =>
{
// Автоматически применяем асинхронную валидацию ко всем контроллерам
options.Filters.Add<AsyncValidationFilter>();
})
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
@@ -43,7 +47,10 @@ namespace PARR.API.Infrastructure
//services.AddValidatorsFromAssemblies(Assembly.GetExecutingAssembly());
// Включаем авто-валидацию FluentValidation для контроллеров
services.AddFluentValidationAutoValidation();
//services.AddFluentValidationAutoValidation();
services.AddScoped<AsyncValidationFilter>();
return services;
}

View File

@@ -0,0 +1,47 @@
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using PARR.API.Contracts.V1.Responses.Base;
namespace PARR.API.Infrastructure.Filters
{
public class AsyncValidationFilter : IAsyncActionFilter
{
private readonly IServiceProvider _serviceProvider;
public AsyncValidationFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
foreach (var argument in context.ActionArguments.Values)
{
if (argument == null)
continue;
var validatorType = typeof(IValidator<>).MakeGenericType(argument.GetType());
var validator = _serviceProvider.GetService(validatorType);
if (validator is IValidator validatorInstance)
{
var validationContext = new ValidationContext<object>(argument);
var validationResult = await validatorInstance.ValidateAsync(validationContext, context.HttpContext.RequestAborted);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors
.Select(e => new ErrorModel { Message = e.ErrorMessage })
.ToList();
context.Result = new BadRequestObjectResult(new Response(false, errors));
return;
}
}
}
await next();
}
}
}

View File

@@ -1,44 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591;1587;1573;NU1803</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591;1587;1573;NU1803</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;1591;1587;1573;NU1803</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;1591;1587;1573;NU1803</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.19.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.16">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.19.0" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.16">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PARR.Core\PARR.Core.csproj" />
<ProjectReference Include="..\PARR.DAL\PARR.DAL.csproj" />
<ProjectReference Include="..\PARR.Infrastructure\PARR.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PARR.Core\PARR.Core.csproj" />
<ProjectReference Include="..\PARR.DAL\PARR.DAL.csproj" />
<ProjectReference Include="..\PARR.Infrastructure\PARR.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -11,7 +11,7 @@ namespace PARR.API.Validators
{
private readonly ITnkRepository tnkService;
private readonly IJobGroupRepository jobGroupService;
private readonly IJobRepository jobService;
private readonly IJobRepository jobRepository;
private readonly IUnitFieldRepository unitFieldService;
private JobGroup? jobGroup;
@@ -24,7 +24,7 @@ namespace PARR.API.Validators
{
this.tnkService = tnkService;
this.jobGroupService = jobGroupService;
this.jobService = jobService;
this.jobRepository = jobService;
this.unitFieldService = unitFieldService;
RuleFor(t => t.Name)