Browse Source

Add a contributor extension point to the property api description

IPropertyApiDescriptionModelContributor lets a package enrich the properties of
a type after they are created from the attributes. The contributors are applied
by AspNetCoreApiDescriptionModelProvider, so TypeApiDescriptionModel stays a
plain model.
pull/26112/head
maliming 5 days ago
parent
commit
22ebf24aed
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 35
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs
  2. 1
      framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj
  3. 11
      framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs
  4. 377
      framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentValidationApiDescriptionModelContributor.cs
  5. 4
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs
  6. 21
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModelContributionContext.cs
  7. 16
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs

35
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs

@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -35,19 +34,22 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
private readonly AbpAspNetCoreMvcOptions _abpAspNetCoreMvcOptions;
private readonly AbpApiDescriptionModelOptions _modelOptions;
private readonly IXmlDocumentationProvider _xmlDocProvider;
public IAbpLazyServiceProvider LazyServiceProvider { get; set; } = default!;
private readonly IPropertyApiDescriptionModelContributor[] _propertyContributors;
public AspNetCoreApiDescriptionModelProvider(
IOptions<AspNetCoreApiDescriptionModelProviderOptions> options,
IApiDescriptionGroupCollectionProvider descriptionProvider,
IOptions<AbpAspNetCoreMvcOptions> abpAspNetCoreMvcOptions,
IOptions<AbpApiDescriptionModelOptions> modelOptions,
IXmlDocumentationProvider xmlDocProvider)
IXmlDocumentationProvider xmlDocProvider,
IEnumerable<IPropertyApiDescriptionModelContributor> propertyContributors)
{
_options = options.Value;
_descriptionProvider = descriptionProvider;
_abpAspNetCoreMvcOptions = abpAspNetCoreMvcOptions.Value;
_modelOptions = modelOptions.Value;
_xmlDocProvider = xmlDocProvider;
_propertyContributors = propertyContributors.ToArray();
Logger = NullLogger<AspNetCoreApiDescriptionModelProvider>.Instance;
}
@ -315,19 +317,15 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
return;
}
var contributors =
LazyServiceProvider
.GetServices<IPropertyApiDescriptionModelContributor>()
.ToHashSet();
var typeModel = TypeApiDescriptionModel.Create(type, contributors);
applicationModel.Types[typeName] = typeModel;
applicationModel.Types[typeName] = TypeApiDescriptionModel.Create(type);
if (includeDescriptions)
{
await PopulateTypeDescriptionsAsync(applicationModel.Types[typeName], type);
}
await ContributeToPropertiesAsync(applicationModel.Types[typeName], type);
await AddCustomTypesToModelAsync(applicationModel, type.BaseType, includeDescriptions);
foreach (var propertyInfo in type.GetProperties().Where(p => p.DeclaringType == type))
@ -336,6 +334,23 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
}
}
protected virtual async Task ContributeToPropertiesAsync(TypeApiDescriptionModel typeModel, Type type)
{
if (_propertyContributors.IsNullOrEmpty() || typeModel.Properties.IsNullOrEmpty())
{
return;
}
foreach (var propertyModel in typeModel.Properties!)
{
var context = new PropertyApiDescriptionModelContributionContext(propertyModel, type);
foreach (var contributor in _propertyContributors)
{
await contributor.ContributeAsync(context);
}
}
}
private static string CalculateTypeName(Type type)
{
if (!type.IsGenericTypeDefinition)

1
framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj

@ -22,7 +22,6 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Validation\Volo.Abp.Validation.csproj" />
<ProjectReference Include="..\Volo.Abp.Http\Volo.Abp.Http.csproj" />
</ItemGroup>
</Project>

11
framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs

@ -1,14 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Modularity;
using Volo.Abp.Validation;
namespace Volo.Abp.FluentValidation;
[DependsOn(
typeof(AbpValidationModule),
typeof(AbpHttpModule)
typeof(AbpValidationModule)
)]
public class AbpFluentValidationModule : AbpModule
{
@ -16,10 +13,4 @@ public class AbpFluentValidationModule : AbpModule
{
context.Services.AddConventionalRegistrar(new AbpFluentValidationConventionalRegistrar());
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
base.ConfigureServices(context);
context.Services.AddTransient<IPropertyApiDescriptionModelContributor, FluentValidationApiDescriptionModelContributor>();
}
}

377
framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentValidationApiDescriptionModelContributor.cs

@ -1,377 +0,0 @@
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);
}
}

4
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs

@ -1,8 +1,8 @@
using System.Reflection;
using System.Threading.Tasks;
namespace Volo.Abp.Http.Modeling;
public interface IPropertyApiDescriptionModelContributor
{
void Contribute(PropertyApiDescriptionModel model, PropertyInfo propertyInfo);
Task ContributeAsync(PropertyApiDescriptionModelContributionContext context);
}

21
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModelContributionContext.cs

@ -0,0 +1,21 @@
using System;
using JetBrains.Annotations;
namespace Volo.Abp.Http.Modeling;
public class PropertyApiDescriptionModelContributionContext
{
[NotNull]
public PropertyApiDescriptionModel Model { get; }
[NotNull]
public Type DeclaringType { get; }
public PropertyApiDescriptionModelContributionContext(
[NotNull] PropertyApiDescriptionModel model,
[NotNull] Type declaringType)
{
Model = Check.NotNull(model, nameof(model));
DeclaringType = Check.NotNull(declaringType, nameof(declaringType));
}
}

16
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Volo.Abp.Reflection;
@ -34,7 +33,7 @@ public class TypeApiDescriptionModel
}
public static TypeApiDescriptionModel Create(Type type, IEnumerable<IPropertyApiDescriptionModelContributor>? contributors = default)
public static TypeApiDescriptionModel Create(Type type)
{
var baseType = type.BaseType;
if (baseType == typeof(object))
@ -58,18 +57,7 @@ public class TypeApiDescriptionModel
typeModel.Properties = type
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.DeclaringType == type)
.Select(p =>
{
var model = PropertyApiDescriptionModel.Create(p);
if (contributors != null && contributors.Any())
{
foreach (var contributor in contributors)
{
contributor.Contribute(model, p);
}
}
return model;
})
.Select(PropertyApiDescriptionModel.Create)
.ToArray();
if (type.IsGenericTypeDefinition)

Loading…
Cancel
Save