Browse Source

Fix FluentValidation not working with ConventionalControllers

When Application Services are registered via ConventionalControllers.Create(),
their types are added to DynamicProxyIgnoreTypes, which disables the
ValidationInterceptor. The AbpValidationActionFilter only checked ModelState
(DataAnnotations), so FluentValidation rules were never executed.

Add IValidationEnabled check in AbpValidationActionFilter to call
IMethodInvocationValidator for conventional controllers, enabling
FluentValidation support without duplicating validation for regular controllers.

Resolve #23457
pull/25105/head
maliming 2 weeks ago
parent
commit
234c46aab8
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 4
      docs/en/framework/api-development/auto-controllers.md
  2. 22
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs
  3. 1
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj
  4. 4
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs
  5. 64
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/FluentValidationTestAppService_Tests.cs
  6. 12
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/FluentValidationTestInputValidator.cs
  7. 11
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController.cs
  8. 18
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs
  9. 17
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/FluentValidationTestAppService.cs

4
docs/en/framework/api-development/auto-controllers.md

@ -70,7 +70,7 @@ Route is calculated based on some conventions:
* Continues with a **route path**. Default value is '**/app**' and can be configured as like below:
````csharp
Configure<AbpAspNetCoreMvcOptions>(options =>
PreConfigure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers
.Create(typeof(BookStoreApplicationModule).Assembly, opts =>
@ -149,7 +149,7 @@ public class PersonAppService : ApplicationService
You can further filter classes to become an API controller by providing the `TypePredicate` option:
````csharp
services.Configure<AbpAspNetCoreMvcOptions>(options =>
PreConfigure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers
.Create(typeof(BookStoreApplicationModule).Assembly, opts =>

22
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs

@ -60,6 +60,28 @@ public class AbpValidationActionFilter : IAsyncActionFilter, IAbpFilter, ITransi
}
context.GetRequiredService<IModelStateValidator>().Validate(context.ModelState);
if (context.Controller is IValidationEnabled)
{
await ValidateActionArgumentsAsync(context);
}
await next();
}
protected virtual async Task ValidateActionArgumentsAsync(ActionExecutingContext context)
{
var methodInfo = context.ActionDescriptor.GetMethodInfo();
var parameterValues = methodInfo.GetParameters()
.Select(p => context.ActionArguments.TryGetValue(p.Name!, out var value) ? value : null)
.ToArray();
await context.GetRequiredService<IMethodInvocationValidator>().ValidateAsync(
new MethodInvocationValidationContext(
context.Controller,
methodInfo,
parameterValues
)
);
}
}

1
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj

@ -26,6 +26,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc.UI\Volo.Abp.AspNetCore.Mvc.UI.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.FluentValidation\Volo.Abp.FluentValidation.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Tests\Volo.Abp.AspNetCore.Tests.csproj" />
<ProjectReference Include="..\Volo.Abp.MemoryDb.Tests\Volo.Abp.MemoryDb.Tests.csproj" />
</ItemGroup>

4
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs

@ -23,6 +23,7 @@ using Volo.Abp.TestApp;
using Volo.Abp.TestApp.Application;
using Volo.Abp.Threading;
using Volo.Abp.Validation.Localization;
using Volo.Abp.FluentValidation;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.AspNetCore.Mvc;
@ -31,7 +32,8 @@ namespace Volo.Abp.AspNetCore.Mvc;
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpMemoryDbTestModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpAutofacModule)
typeof(AbpAutofacModule),
typeof(AbpFluentValidationModule)
)]
public class AbpAspNetCoreMvcTestModule : AbpModule
{

64
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/FluentValidationTestAppService_Tests.cs

@ -0,0 +1,64 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Http;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Validation;
public class FluentValidationTestAppService_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task Should_Validate_With_FluentValidation_On_ConventionalController()
{
// Name is "A" which is less than 3 characters, should fail FluentValidation
var response = await PostAsync("{\"name\": \"A\"}");
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
[Fact]
public async Task Should_Validate_With_FluentValidation_On_ConventionalController_EmptyName()
{
// Empty name should fail FluentValidation (NotEmpty rule)
var response = await PostAsync("{\"name\": \"\"}");
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
[Fact]
public async Task Should_Validate_With_FluentValidation_On_ConventionalController_MaxLength()
{
// Name exceeds 10 characters, should fail FluentValidation (MaximumLength rule)
var response = await PostAsync("{\"name\": \"12345678901\"}");
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
[Fact]
public async Task Should_Return_Validation_Errors_With_Details()
{
var response = await PostAsync("{\"name\": \"A\"}");
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
content.ShouldContain("Name");
content.ShouldContain("validationErrors");
}
[Fact]
public async Task Should_Pass_Validation_With_Valid_Input()
{
var response = await PostAsync("{\"name\": \"Hello\"}");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
private async Task<HttpResponseMessage> PostAsync(string jsonContent)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/api/app/fluent-validation-test")
{
Content = new StringContent(jsonContent, Encoding.UTF8, "application/json")
};
return await Client.SendAsync(request);
}
}

12
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/FluentValidationTestInputValidator.cs

@ -0,0 +1,12 @@
using FluentValidation;
using Volo.Abp.TestApp.Application;
namespace Volo.Abp.AspNetCore.Mvc.Validation;
public class FluentValidationTestInputValidator : AbstractValidator<FluentValidationTestInput>
{
public FluentValidationTestInputValidator()
{
RuleFor(x => x.Name).NotEmpty().MinimumLength(3).MaximumLength(10);
}
}

11
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController.cs

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Shouldly;
using Volo.Abp.DependencyInjection;
using Volo.Abp.TestApp.Application;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.Validation;
@ -120,6 +121,16 @@ public class ValidationTestController : AbpController
}
}
[HttpPost]
[Route("fluent-validation-action")]
public Task<string> FluentValidationAction([FromBody] FluentValidationTestInput input)
{
// This action uses a DTO that has a FluentValidator registered,
// but since this is a regular AbpController (not IValidationEnabled),
// FluentValidation should NOT be triggered by AbpValidationActionFilter.
return Task.FromResult(input.Name);
}
public class CustomValidateModel : IValidatableObject
{
public string Value1 { get; set; }

18
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs

@ -1,4 +1,6 @@
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
@ -112,6 +114,22 @@ public class ValidationTestController_Tests : AspNetCoreMvcTestBase
var result = await GetResponseAsStringAsync("/api/validation-test/object-result-action2");
result.ShouldBe("ModelState.IsValid: false");
}
[Fact]
public async Task Should_Not_Trigger_FluentValidation_On_Regular_Controller()
{
// FluentValidationTestInput has a FluentValidator with MinimumLength(3) rule,
// but this is a regular AbpController (not IValidationEnabled),
// so FluentValidation should NOT be triggered.
var request = new HttpRequestMessage(HttpMethod.Post, "/api/validation-test/fluent-validation-action")
{
Content = new StringContent("{\"name\": \"A\"}", Encoding.UTF8, "application/json")
};
var response = await Client.SendAsync(request);
// Should return OK because FluentValidation is not triggered for regular controllers
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
}
public class DisableAutoModelValidationTestController_Tests : AspNetCoreMvcTestBase

17
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/FluentValidationTestAppService.cs

@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Volo.Abp.TestApp.Application;
public class FluentValidationTestAppService : ApplicationService
{
public virtual Task<string> CreateAsync(FluentValidationTestInput input)
{
return Task.FromResult(input.Name);
}
}
public class FluentValidationTestInput
{
public string Name { get; set; }
}
Loading…
Cancel
Save