Browse Source

Add the Volo.Abp.Http.FluentValidation package

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
maliming 3 weeks ago
parent
commit
3b011f7703
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 65
      docs/en/framework/fundamentals/fluent-validation.md
  2. 2
      framework/Volo.Abp.slnx
  3. 3
      framework/src/Volo.Abp.Http.FluentValidation/Properties/AssemblyInfo.cs
  4. 3
      framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg
  5. 68
      framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg.analyze.json
  6. 24
      framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.csproj
  7. 12
      framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationModule.cs
  8. 254
      framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/FluentValidationPropertyApiDescriptionModelContributor.cs
  9. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj
  10. 4
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs
  11. 15
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs
  12. 3
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.abppkg
  13. 17
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.csproj
  14. 36
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestBase.cs
  15. 12
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestModule.cs
  16. 302
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs
  17. 25
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_WithoutAutofac_Tests.cs
  18. 41
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConditionalTestDto.cs
  19. 74
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConstraintTestDto.cs
  20. 22
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs
  21. 38
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/DataAnnotationTestDto.cs
  22. 18
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/GenericTestDto.cs
  23. 22
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/InheritanceTestDto.cs
  24. 26
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/PropertyNameTestDto.cs
  25. 26
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/RuleSetTestDto.cs
  26. 6
      framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/UnvalidatedTestDto.cs
  27. 2
      npm/ng-packs/packages/core/src/lib/proxy/volo/abp/http/modeling/models.ts
  28. 1
      nupkg/common.ps1

65
docs/en/framework/fundamentals/fluent-validation.md

@ -60,6 +60,71 @@ public class CreateUpdateBookDtoValidator : AbstractValidator<CreateUpdateBookDt
ABP will automatically find this class and associate with the `CreateUpdateBookDto` on object validation.
## Exposing the Rules in the API Definition
ABP creates the API definition from the data annotation attributes of the DTO properties. So, a DTO that is only validated by FluentValidation has no constraints in the API definition, and the client proxy generators can not see them. The [Volo.Abp.Http.FluentValidation](https://www.nuget.org/packages/Volo.Abp.Http.FluentValidation) NuGet package adds the FluentValidation rules to the API definition, merged with the ones coming from the attributes.
### Installation
Open a command line window in the folder of the project (.csproj file) that hosts your HTTP API and type the following command:
````bash
abp add-package Volo.Abp.Http.FluentValidation
````
If you want to manually install, add the [Volo.Abp.Http.FluentValidation](https://www.nuget.org/packages/Volo.Abp.Http.FluentValidation) NuGet package to your project and add the `AbpHttpFluentValidationModule` to the dependency list of your module:
````csharp
[DependsOn(
//...other dependencies
typeof(AbpHttpFluentValidationModule)
)]
public class YourModule : AbpModule
{
}
````
`AbpHttpFluentValidationModule` already depends on `AbpFluentValidationModule`, so you don't need both.
### Mapped Rules
The following rules are mapped:
| FluentValidation rule | API definition |
|---|---|
| `NotNull()`, `NotEmpty()` | `IsRequired` |
| `Length(min, max)`, `MinimumLength(min)`, `MaximumLength(max)` | `MinLength`, `MaxLength` |
| `Matches(...)` | `Regex` |
| `GreaterThanOrEqualTo(...)`, `GreaterThan(...)` | `Minimum` (+ `MinimumIsExclusive`) |
| `LessThanOrEqualTo(...)`, `LessThan(...)` | `Maximum` (+ `MaximumIsExclusive`) |
| `InclusiveBetween(...)`, `ExclusiveBetween(...)` | `Minimum`, `Maximum` (+ the exclusive flags) |
`MinimumIsExclusive` and `MaximumIsExclusive` indicate whether the value can be equal to the bound. They are also filled from the `Range` attribute, so an exclusive bound is not lost when it is declared with an attribute.
When a rule and an attribute constrain the same property, the stricter bound is used: the higher minimum and the lower maximum. When both bounds have the same value, the exclusive one is used. The exclusivity always comes from the bound that is used, so `[Range(0, 100)]` with `GreaterThan(-5)` results in an inclusive `Minimum = 0`. A non-numeric bound, like a `Range` attribute on a `DateTime` property, is kept as-is. An existing `Regex` is also kept, because a single value can not express two patterns that both have to match.
### Rules That Are Not Mapped
The following rules are not mapped, because they don't apply to every instance of the DTO:
* Rules under `When(...)` / `Unless(...)` (both the chained and the block form) and their async variants, because the same property can be required for one instance and optional for another.
* Rules that only belong to a non-default rule set, because ABP validates with FluentValidation's default selector, which does not run them.
* `RuleForEach(...)` rules, because they constrain the items of a collection rather than the collection property.
* Comparisons against another property, and any bound that is not a number.
### Rules That Are Not Fully Expressed
The following rules are not fully expressed in the API definition:
* A zero length bound, from `MaximumLength(0)` or `Length(0, 0)`, is not published. Every length rule has a `Func<T, int>` form that reports the same zero on the descriptor, so the two can not be told apart.
* Rules that come from an `Include(...)` call are not published, because FluentValidation does not expose the included validator on its descriptor.
* A validator of a derived DTO can not add rules to a property declared by its base class, because each type describes only its own properties.
* A rule on a nested object, like `RuleFor(x => x.Address.City)`, is not published either. The nested type is described on its own, with its own validator, and its model is shared by every DTO that uses it.
* A validator of a closed generic DTO is not used, because the API definition describes the generic type definition, which is shared by all of its instantiations.
* `Matches(pattern, RegexOptions)` publishes the pattern without the options. This is the one case where a client can be stricter than the server, so avoid the overload if the client should not reject what the server accepts.
> The API definition describes a type, while the server runs the validation per action. So, a DTO that is only used as a return value, or that is sent to an action which doesn't validate its parameters, still declares its constraints here. This is also how the data annotation attributes have always been reported.
## See Also
* [Validation System](./validation.md)

2
framework/Volo.Abp.slnx

@ -121,6 +121,7 @@
<Project Path="src/Volo.Abp.Http.Client.IdentityModel/Volo.Abp.Http.Client.IdentityModel.csproj" />
<Project Path="src/Volo.Abp.Http.Client.Web/Volo.Abp.Http.Client.Web.csproj" />
<Project Path="src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj" />
<Project Path="src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.csproj" />
<Project Path="src/Volo.Abp.Http/Volo.Abp.Http.csproj" />
<Project Path="src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj" />
<Project Path="src/Volo.Abp.Imaging.Abstractions/Volo.Abp.Imaging.Abstractions.csproj" />
@ -229,6 +230,7 @@
<Project Path="test/Volo.Abp.GlobalFeatures.Tests/Volo.Abp.GlobalFeatures.Tests.csproj" />
<Project Path="test/Volo.Abp.Http.Client.IdentityModel.Web.Tests/Volo.Abp.Http.Client.IdentityModel.Web.Tests.csproj" />
<Project Path="test/Volo.Abp.Http.Client.Tests/Volo.Abp.Http.Client.Tests.csproj" />
<Project Path="test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.csproj" />
<Project Path="test/Volo.Abp.Http.Tests/Volo.Abp.Http.Tests.csproj" />
<Project Path="test/Volo.Abp.IdentityModel.Tests/Volo.Abp.IdentityModel.Tests.csproj" />
<Project Path="test/Volo.Abp.Imaging.Abstractions.Tests/Volo.Abp.Imaging.Abstractions.Tests.csproj" />

3
framework/src/Volo.Abp.Http.FluentValidation/Properties/AssemblyInfo.cs

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Volo.Abp.Http.FluentValidation.Tests")]

3
framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg

@ -0,0 +1,3 @@
{
"role": "lib.framework"
}

68
framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg.analyze.json

@ -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
}
]
}

