mirror of https://github.com/abpframework/abp.git
Browse Source
It reflects the statically evaluable FluentValidation rules of a DTO into the api definition, so the client proxy generators see the same constraints the attributes already provide. A rule is only published when it applies to every instance of the type, which leaves out the conditional rules, the rules of a non-default rule set and the rules of a RuleForEach. Volo.Abp.FluentValidation keeps its own dependencies, it does not depend on Volo.Abp.Http.pull/26112/head
28 changed files with 1120 additions and 3 deletions
@ -0,0 +1,3 @@ |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
[assembly: InternalsVisibleTo("Volo.Abp.Http.FluentValidation.Tests")] |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.framework" |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
{ |
|||
"name": "Volo.Abp.Http.FluentValidation", |
|||
"hash": "", |
|||
"contents": [ |
|||
{ |
|||
"namespace": "Volo.Abp.Http.FluentValidation", |
|||
"dependsOnModules": [ |
|||
{ |
|||
"declaringAssemblyName": "Volo.Abp.Http", |
|||
"namespace": "Volo.Abp.Http", |
|||
"name": "AbpHttpModule" |
|||
}, |
|||
{ |
|||
"declaringAssemblyName": "Volo.Abp.FluentValidation", |
|||
"namespace": "Volo.Abp.FluentValidation", |
|||
"name": "AbpFluentValidationModule" |
|||
} |
|||
], |
|||
"implementingInterfaces": [ |
|||
{ |
|||
"name": "IAbpModule", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IAbpModule" |
|||
}, |
|||
{ |
|||
"name": "IOnPreApplicationInitialization", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization" |
|||
}, |
|||
{ |
|||
"name": "IOnApplicationInitialization", |
|||
"namespace": "Volo.Abp", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.IOnApplicationInitialization" |
|||
}, |
|||
{ |
|||
"name": "IOnPostApplicationInitialization", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization" |
|||
}, |
|||
{ |
|||
"name": "IOnApplicationShutdown", |
|||
"namespace": "Volo.Abp", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.IOnApplicationShutdown" |
|||
}, |
|||
{ |
|||
"name": "IPreConfigureServices", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IPreConfigureServices" |
|||
}, |
|||
{ |
|||
"name": "IPostConfigureServices", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IPostConfigureServices" |
|||
} |
|||
], |
|||
"contentType": "abpModule", |
|||
"name": "AbpHttpFluentValidationModule", |
|||
"summary": null |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> |
|||
<Nullable>enable</Nullable> |
|||
<WarningsAsErrors>Nullable</WarningsAsErrors> |
|||
<AssemblyName>Volo.Abp.Http.FluentValidation</AssemblyName> |
|||
<PackageId>Volo.Abp.Http.FluentValidation</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.FluentValidation\Volo.Abp.FluentValidation.csproj" /> |
|||
<ProjectReference Include="..\Volo.Abp.Http\Volo.Abp.Http.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.FluentValidation; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpHttpModule), |
|||
typeof(AbpFluentValidationModule) |
|||
)] |
|||
public class AbpHttpFluentValidationModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,254 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using FluentValidation; |
|||
using FluentValidation.Internal; |
|||
using FluentValidation.Validators; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http.Modeling; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation; |
|||
|
|||
[ExposeServices(typeof(IPropertyApiDescriptionModelContributor))] |
|||
public class FluentValidationPropertyApiDescriptionModelContributor : IPropertyApiDescriptionModelContributor, ITransientDependency |
|||
{ |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
protected ConcurrentDictionary<Type, ILookup<string, IPropertyValidator>?> RuleCache { get; } |
|||
|
|||
public FluentValidationPropertyApiDescriptionModelContributor(IServiceProvider serviceProvider) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
RuleCache = new ConcurrentDictionary<Type, ILookup<string, IPropertyValidator>?>(); |
|||
} |
|||
|
|||
public virtual Task ContributeAsync(PropertyApiDescriptionModelContributionContext context) |
|||
{ |
|||
Check.NotNull(context, nameof(context)); |
|||
|
|||
var rules = RuleCache.GetOrAdd(context.DeclaringType, GetUnconditionalRules); |
|||
if (rules == null) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
foreach (var validator in rules[context.Model.Name]) |
|||
{ |
|||
ApplyValidator(context.Model, validator); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
protected virtual ILookup<string, IPropertyValidator>? GetUnconditionalRules(Type declaringType) |
|||
{ |
|||
if (declaringType.ContainsGenericParameters) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var validator = ServiceProvider.GetService(typeof(IValidator<>).MakeGenericType(declaringType)) as IValidator; |
|||
if (validator == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return validator |
|||
.CreateDescriptor() |
|||
.Rules |
|||
.Where(rule => IsUnconditional(rule) && DescribesPropertyOf(rule, declaringType)) |
|||
.SelectMany(rule => rule |
|||
.Components |
|||
.Where(component => !component.HasCondition && !component.HasAsyncCondition) |
|||
.Select(component => new KeyValuePair<string, IPropertyValidator>(rule.Member.Name, component.Validator))) |
|||
.ToLookup(x => x.Key, x => x.Value, StringComparer.Ordinal); |
|||
} |
|||
|
|||
protected virtual bool DescribesPropertyOf(IValidationRule rule, Type declaringType) |
|||
{ |
|||
// The member is matched instead of the rule's property name, which OverridePropertyName
|
|||
// and a custom PropertyNameResolver can change. A rule on a nested object declares its
|
|||
// member on that object, so it must not be attributed to a property of this type.
|
|||
return rule.Member != null && rule.Member.DeclaringType!.IsAssignableFrom(declaringType); |
|||
} |
|||
|
|||
protected virtual bool IsUnconditional(IValidationRule rule) |
|||
{ |
|||
|
|||
// RuleForEach constrains the items, not the collection property itself.
|
|||
if (IsCollectionRule(rule)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// A When(...)/Unless(...) block sets the condition on the rule, the chained form sets it on the components.
|
|||
if (rule.HasCondition || rule.HasAsyncCondition) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Mirrors DefaultValidatorSelector: ABP never runs a rule that only belongs to a named rule set.
|
|||
return rule.RuleSets.IsNullOrEmpty() || |
|||
rule.RuleSets!.Contains(RulesetValidatorSelector.DefaultRuleSetName, StringComparer.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
protected virtual bool IsCollectionRule(IValidationRule rule) |
|||
{ |
|||
return rule |
|||
.GetType() |
|||
.GetInterfaces() |
|||
.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollectionRule<,>)); |
|||
} |
|||
|
|||
protected virtual void ApplyValidator(PropertyApiDescriptionModel model, IPropertyValidator validator) |
|||
{ |
|||
switch (validator) |
|||
{ |
|||
case INotNullValidator: |
|||
case INotEmptyValidator: |
|||
model.IsRequired = true; |
|||
break; |
|||
case ILengthValidator lengthValidator: |
|||
ApplyLength(model, lengthValidator); |
|||
break; |
|||
case IRegularExpressionValidator regularExpressionValidator: |
|||
ApplyRegularExpression(model, regularExpressionValidator); |
|||
break; |
|||
case IBetweenValidator betweenValidator: |
|||
ApplyBetween(model, betweenValidator); |
|||
break; |
|||
case IComparisonValidator comparisonValidator: |
|||
ApplyComparison(model, comparisonValidator); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyLength(PropertyApiDescriptionModel model, ILengthValidator validator) |
|||
{ |
|||
// Every length validator has a Func<T,int> form that leaves the bound at zero on the
|
|||
// descriptor, so a zero is not distinguishable from a literal one and is left out.
|
|||
if (validator.Min > 0) |
|||
{ |
|||
model.MinLength = model.MinLength.HasValue |
|||
? Math.Max(model.MinLength.Value, validator.Min) |
|||
: validator.Min; |
|||
} |
|||
|
|||
if (validator.Max > 0) |
|||
{ |
|||
model.MaxLength = model.MaxLength.HasValue |
|||
? Math.Min(model.MaxLength.Value, validator.Max) |
|||
: validator.Max; |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyRegularExpression(PropertyApiDescriptionModel model, IRegularExpressionValidator validator) |
|||
{ |
|||
// A single Regex field cannot express "must match all of them", so the first pattern wins.
|
|||
if (!model.Regex.IsNullOrWhiteSpace() || validator.Expression.IsNullOrWhiteSpace()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
model.Regex = validator.Expression; |
|||
} |
|||
|
|||
protected virtual void ApplyComparison(PropertyApiDescriptionModel model, IComparisonValidator validator) |
|||
{ |
|||
switch (validator.Comparison) |
|||
{ |
|||
case Comparison.GreaterThan: |
|||
ApplyMinimum(model, validator.ValueToCompare, isExclusive: true); |
|||
break; |
|||
case Comparison.GreaterThanOrEqual: |
|||
ApplyMinimum(model, validator.ValueToCompare, isExclusive: false); |
|||
break; |
|||
case Comparison.LessThan: |
|||
ApplyMaximum(model, validator.ValueToCompare, isExclusive: true); |
|||
break; |
|||
case Comparison.LessThanOrEqual: |
|||
ApplyMaximum(model, validator.ValueToCompare, isExclusive: false); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyBetween(PropertyApiDescriptionModel model, IBetweenValidator validator) |
|||
{ |
|||
var isExclusive = validator is not IInclusiveBetweenValidator; |
|||
|
|||
ApplyMinimum(model, validator.From, isExclusive); |
|||
ApplyMaximum(model, validator.To, isExclusive); |
|||
} |
|||
|
|||
protected virtual void ApplyMinimum(PropertyApiDescriptionModel model, object? value, bool isExclusive) |
|||
{ |
|||
// Minimum and Maximum are ordered bounds, so a value that is not a number, such as a
|
|||
// DateTime, has nothing meaningful to publish there.
|
|||
if (!TryGetNumber(value, out var minimum)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (model.Minimum != null) |
|||
{ |
|||
if (!TryParseNumber(model.Minimum, out var existingMinimum)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// The flag follows the winning bound instead of being combined, otherwise ">= 10" merged with "> 5" would become "> 10".
|
|||
var existingIsExclusive = model.MinimumIsExclusive == true; |
|||
if (existingMinimum > minimum || (existingMinimum == minimum && existingIsExclusive)) |
|||
{ |
|||
minimum = existingMinimum; |
|||
isExclusive = existingIsExclusive; |
|||
} |
|||
} |
|||
|
|||
model.Minimum = minimum.ToString(CultureInfo.InvariantCulture); |
|||
model.MinimumIsExclusive = isExclusive; |
|||
} |
|||
|
|||
protected virtual void ApplyMaximum(PropertyApiDescriptionModel model, object? value, bool isExclusive) |
|||
{ |
|||
if (!TryGetNumber(value, out var maximum)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (model.Maximum != null) |
|||
{ |
|||
if (!TryParseNumber(model.Maximum, out var existingMaximum)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var existingIsExclusive = model.MaximumIsExclusive == true; |
|||
if (existingMaximum < maximum || (existingMaximum == maximum && existingIsExclusive)) |
|||
{ |
|||
maximum = existingMaximum; |
|||
isExclusive = existingIsExclusive; |
|||
} |
|||
} |
|||
|
|||
model.Maximum = maximum.ToString(CultureInfo.InvariantCulture); |
|||
model.MaximumIsExclusive = isExclusive; |
|||
} |
|||
|
|||
protected virtual bool TryGetNumber(object? value, out decimal number) |
|||
{ |
|||
// A comparison against another property has no value to read.
|
|||
var bound = value != null ? Convert.ToString(value, CultureInfo.InvariantCulture) : null; |
|||
return TryParseNumber(bound, out number); |
|||
} |
|||
|
|||
protected virtual bool TryParseNumber(string? value, out decimal number) |
|||
{ |
|||
// Float allows the exponent notation but not the group separators, which a Range bound
|
|||
// rendered by a decimal-comma culture would otherwise smuggle in as "1,5" meaning 15.
|
|||
return decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out number); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.test" |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.test.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net10.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" /> |
|||
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Http.FluentValidation\Volo.Abp.Http.FluentValidation.csproj" /> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Testing; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation; |
|||
|
|||
public abstract class AbpHttpFluentValidationTestBase<TStartupModule> : AbpIntegratedTest<TStartupModule> |
|||
where TStartupModule : IAbpModule |
|||
{ |
|||
protected virtual async Task<PropertyApiDescriptionModel> GetPropertyAsync<TDto>(string propertyName) |
|||
{ |
|||
var typeModel = await CreateTypeModelAsync(typeof(TDto)); |
|||
return typeModel.Properties!.Single(x => x.Name == propertyName); |
|||
} |
|||
|
|||
protected virtual async Task<TypeApiDescriptionModel> CreateTypeModelAsync(Type type) |
|||
{ |
|||
var typeModel = TypeApiDescriptionModel.Create(type); |
|||
var contributors = ServiceProvider.GetServices<IPropertyApiDescriptionModelContributor>().ToArray(); |
|||
|
|||
foreach (var propertyModel in typeModel.Properties!) |
|||
{ |
|||
var context = new PropertyApiDescriptionModelContributionContext(propertyModel, type); |
|||
foreach (var contributor in contributors) |
|||
{ |
|||
await contributor.ContributeAsync(context); |
|||
} |
|||
} |
|||
|
|||
return typeModel; |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.Autofac; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpAutofacModule), |
|||
typeof(AbpHttpFluentValidationModule) |
|||
)] |
|||
public class AbpHttpFluentValidationTestModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,302 @@ |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Volo.Abp.Autofac; |
|||
using Volo.Abp.Http.FluentValidation.TestObjects; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.Localization; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation; |
|||
|
|||
public class FluentValidationApiDescription_Tests : AbpHttpFluentValidationTestBase<AbpHttpFluentValidationTestModule> |
|||
{ |
|||
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
|||
{ |
|||
options.UseAutofac(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Register_The_Contributor() |
|||
{ |
|||
ServiceProvider |
|||
.GetServices<IPropertyApiDescriptionModelContributor>() |
|||
.ShouldContain(x => x is FluentValidationPropertyApiDescriptionModelContributor); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Map_Required_Rules() |
|||
{ |
|||
(await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.NotEmptyValue))).IsRequired.ShouldBeTrue(); |
|||
(await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.NotNullValue))).IsRequired.ShouldBeTrue(); |
|||
(await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.UnconstrainedValue))).IsRequired.ShouldBeFalse(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Map_Length_Rules() |
|||
{ |
|||
var length = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.LengthValue)); |
|||
length.MinLength.ShouldBe(3); |
|||
length.MaxLength.ShouldBe(10); |
|||
|
|||
var minimumLength = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.MinimumLengthValue)); |
|||
minimumLength.MinLength.ShouldBe(4); |
|||
minimumLength.MaxLength.ShouldBeNull(); |
|||
|
|||
var maximumLength = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.MaximumLengthValue)); |
|||
maximumLength.MinLength.ShouldBeNull(); |
|||
maximumLength.MaxLength.ShouldBe(12); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_Zero_Length_Rules() |
|||
{ |
|||
// A Func<T, int> bound is reported as a zero too, so a zero can not be published safely.
|
|||
(await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.EmptyOnlyValue))).MaxLength.ShouldBeNull(); |
|||
(await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.ZeroLengthValue))).MaxLength.ShouldBeNull(); |
|||
|
|||
var dynamicLength = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.DynamicLengthValue)); |
|||
dynamicLength.MinLength.ShouldBeNull(); |
|||
dynamicLength.MaxLength.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_A_Comparison_Bound_That_Is_Not_A_Number() |
|||
{ |
|||
var property = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.DateComparisonValue)); |
|||
|
|||
property.Minimum.ShouldBeNull(); |
|||
property.MinimumIsExclusive.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Map_A_Bound_Written_In_The_Exponent_Notation() |
|||
{ |
|||
var property = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.SmallExponentValue)); |
|||
|
|||
property.Minimum.ShouldBe("0.00000000000000000001"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Map_Regular_Expression_Rules() |
|||
{ |
|||
var property = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.RegexValue)); |
|||
|
|||
property.Regex.ShouldBe("^[a-z]+$"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Map_Inclusive_Comparison_Rules() |
|||
{ |
|||
var minimum = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.InclusiveMinimumValue)); |
|||
minimum.Minimum.ShouldBe("5"); |
|||
minimum.MinimumIsExclusive.ShouldBe(false); |
|||
|
|||
var maximum = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.InclusiveMaximumValue)); |
|||
maximum.Maximum.ShouldBe("50"); |
|||
maximum.MaximumIsExclusive.ShouldBe(false); |
|||
|
|||
var between = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.InclusiveBetweenValue)); |
|||
between.Minimum.ShouldBe("1"); |
|||
between.Maximum.ShouldBe("10"); |
|||
between.MinimumIsExclusive.ShouldBe(false); |
|||
between.MaximumIsExclusive.ShouldBe(false); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Map_Exclusive_Comparison_Rules() |
|||
{ |
|||
var minimum = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.ExclusiveMinimumValue)); |
|||
minimum.Minimum.ShouldBe("5"); |
|||
minimum.MinimumIsExclusive.ShouldBe(true); |
|||
|
|||
var maximum = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.ExclusiveMaximumValue)); |
|||
maximum.Maximum.ShouldBe("50"); |
|||
maximum.MaximumIsExclusive.ShouldBe(true); |
|||
|
|||
var between = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.ExclusiveBetweenValue)); |
|||
between.Minimum.ShouldBe("1"); |
|||
between.Maximum.ShouldBe("10"); |
|||
between.MinimumIsExclusive.ShouldBe(true); |
|||
between.MaximumIsExclusive.ShouldBe(true); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_Comparison_Against_Another_Property() |
|||
{ |
|||
var property = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.ComparedToOtherPropertyValue)); |
|||
|
|||
property.Minimum.ShouldBeNull(); |
|||
property.Maximum.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_RuleForEach_Rules_To_The_Collection_Property() |
|||
{ |
|||
var property = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.CollectionValue)); |
|||
|
|||
property.IsRequired.ShouldBeFalse(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_Conditional_Rules() |
|||
{ |
|||
(await GetPropertyAsync<ConditionalTestDto>(nameof(ConditionalTestDto.ChainedConditionValue))).IsRequired.ShouldBeFalse(); |
|||
(await GetPropertyAsync<ConditionalTestDto>(nameof(ConditionalTestDto.UnlessConditionValue))).IsRequired.ShouldBeFalse(); |
|||
(await GetPropertyAsync<ConditionalTestDto>(nameof(ConditionalTestDto.AsyncConditionValue))).IsRequired.ShouldBeFalse(); |
|||
(await GetPropertyAsync<ConditionalTestDto>(nameof(ConditionalTestDto.UnconditionalValue))).IsRequired.ShouldBeTrue(); |
|||
|
|||
var blockCondition = await GetPropertyAsync<ConditionalTestDto>(nameof(ConditionalTestDto.BlockConditionValue)); |
|||
blockCondition.IsRequired.ShouldBeFalse(); |
|||
blockCondition.MaxLength.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_Rules_Of_Named_Rule_Sets() |
|||
{ |
|||
var namedRuleSet = await GetPropertyAsync<RuleSetTestDto>(nameof(RuleSetTestDto.NamedRuleSetValue)); |
|||
namedRuleSet.IsRequired.ShouldBeFalse(); |
|||
namedRuleSet.MinLength.ShouldBeNull(); |
|||
|
|||
var defaultRuleSet = await GetPropertyAsync<RuleSetTestDto>(nameof(RuleSetTestDto.DefaultRuleSetValue)); |
|||
defaultRuleSet.IsRequired.ShouldBeTrue(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Keep_The_Stricter_Bound_When_Merging_With_Data_Annotations() |
|||
{ |
|||
var length = await GetPropertyAsync<DataAnnotationTestDto>(nameof(DataAnnotationTestDto.MergedLengthValue)); |
|||
length.MinLength.ShouldBe(5); |
|||
length.MaxLength.ShouldBe(10); |
|||
|
|||
var range = await GetPropertyAsync<DataAnnotationTestDto>(nameof(DataAnnotationTestDto.MergedRangeValue)); |
|||
range.Minimum.ShouldBe("10"); |
|||
range.Maximum.ShouldBe("90"); |
|||
range.MinimumIsExclusive.ShouldBe(false); |
|||
range.MaximumIsExclusive.ShouldBe(false); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Read_Exclusive_Bounds_From_The_Range_Attribute() |
|||
{ |
|||
var property = await GetPropertyAsync<DataAnnotationTestDto>(nameof(DataAnnotationTestDto.ExclusiveAttributeValue)); |
|||
|
|||
property.Minimum.ShouldBe("1"); |
|||
property.Maximum.ShouldBe("100"); |
|||
property.MinimumIsExclusive.ShouldBe(true); |
|||
property.MaximumIsExclusive.ShouldBe(true); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Keep_The_Exclusivity_Of_The_Winning_Bound() |
|||
{ |
|||
// The attribute bounds are stricter, so the exclusive flags must not leak onto them.
|
|||
var looser = await GetPropertyAsync<DataAnnotationTestDto>(nameof(DataAnnotationTestDto.LooserFluentBoundValue)); |
|||
looser.Minimum.ShouldBe("0"); |
|||
looser.Maximum.ShouldBe("100"); |
|||
looser.MinimumIsExclusive.ShouldBe(false); |
|||
looser.MaximumIsExclusive.ShouldBe(false); |
|||
|
|||
// On an equal bound the exclusive rule is the stricter one and wins.
|
|||
var same = await GetPropertyAsync<DataAnnotationTestDto>(nameof(DataAnnotationTestDto.SameBoundValue)); |
|||
same.Minimum.ShouldBe("10"); |
|||
same.Maximum.ShouldBe("90"); |
|||
same.MinimumIsExclusive.ShouldBe(true); |
|||
same.MaximumIsExclusive.ShouldBe(true); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Keep_The_Attribute_Regular_Expression() |
|||
{ |
|||
var property = await GetPropertyAsync<DataAnnotationTestDto>(nameof(DataAnnotationTestDto.AttributeRegexValue)); |
|||
|
|||
property.Regex.ShouldBe("^attribute$"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Ignore_Types_Without_A_Validator() |
|||
{ |
|||
var property = await GetPropertyAsync<UnvalidatedTestDto>(nameof(UnvalidatedTestDto.Value)); |
|||
|
|||
property.IsRequired.ShouldBeFalse(); |
|||
property.MinLength.ShouldBeNull(); |
|||
property.MaxLength.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Ignore_Open_Generic_Types() |
|||
{ |
|||
var typeModel = await CreateTypeModelAsync(typeof(GenericTestDto<>)); |
|||
var property = typeModel.Properties!.Single(x => x.Name == nameof(GenericTestDto<string>.Name)); |
|||
|
|||
property.IsRequired.ShouldBeFalse(); |
|||
property.MinLength.ShouldBeNull(); |
|||
property.MaxLength.ShouldBeNull(); |
|||
property.Regex.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Merge_Range_Attribute_Bounds_Under_Any_Culture() |
|||
{ |
|||
using (CultureHelper.Use(CultureInfo.GetCultureInfo("de-DE"))) |
|||
{ |
|||
var property = await GetPropertyAsync<CultureTestDto>(nameof(CultureTestDto.DecimalRangeValue)); |
|||
|
|||
property.Minimum.ShouldBe("2"); |
|||
property.Maximum.ShouldBe("9.5"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Map_Rules_With_An_Overridden_Property_Name() |
|||
{ |
|||
var property = await GetPropertyAsync<PropertyNameTestDto>(nameof(PropertyNameTestDto.RenamedValue)); |
|||
|
|||
property.IsRequired.ShouldBeTrue(); |
|||
property.MaxLength.ShouldBe(20); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_Rules_Of_A_Nested_Object_To_A_Property_Of_The_Same_Name() |
|||
{ |
|||
var property = await GetPropertyAsync<PropertyNameTestDto>(nameof(PropertyNameTestDto.City)); |
|||
|
|||
property.IsRequired.ShouldBeFalse(); |
|||
property.MaxLength.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_Rules_Of_A_Nested_Object_To_The_Nested_Type() |
|||
{ |
|||
// The model of the nested type is shared by every DTO that uses it, so a rule declared
|
|||
// by the validator of one of them can not be attributed to it.
|
|||
var property = await GetPropertyAsync<PropertyNameTestAddress>(nameof(PropertyNameTestAddress.City)); |
|||
|
|||
property.IsRequired.ShouldBeFalse(); |
|||
property.MaxLength.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Map_Rules_Of_A_Derived_Validator_To_A_Base_Property() |
|||
{ |
|||
// Each type describes only the properties it declares, and the base type is described
|
|||
// with its own validator, which does not exist here.
|
|||
var inherited = await GetPropertyAsync<InheritanceTestBaseDto>(nameof(InheritanceTestBaseDto.InheritedValue)); |
|||
inherited.IsRequired.ShouldBeFalse(); |
|||
inherited.MaxLength.ShouldBeNull(); |
|||
|
|||
var own = await GetPropertyAsync<InheritanceTestDto>(nameof(InheritanceTestDto.OwnValue)); |
|||
own.IsRequired.ShouldBeTrue(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Keep_A_Bound_That_Is_Not_A_Number() |
|||
{ |
|||
var property = await GetPropertyAsync<CultureTestDto>(nameof(CultureTestDto.DateRangeValue)); |
|||
|
|||
property.Minimum.ShouldBe("2020-01-01"); |
|||
property.Maximum.ShouldBe("2030-01-01"); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Http.FluentValidation.TestObjects; |
|||
using Volo.Abp.Modularity; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation; |
|||
|
|||
public class FluentValidationApiDescription_WithoutAutofac_Tests |
|||
: AbpHttpFluentValidationTestBase<FluentValidationApiDescription_WithoutAutofac_Tests.TestModule> |
|||
{ |
|||
[DependsOn(typeof(AbpHttpFluentValidationModule))] |
|||
public class TestModule : AbpModule |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Contribute_Without_Autofac() |
|||
{ |
|||
var property = await GetPropertyAsync<ConstraintTestDto>(nameof(ConstraintTestDto.LengthValue)); |
|||
|
|||
property.MinLength.ShouldBe(3); |
|||
property.MaxLength.ShouldBe(10); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using System.Threading.Tasks; |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class ConditionalTestDto |
|||
{ |
|||
public bool Enabled { get; set; } |
|||
|
|||
public string? ChainedConditionValue { get; set; } |
|||
|
|||
public string? BlockConditionValue { get; set; } |
|||
|
|||
public string? UnlessConditionValue { get; set; } |
|||
|
|||
public string? AsyncConditionValue { get; set; } |
|||
|
|||
public string? UnconditionalValue { get; set; } |
|||
} |
|||
|
|||
public class ConditionalTestDtoValidator : AbstractValidator<ConditionalTestDto> |
|||
{ |
|||
public ConditionalTestDtoValidator() |
|||
{ |
|||
RuleFor(x => x.ChainedConditionValue).NotEmpty().When(x => x.Enabled); |
|||
|
|||
When(x => x.Enabled, () => |
|||
{ |
|||
RuleFor(x => x.BlockConditionValue).NotEmpty().MaximumLength(32); |
|||
}); |
|||
|
|||
Unless(x => x.Enabled, () => |
|||
{ |
|||
RuleFor(x => x.UnlessConditionValue).NotEmpty(); |
|||
}); |
|||
|
|||
RuleFor(x => x.AsyncConditionValue).NotEmpty().WhenAsync((x, _) => Task.FromResult(x.Enabled)); |
|||
|
|||
RuleFor(x => x.UnconditionalValue).NotEmpty(); |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class ConstraintTestDto |
|||
{ |
|||
public string? NotEmptyValue { get; set; } |
|||
|
|||
public string? NotNullValue { get; set; } |
|||
|
|||
public string? LengthValue { get; set; } |
|||
|
|||
public string? MinimumLengthValue { get; set; } |
|||
|
|||
public string? MaximumLengthValue { get; set; } |
|||
|
|||
public string? RegexValue { get; set; } |
|||
|
|||
public int InclusiveMinimumValue { get; set; } |
|||
|
|||
public int InclusiveMaximumValue { get; set; } |
|||
|
|||
public int ExclusiveMinimumValue { get; set; } |
|||
|
|||
public int ExclusiveMaximumValue { get; set; } |
|||
|
|||
public int InclusiveBetweenValue { get; set; } |
|||
|
|||
public int ExclusiveBetweenValue { get; set; } |
|||
|
|||
public int ComparedToOtherPropertyValue { get; set; } |
|||
|
|||
public List<string> CollectionValue { get; set; } = new List<string>(); |
|||
|
|||
public string? EmptyOnlyValue { get; set; } |
|||
|
|||
public string? ZeroLengthValue { get; set; } |
|||
|
|||
public string? DynamicLengthValue { get; set; } |
|||
|
|||
public DateTime DateComparisonValue { get; set; } |
|||
|
|||
public double SmallExponentValue { get; set; } |
|||
|
|||
public string? UnconstrainedValue { get; set; } |
|||
} |
|||
|
|||
public class ConstraintTestDtoValidator : AbstractValidator<ConstraintTestDto> |
|||
{ |
|||
public ConstraintTestDtoValidator() |
|||
{ |
|||
RuleFor(x => x.NotEmptyValue).NotEmpty(); |
|||
RuleFor(x => x.NotNullValue).NotNull(); |
|||
RuleFor(x => x.LengthValue).Length(3, 10); |
|||
RuleFor(x => x.MinimumLengthValue).MinimumLength(4); |
|||
RuleFor(x => x.MaximumLengthValue).MaximumLength(12); |
|||
RuleFor(x => x.RegexValue).Matches("^[a-z]+$"); |
|||
RuleFor(x => x.InclusiveMinimumValue).GreaterThanOrEqualTo(5); |
|||
RuleFor(x => x.InclusiveMaximumValue).LessThanOrEqualTo(50); |
|||
RuleFor(x => x.ExclusiveMinimumValue).GreaterThan(5); |
|||
RuleFor(x => x.ExclusiveMaximumValue).LessThan(50); |
|||
RuleFor(x => x.InclusiveBetweenValue).InclusiveBetween(1, 10); |
|||
RuleFor(x => x.ExclusiveBetweenValue).ExclusiveBetween(1, 10); |
|||
RuleFor(x => x.ComparedToOtherPropertyValue).GreaterThanOrEqualTo(x => x.InclusiveMinimumValue); |
|||
RuleForEach(x => x.CollectionValue).NotEmpty(); |
|||
RuleFor(x => x.EmptyOnlyValue).MaximumLength(0); |
|||
RuleFor(x => x.ZeroLengthValue).Length(0, 0); |
|||
RuleFor(x => x.DateComparisonValue).GreaterThanOrEqualTo(new DateTime(2020, 1, 1)); |
|||
RuleFor(x => x.DynamicLengthValue).Length(_ => 2, _ => 8); |
|||
RuleFor(x => x.SmallExponentValue).GreaterThanOrEqualTo(1e-20); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class CultureTestDto |
|||
{ |
|||
[Range(1.5, 9.5)] |
|||
public double DecimalRangeValue { get; set; } |
|||
|
|||
[Range(typeof(DateTime), "2020-01-01", "2030-01-01")] |
|||
public DateTime DateRangeValue { get; set; } |
|||
} |
|||
|
|||
public class CultureTestDtoValidator : AbstractValidator<CultureTestDto> |
|||
{ |
|||
public CultureTestDtoValidator() |
|||
{ |
|||
RuleFor(x => x.DecimalRangeValue).GreaterThanOrEqualTo(2.0); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System.ComponentModel.DataAnnotations; |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class DataAnnotationTestDto |
|||
{ |
|||
[MinLength(2)] |
|||
[MaxLength(50)] |
|||
public string? MergedLengthValue { get; set; } |
|||
|
|||
[RegularExpression("^attribute$")] |
|||
public string? AttributeRegexValue { get; set; } |
|||
|
|||
[Range(0, 100)] |
|||
public int MergedRangeValue { get; set; } |
|||
|
|||
[Range(0, 100)] |
|||
public int LooserFluentBoundValue { get; set; } |
|||
|
|||
[Range(10, 90)] |
|||
public int SameBoundValue { get; set; } |
|||
|
|||
[Range(1, 100, MinimumIsExclusive = true, MaximumIsExclusive = true)] |
|||
public int ExclusiveAttributeValue { get; set; } |
|||
} |
|||
|
|||
public class DataAnnotationTestDtoValidator : AbstractValidator<DataAnnotationTestDto> |
|||
{ |
|||
public DataAnnotationTestDtoValidator() |
|||
{ |
|||
RuleFor(x => x.MergedLengthValue).Length(5, 10); |
|||
RuleFor(x => x.AttributeRegexValue).Matches("^fluent$"); |
|||
RuleFor(x => x.MergedRangeValue).GreaterThanOrEqualTo(10).LessThanOrEqualTo(90); |
|||
RuleFor(x => x.LooserFluentBoundValue).GreaterThan(-5).LessThan(500); |
|||
RuleFor(x => x.SameBoundValue).GreaterThan(10).LessThan(90); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class GenericTestDto<T> |
|||
{ |
|||
public T? Value { get; set; } |
|||
|
|||
public string? Name { get; set; } |
|||
} |
|||
|
|||
public class GenericTestDtoValidator : AbstractValidator<GenericTestDto<string>> |
|||
{ |
|||
public GenericTestDtoValidator() |
|||
{ |
|||
RuleFor(x => x.Name).NotEmpty().Length(3, 9).Matches("^[a-z]+$"); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class InheritanceTestBaseDto |
|||
{ |
|||
public string? InheritedValue { get; set; } |
|||
} |
|||
|
|||
public class InheritanceTestDto : InheritanceTestBaseDto |
|||
{ |
|||
public string? OwnValue { get; set; } |
|||
} |
|||
|
|||
public class InheritanceTestDtoValidator : AbstractValidator<InheritanceTestDto> |
|||
{ |
|||
public InheritanceTestDtoValidator() |
|||
{ |
|||
RuleFor(x => x.InheritedValue).NotEmpty().MaximumLength(15); |
|||
RuleFor(x => x.OwnValue).NotEmpty(); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class PropertyNameTestDto |
|||
{ |
|||
public string? RenamedValue { get; set; } |
|||
|
|||
public string? City { get; set; } |
|||
|
|||
public PropertyNameTestAddress Address { get; set; } = new PropertyNameTestAddress(); |
|||
} |
|||
|
|||
public class PropertyNameTestAddress |
|||
{ |
|||
public string? City { get; set; } |
|||
} |
|||
|
|||
public class PropertyNameTestDtoValidator : AbstractValidator<PropertyNameTestDto> |
|||
{ |
|||
public PropertyNameTestDtoValidator() |
|||
{ |
|||
RuleFor(x => x.RenamedValue).NotEmpty().MaximumLength(20).OverridePropertyName("displayed_name"); |
|||
RuleFor(x => x.Address.City).NotEmpty().MaximumLength(30); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using FluentValidation; |
|||
|
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class RuleSetTestDto |
|||
{ |
|||
public string? NamedRuleSetValue { get; set; } |
|||
|
|||
public string? DefaultRuleSetValue { get; set; } |
|||
} |
|||
|
|||
public class RuleSetTestDtoValidator : AbstractValidator<RuleSetTestDto> |
|||
{ |
|||
public RuleSetTestDtoValidator() |
|||
{ |
|||
RuleSet("Create", () => |
|||
{ |
|||
RuleFor(x => x.NamedRuleSetValue).NotEmpty().MinimumLength(4); |
|||
}); |
|||
|
|||
RuleSet("default", () => |
|||
{ |
|||
RuleFor(x => x.DefaultRuleSetValue).NotEmpty(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.Http.FluentValidation.TestObjects; |
|||
|
|||
public class UnvalidatedTestDto |
|||
{ |
|||
public string? Value { get; set; } |
|||
} |
|||
Loading…
Reference in new issue