diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index cbb42f7822..4c8f0a5bec 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/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 options, IApiDescriptionGroupCollectionProvider descriptionProvider, IOptions abpAspNetCoreMvcOptions, IOptions modelOptions, - IXmlDocumentationProvider xmlDocProvider) + IXmlDocumentationProvider xmlDocProvider, + IEnumerable propertyContributors) { _options = options.Value; _descriptionProvider = descriptionProvider; _abpAspNetCoreMvcOptions = abpAspNetCoreMvcOptions.Value; _modelOptions = modelOptions.Value; _xmlDocProvider = xmlDocProvider; + _propertyContributors = propertyContributors.ToArray(); Logger = NullLogger.Instance; } @@ -315,19 +317,15 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide return; } - var contributors = - LazyServiceProvider - .GetServices() - .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) diff --git a/framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj b/framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj index 0f2eb70d44..563ef49f80 100644 --- a/framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj +++ b/framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj @@ -22,7 +22,6 @@ - diff --git a/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs index fa2fc0885a..1753e76351 100644 --- a/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs +++ b/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(); - } } diff --git a/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentValidationApiDescriptionModelContributor.cs b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentValidationApiDescriptionModelContributor.cs deleted file mode 100644 index 7116ef4706..0000000000 --- a/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentValidationApiDescriptionModelContributor.cs +++ /dev/null @@ -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 - // 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); - } -} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs index 5aece84991..a802b56409 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs +++ b/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); } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModelContributionContext.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModelContributionContext.cs new file mode 100644 index 0000000000..a7bda9890c --- /dev/null +++ b/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)); + } +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs index ce4f13c2ee..703c5a8583 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs +++ b/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? 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)