mirror of https://github.com/abpframework/abp.git
5 changed files with 299 additions and 5 deletions
@ -1,8 +1,26 @@ |
|||||
using System.ComponentModel.DataAnnotations; |
using System.ComponentModel.DataAnnotations; |
||||
|
#if NET11_0_OR_GREATER
|
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
#endif
|
||||
|
|
||||
namespace Volo.Abp.Validation; |
namespace Volo.Abp.Validation; |
||||
|
|
||||
public interface IAttributeValidationResultProvider |
public interface IAttributeValidationResultProvider |
||||
{ |
{ |
||||
ValidationResult? GetOrDefault(ValidationAttribute validationAttribute, object? validatingObject, ValidationContext validationContext); |
ValidationResult? GetOrDefault(ValidationAttribute validationAttribute, object? validatingObject, ValidationContext validationContext); |
||||
|
|
||||
|
#if NET11_0_OR_GREATER
|
||||
|
/* AsyncValidationAttribute seals IsValid and routes it through the synchronous overload, which |
||||
|
* attributes that only validate asynchronously are expected to throw from. They have to be |
||||
|
* awaited here instead. The default implementation keeps existing providers working. */ |
||||
|
Task<ValidationResult?> GetOrDefaultAsync( |
||||
|
ValidationAttribute validationAttribute, |
||||
|
object? validatingObject, |
||||
|
ValidationContext validationContext, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return Task.FromResult(GetOrDefault(validationAttribute, validatingObject, validationContext)); |
||||
|
} |
||||
|
#endif
|
||||
} |
} |
||||
|
|||||
@ -0,0 +1,120 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Autofac; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.Testing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Validation; |
||||
|
|
||||
|
public class AsyncDataAnnotations_Validation_Tests : AbpIntegratedTest<AsyncDataAnnotations_Validation_Tests.TestModule> |
||||
|
{ |
||||
|
private readonly IObjectValidator _objectValidator; |
||||
|
|
||||
|
public AsyncDataAnnotations_Validation_Tests() |
||||
|
{ |
||||
|
_objectValidator = GetRequiredService<IObjectValidator>(); |
||||
|
} |
||||
|
|
||||
|
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
||||
|
{ |
||||
|
options.UseAutofac(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Run_Async_Validation_Attribute() |
||||
|
{ |
||||
|
var errors = await _objectValidator.GetErrorsAsync(new AsyncAttributeInput { Name = "taken" }); |
||||
|
|
||||
|
errors.ShouldContain(e => e.ErrorMessage == "Name is already taken"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Report_Error_When_Async_Validation_Attribute_Passes() |
||||
|
{ |
||||
|
var errors = await _objectValidator.GetErrorsAsync(new AsyncAttributeInput { Name = "free" }); |
||||
|
|
||||
|
errors.ShouldBeEmpty(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Run_IAsyncValidatableObject() |
||||
|
{ |
||||
|
var errors = await _objectValidator.GetErrorsAsync(new AsyncValidatableInput { Value = -1 }); |
||||
|
|
||||
|
errors.ShouldContain(e => e.ErrorMessage == "Value must not be negative"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Run_Synchronous_Validate_Of_IAsyncValidatableObject() |
||||
|
{ |
||||
|
var input = new AsyncValidatableInput { Value = 1 }; |
||||
|
|
||||
|
await _objectValidator.ValidateAsync(input); |
||||
|
|
||||
|
/* IAsyncValidatableObject extends IValidatableObject. Running both would validate twice and |
||||
|
* the synchronous overload is the one such a type is expected to throw from. */ |
||||
|
input.SynchronousValidateCallCount.ShouldBe(0); |
||||
|
} |
||||
|
|
||||
|
public class AsyncAttributeInput |
||||
|
{ |
||||
|
[NameNotTaken] |
||||
|
public string? Name { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class NameNotTakenAttribute : AsyncValidationAttribute |
||||
|
{ |
||||
|
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) |
||||
|
{ |
||||
|
throw new AbpException("This attribute only validates asynchronously."); |
||||
|
} |
||||
|
|
||||
|
protected override async Task<ValidationResult?> IsValidAsync( |
||||
|
object? value, |
||||
|
ValidationContext validationContext, |
||||
|
CancellationToken cancellationToken) |
||||
|
{ |
||||
|
await Task.Yield(); |
||||
|
|
||||
|
return value as string == "taken" |
||||
|
? new ValidationResult("Name is already taken", new[] { validationContext.MemberName! }) |
||||
|
: ValidationResult.Success; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class AsyncValidatableInput : IAsyncValidatableObject |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
|
||||
|
public int SynchronousValidateCallCount { get; private set; } |
||||
|
|
||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) |
||||
|
{ |
||||
|
SynchronousValidateCallCount++; |
||||
|
return Enumerable.Empty<ValidationResult>(); |
||||
|
} |
||||
|
|
||||
|
public async IAsyncEnumerable<ValidationResult> ValidateAsync( |
||||
|
ValidationContext validationContext, |
||||
|
[EnumeratorCancellation] CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
await Task.Yield(); |
||||
|
|
||||
|
if (Value < 0) |
||||
|
{ |
||||
|
yield return new ValidationResult("Value must not be negative", new[] { nameof(Value) }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[DependsOn(typeof(AbpValidationModule), typeof(AbpAutofacModule))] |
||||
|
public class TestModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue