mirror of https://github.com/abpframework/abp.git
3 changed files with 388 additions and 1 deletions
@ -0,0 +1,377 @@ |
|||
using FluentValidation; |
|||
using FluentValidation.Validators; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http.Modeling; |
|||
|
|||
namespace Volo.Abp.FluentValidation; |
|||
|
|||
public class FluentValidationApiDescriptionModelContributor : IPropertyApiDescriptionModelContributor |
|||
{ |
|||
public IAbpLazyServiceProvider LazyServiceProvider { get; set; } = default!; |
|||
public virtual void Contribute( |
|||
PropertyApiDescriptionModel model, |
|||
PropertyInfo propertyInfo) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(model); |
|||
ArgumentNullException.ThrowIfNull(propertyInfo); |
|||
|
|||
var declaringType = propertyInfo.DeclaringType; |
|||
|
|||
if (declaringType == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Guard against open generic types (e.g. a property inherited from
|
|||
// a generic base DTO like ExtensibleFullAuditedEntityDto<TPrimaryKey>
|
|||
// where TPrimaryKey hasn't been substituted with a concrete type).
|
|||
// There is no real IValidator<> for an open generic, so DI can never
|
|||
// have it registered — attempting the lookup would either throw or
|
|||
// be meaningless.
|
|||
if (declaringType.ContainsGenericParameters) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var validatorType = |
|||
typeof(IValidator<>).MakeGenericType(declaringType); |
|||
|
|||
var validators = |
|||
LazyServiceProvider |
|||
.GetServices(validatorType) |
|||
.ToArray(); |
|||
|
|||
if (validators.Length == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
foreach (var validator in validators) |
|||
{ |
|||
|
|||
if (validator is IValidator typedValidator) |
|||
{ |
|||
|
|||
ApplyValidatorRules( |
|||
model, |
|||
propertyInfo, |
|||
typedValidator); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyValidatorRules( |
|||
PropertyApiDescriptionModel model, |
|||
PropertyInfo propertyInfo, |
|||
IValidator validator) |
|||
{ |
|||
var descriptor = validator.CreateDescriptor(); |
|||
|
|||
var rule = descriptor |
|||
.GetMembersWithValidators() |
|||
.FirstOrDefault(x => |
|||
string.Equals( |
|||
x.Key, |
|||
propertyInfo.Name, |
|||
StringComparison.Ordinal)); |
|||
|
|||
if (rule == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
foreach (var (Validator, Options) in rule) |
|||
{ |
|||
/* |
|||
* Only statically-evaluable validators are reflected here. |
|||
* |
|||
* FluentValidation conditions such as: |
|||
* |
|||
* When(...) |
|||
* Unless(...) |
|||
* |
|||
* are intentionally ignored. They depend on the runtime state |
|||
* of the object being validated (other property values, external |
|||
* context, etc.), so there is no single correct answer to |
|||
* "is this required?" at the type/schema level — the same |
|||
* property could be required in one instance and optional in |
|||
* another. Baking a conditional rule into a static schema would |
|||
* misrepresent it either way, so we skip it rather than guess. |
|||
* |
|||
* If the validator is a custom/unsupported type, it simply |
|||
* won't match one of the cases below and is ignored. |
|||
*/ |
|||
if (Options.HasCondition || Options.HasAsyncCondition) |
|||
continue; |
|||
ApplyValidator( |
|||
model, |
|||
Validator); |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyValidator( |
|||
PropertyApiDescriptionModel model, |
|||
IPropertyValidator validator) |
|||
{ |
|||
switch (validator) |
|||
{ |
|||
// ============================================================
|
|||
// Required
|
|||
//
|
|||
// NotNull() -> value must not be null (empty string/whitespace
|
|||
// still pass)
|
|||
// NotEmpty() -> value must not be null AND not the "empty"
|
|||
// value for its type (empty/whitespace string,
|
|||
// default(T), empty collection, all fail)
|
|||
//
|
|||
// Both are stricter-or-equal to "must be present," so both
|
|||
// map to IsRequired = true. NotEmpty is the stronger check;
|
|||
// if both happen to be applied, keep IsRequired true either way.
|
|||
// ============================================================
|
|||
|
|||
case INotEmptyValidator: |
|||
case INotNullValidator: |
|||
|
|||
model.IsRequired = true; |
|||
|
|||
break; |
|||
|
|||
// ============================================================
|
|||
// Length
|
|||
//
|
|||
// Length(min, max)
|
|||
// MinimumLength(min)
|
|||
// MaximumLength(max)
|
|||
// ============================================================
|
|||
|
|||
case ILengthValidator lengthValidator: |
|||
|
|||
ApplyLength( |
|||
model, |
|||
lengthValidator); |
|||
|
|||
break; |
|||
|
|||
// ============================================================
|
|||
// Regex
|
|||
//
|
|||
// Matches(...)
|
|||
// ============================================================
|
|||
|
|||
case IRegularExpressionValidator regexValidator: |
|||
|
|||
ApplyRegex( |
|||
model, |
|||
regexValidator); |
|||
|
|||
break; |
|||
|
|||
// ============================================================
|
|||
// Comparisons
|
|||
//
|
|||
// GreaterThan(...)
|
|||
// GreaterThanOrEqualTo(...)
|
|||
// LessThan(...)
|
|||
// LessThanOrEqualTo(...)
|
|||
// ============================================================
|
|||
|
|||
case IComparisonValidator comparisonValidator: |
|||
|
|||
ApplyComparison( |
|||
model, |
|||
comparisonValidator); |
|||
|
|||
break; |
|||
|
|||
// ============================================================
|
|||
// Range shortcuts
|
|||
//
|
|||
// InclusiveBetween(min, max)
|
|||
// ExclusiveBetween(min, max)
|
|||
//
|
|||
// FluentValidation implements these as a single validator
|
|||
// exposing both bounds, rather than as two IComparisonValidator
|
|||
// instances, so they need their own case.
|
|||
// ============================================================
|
|||
|
|||
case IBetweenValidator betweenValidator: |
|||
|
|||
ApplyBetween( |
|||
model, |
|||
betweenValidator); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyLength( |
|||
PropertyApiDescriptionModel model, |
|||
ILengthValidator validator) |
|||
{ |
|||
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 ApplyRegex( |
|||
PropertyApiDescriptionModel model, |
|||
IRegularExpressionValidator validator) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace( |
|||
validator.Expression)) |
|||
{ |
|||
model.Regex = |
|||
validator.Expression; |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyComparison( |
|||
PropertyApiDescriptionModel model, |
|||
IComparisonValidator validator) |
|||
{ |
|||
var value = FormatComparisonValue(validator.ValueToCompare); |
|||
|
|||
if (value == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
switch (validator.Comparison) |
|||
{ |
|||
case Comparison.GreaterThan: |
|||
case Comparison.GreaterThanOrEqual: |
|||
|
|||
ApplyMinimum( |
|||
model, |
|||
value); |
|||
|
|||
break; |
|||
|
|||
case Comparison.LessThan: |
|||
case Comparison.LessThanOrEqual: |
|||
|
|||
ApplyMaximum( |
|||
model, |
|||
value); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected virtual void ApplyBetween( |
|||
PropertyApiDescriptionModel model, |
|||
IBetweenValidator validator) |
|||
{ |
|||
var from = FormatComparisonValue(validator.From); |
|||
var to = FormatComparisonValue(validator.To); |
|||
|
|||
if (from != null) |
|||
{ |
|||
ApplyMinimum( |
|||
model, |
|||
from); |
|||
} |
|||
|
|||
if (to != null) |
|||
{ |
|||
ApplyMaximum( |
|||
model, |
|||
to); |
|||
} |
|||
} |
|||
|
|||
protected virtual string? FormatComparisonValue(object? rawValue) |
|||
{ |
|||
if (rawValue == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var value = Convert.ToString( |
|||
rawValue, |
|||
CultureInfo.InvariantCulture); |
|||
|
|||
return string.IsNullOrWhiteSpace(value) |
|||
? null |
|||
: value; |
|||
} |
|||
|
|||
protected virtual void ApplyMinimum( |
|||
PropertyApiDescriptionModel model, |
|||
string value) |
|||
{ |
|||
if (!decimal.TryParse( |
|||
value, |
|||
NumberStyles.Number, |
|||
CultureInfo.InvariantCulture, |
|||
out var minimum)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (decimal.TryParse( |
|||
model.Minimum, |
|||
NumberStyles.Number, |
|||
CultureInfo.InvariantCulture, |
|||
out var existingMinimum)) |
|||
{ |
|||
minimum = Math.Max( |
|||
minimum, |
|||
existingMinimum); |
|||
} |
|||
|
|||
model.Minimum = |
|||
minimum.ToString( |
|||
CultureInfo.InvariantCulture); |
|||
} |
|||
|
|||
protected virtual void ApplyMaximum( |
|||
PropertyApiDescriptionModel model, |
|||
string value) |
|||
{ |
|||
if (!decimal.TryParse( |
|||
value, |
|||
NumberStyles.Number, |
|||
CultureInfo.InvariantCulture, |
|||
out var maximum)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (decimal.TryParse( |
|||
model.Maximum, |
|||
NumberStyles.Number, |
|||
CultureInfo.InvariantCulture, |
|||
out var existingMaximum)) |
|||
{ |
|||
maximum = Math.Min( |
|||
maximum, |
|||
existingMaximum); |
|||
} |
|||
|
|||
model.Maximum = |
|||
maximum.ToString( |
|||
CultureInfo.InvariantCulture); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue