Browse Source

Merge pull request #25108 from abpframework/auto-merge/rel-10-1/4434

Merge branch rel-10.2 with rel-10.1
pull/25109/head
Volosoft Agent 2 weeks ago
committed by GitHub
parent
commit
8577eb22a8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      docs/en/framework/api-development/auto-controllers.md
  2. 59
      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. 62
      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 =>

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

@ -1,4 +1,5 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
@ -39,27 +40,55 @@ public class AbpValidationActionFilter : IAsyncActionFilter, IAbpFilter, ITransi
return;
}
if (context.ActionDescriptor.GetMethodInfo().DeclaringType != context.Controller.GetType())
var effectiveMethod = GetEffectiveMethodInfo(context);
if (effectiveMethod != null)
{
var baseMethod = context.ActionDescriptor.GetMethodInfo();
var overrideMethod = context.Controller.GetType().GetMethods().FirstOrDefault(x =>
x.DeclaringType == context.Controller.GetType() &&
x.Name == baseMethod.Name &&
x.ReturnType == baseMethod.ReturnType &&
x.GetParameters().Select(p => p.ToString()).SequenceEqual(baseMethod.GetParameters().Select(p => p.ToString())));
if (overrideMethod != null)
if (ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(effectiveMethod) != null)
{
if (ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(overrideMethod) != null)
{
await next();
return;
}
await next();
return;
}
}
context.GetRequiredService<IModelStateValidator>().Validate(context.ModelState);
if (context.Controller is IValidationEnabled)
{
await ValidateActionArgumentsAsync(context, effectiveMethod);
}
await next();
}
protected virtual MethodInfo? GetEffectiveMethodInfo(ActionExecutingContext context)
{
var baseMethod = context.ActionDescriptor.GetMethodInfo();
if (baseMethod.DeclaringType == context.Controller.GetType())
{
return null;
}
return context.Controller.GetType().GetMethods().FirstOrDefault(x =>
x.DeclaringType == context.Controller.GetType() &&
x.Name == baseMethod.Name &&
x.ReturnType == baseMethod.ReturnType &&
x.GetParameters().Select(p => p.ToString()).SequenceEqual(baseMethod.GetParameters().Select(p => p.ToString())));
}
protected virtual async Task ValidateActionArgumentsAsync(ActionExecutingContext context, MethodInfo? effectiveMethod = null)
{
var methodInfo = effectiveMethod ?? 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
{

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

@ -0,0 +1,62 @@
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Shouldly;
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)
{
using 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.
using 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