Browse Source

Support async data annotations validation on .NET 11

pull/26147/head
maliming 1 month ago
parent
commit
dd53c4efed
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 22
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpMvcAttributeValidationResultProvider.cs
  2. 120
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs
  3. 22
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DefaultAttributeValidationResultProvider.cs
  4. 20
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IAttributeValidationResultProvider.cs
  5. 120
      framework/test/Volo.Abp.Validation.Tests/Volo/Abp/Validation/AsyncDataAnnotations_Validation_Tests.cs

22
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpMvcAttributeValidationResultProvider.cs

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.Validation;
@ -23,11 +25,27 @@ public class AbpMvcAttributeValidationResultProvider : DefaultAttributeValidatio
}
public override ValidationResult? GetOrDefault(ValidationAttribute validationAttribute, object? validatingObject, ValidationContext validationContext)
{
LocalizeErrorMessage(validationAttribute, validationContext);
return base.GetOrDefault(validationAttribute, validatingObject, validationContext);
}
public override Task<ValidationResult?> GetOrDefaultAsync(
ValidationAttribute validationAttribute,
object? validatingObject,
ValidationContext validationContext,
CancellationToken cancellationToken = default)
{
LocalizeErrorMessage(validationAttribute, validationContext);
return base.GetOrDefaultAsync(validationAttribute, validatingObject, validationContext, cancellationToken);
}
protected virtual void LocalizeErrorMessage(ValidationAttribute validationAttribute, ValidationContext validationContext)
{
var resourceSource = _abpMvcDataAnnotationsLocalizationOptions.AssemblyResources.GetOrDefault(validationContext.ObjectType.Assembly);
if (resourceSource == null)
{
return base.GetOrDefault(validationAttribute, validatingObject, validationContext);
return;
}
if (validationAttribute.ErrorMessage == null)
@ -39,7 +57,5 @@ public class AbpMvcAttributeValidationResultProvider : DefaultAttributeValidatio
{
validationAttribute.ErrorMessage = _stringLocalizerFactory.Create(resourceSource)[validationAttribute.ErrorMessage];
}
return base.GetOrDefault(validationAttribute, validatingObject, validationContext);
}
}

120
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs

@ -29,8 +29,12 @@ public class DataAnnotationObjectValidationContributor : IObjectValidationContri
public Task AddErrorsAsync(ObjectValidationContext context)
{
#if NET11_0_OR_GREATER
return ValidateObjectRecursivelyAsync(context.Errors, context.ValidatingObject, currentDepth: 1);
#else
ValidateObjectRecursively(context.Errors, context.ValidatingObject, currentDepth: 1);
return Task.CompletedTask;
#endif
}
protected virtual void ValidateObjectRecursively(List<ValidationResult> errors, object? validatingObject, int currentDepth)
@ -133,4 +137,120 @@ public class DataAnnotationObjectValidationContributor : IObjectValidationContri
}
}
}
#if NET11_0_OR_GREATER
/* Mirrors the synchronous walk above. Attributes deriving from AsyncValidationAttribute and
* objects implementing IAsyncValidatableObject only produce results when awaited, so the
* synchronous path either throws or silently validates nothing for them. */
protected virtual async Task ValidateObjectRecursivelyAsync(List<ValidationResult> errors, object? validatingObject, int currentDepth)
{
if (currentDepth > MaxRecursiveParameterValidationDepth)
{
return;
}
if (validatingObject == null)
{
return;
}
await AddErrorsAsync(errors, validatingObject);
//Validate items of enumerable
if (validatingObject is IEnumerable enumerable)
{
if (!(enumerable is IQueryable))
{
foreach (var item in enumerable)
{
//Do not recursively validate for primitive objects
if (item == null || TypeHelper.IsPrimitiveExtended(item.GetType()))
{
break;
}
await ValidateObjectRecursivelyAsync(errors, item, currentDepth + 1);
}
}
return;
}
var validatingObjectType = validatingObject.GetType();
//Do not recursively validate for primitive objects
if (TypeHelper.IsPrimitiveExtended(validatingObjectType))
{
return;
}
if (Options.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject)))
{
return;
}
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
if (property.Attributes.OfType<DisableValidationAttribute>().Any())
{
continue;
}
await ValidateObjectRecursivelyAsync(errors, property.GetValue(validatingObject), currentDepth + 1);
}
}
public virtual async Task AddErrorsAsync(List<ValidationResult> errors, object validatingObject)
{
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
await AddPropertyErrorsAsync(validatingObject, property, errors);
}
/* IAsyncValidatableObject extends IValidatableObject, so the asynchronous overload has to win
* to avoid running the object twice and to avoid the synchronous Validate that such a type is
* expected to throw from. */
if (validatingObject is IAsyncValidatableObject asyncValidatableObject)
{
await foreach (var result in asyncValidatableObject.ValidateAsync(new ValidationContext(asyncValidatableObject, ServiceProvider, null)))
{
errors.Add(result);
}
}
else if (validatingObject is IValidatableObject validatableObject)
{
errors.AddRange(
validatableObject.Validate(new ValidationContext(validatableObject, ServiceProvider, null))
);
}
}
protected virtual async Task AddPropertyErrorsAsync(object validatingObject, PropertyDescriptor property, List<ValidationResult> errors)
{
var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();
if (validationAttributes.IsNullOrEmpty())
{
return;
}
var validationContext = new ValidationContext(validatingObject, ServiceProvider, null)
{
DisplayName = property.DisplayName,
MemberName = property.Name
};
var attributeValidationResultProvider = ServiceProvider.GetRequiredService<IAttributeValidationResultProvider>();
foreach (var attribute in validationAttributes)
{
var result = await attributeValidationResultProvider.GetOrDefaultAsync(attribute, property.GetValue(validatingObject), validationContext);
if (result != null)
{
errors.Add(result);
}
}
}
#endif
}

22
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DefaultAttributeValidationResultProvider.cs

@ -1,5 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.DependencyInjection;
#if NET11_0_OR_GREATER
using System.Threading;
using System.Threading.Tasks;
#endif
namespace Volo.Abp.Validation;
@ -9,4 +13,20 @@ public class DefaultAttributeValidationResultProvider : IAttributeValidationResu
{
return validationAttribute.GetValidationResult(validatingObject, validationContext);
}
#if NET11_0_OR_GREATER
public virtual Task<ValidationResult?> GetOrDefaultAsync(
ValidationAttribute validationAttribute,
object? validatingObject,
ValidationContext validationContext,
CancellationToken cancellationToken = default)
{
if (validationAttribute is AsyncValidationAttribute asyncValidationAttribute)
{
return asyncValidationAttribute.GetValidationResultAsync(validatingObject, validationContext, cancellationToken);
}
return Task.FromResult(GetOrDefault(validationAttribute, validatingObject, validationContext));
}
#endif
}

20
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IAttributeValidationResultProvider.cs

@ -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;
public interface IAttributeValidationResultProvider
{
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
}

120
framework/test/Volo.Abp.Validation.Tests/Volo/Abp/Validation/AsyncDataAnnotations_Validation_Tests.cs

@ -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…
Cancel
Save