24
framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.csproj

@ -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>

12
framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationModule.cs

@ -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
{
}

254
framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/FluentValidationPropertyApiDescriptionModelContributor.cs

@ -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);
}
}

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

@ -27,7 +27,7 @@
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Components.WebAssembly\Volo.Abp.AspNetCore.Components.WebAssembly.csproj" />
<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="..\..\src\Volo.Abp.Http.FluentValidation\Volo.Abp.Http.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

@ -24,7 +24,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.Http.FluentValidation;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.AspNetCore.Mvc;
@ -34,7 +34,7 @@ namespace Volo.Abp.AspNetCore.Mvc;
typeof(AbpMemoryDbTestModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpAutofacModule),
typeof(AbpFluentValidationModule)
typeof(AbpHttpFluentValidationModule)
)]
public class AbpAspNetCoreMvcTestModule : AbpModule
{

15
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Http.Modeling;
using Volo.Abp.TestApp.Application;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring;
@ -75,6 +76,20 @@ public class AbpApiDefinitionController_Tests : AspNetCoreMvcTestBase
action.AuthorizeDatas.ShouldContain(a => a.Policy == "TestPolicy2" && a.Roles == "Manager");
}
[Fact]
public async Task Should_Include_FluentValidation_Rules_In_Types()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>("/api/abp/api-definition?includeTypes=true");
var property = model.Types[typeof(FluentValidationTestInput).FullName!]
.Properties!
.Single(p => p.Name == nameof(FluentValidationTestInput.Name));
property.IsRequired.ShouldBeTrue();
property.MinLength.ShouldBe(3);
property.MaxLength.ShouldBe(10);
}
private static ControllerApiDescriptionModel GetPeopleController(ApplicationApiDescriptionModel model)
{
return model.Modules.Values

3
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.abppkg

@ -0,0 +1,3 @@
{
"role": "lib.test"
}

17
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.csproj

@ -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>

36
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestBase.cs

@ -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;
}
}

12
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestModule.cs

@ -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
{
}

302
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs

@ -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");
}
}

25
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_WithoutAutofac_Tests.cs

@ -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);
}
}

41
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConditionalTestDto.cs

@ -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();
}
}

74
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConstraintTestDto.cs

@ -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);
}
}

22
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs

@ -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);
}
}

38
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/DataAnnotationTestDto.cs

@ -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);
}
}

18
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/GenericTestDto.cs

@ -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]+$");
}
}

22
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/InheritanceTestDto.cs

@ -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();
}
}

26
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/PropertyNameTestDto.cs

@ -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);
}
}

26
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/RuleSetTestDto.cs

@ -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();
});
}
}

6
framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/UnvalidatedTestDto.cs

@ -0,0 +1,6 @@
namespace Volo.Abp.Http.FluentValidation.TestObjects;
public class UnvalidatedTestDto
{
public string? Value { get; set; }
}

2
npm/ng-packs/packages/core/src/lib/proxy/volo/abp/http/modeling/models.ts

@ -82,6 +82,8 @@ export interface PropertyApiDescriptionModel {
maxLength?: number;
minimum?: string;
maximum?: string;
minimumIsExclusive?: boolean;
maximumIsExclusive?: boolean;
regex?: string;
}

1
nupkg/common.ps1

@ -221,6 +221,7 @@ $projects = (
"framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly",
"framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor",
"framework/src/Volo.Abp.Http",
"framework/src/Volo.Abp.Http.FluentValidation",
"framework/src/Volo.Abp.IdentityModel",
"framework/src/Volo.Abp.Imaging.Abstractions",
"framework/src/Volo.Abp.Imaging.AspNetCore",

Loading…
Cancel
Save