diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index 263178e0ed..e3cee4e72b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -16,6 +16,7 @@ using System.Net; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; @@ -25,6 +26,7 @@ using Microsoft.Extensions.Localization; using Volo.Abp.ApiVersioning; using Volo.Abp.AspNetCore.Mvc.ApiExploring; using Volo.Abp.AspNetCore.Mvc.Conventions; +using Volo.Abp.AspNetCore.Mvc.DataAnnotations; using Volo.Abp.AspNetCore.Mvc.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Json; using Volo.Abp.AspNetCore.Mvc.Localization; @@ -164,6 +166,9 @@ namespace Volo.Abp.AspNetCore.Mvc partManager.FeatureProviders.Add(new AbpConventionalControllerFeatureProvider(application)); partManager.ApplicationParts.AddIfNotContains(typeof(AbpAspNetCoreMvcModule).Assembly); + context.Services.Replace(ServiceDescriptor.Singleton()); + context.Services.AddSingleton(); + Configure(mvcOptions => { mvcOptions.AddAbp(context.Services); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 4aa183a43d..59166d3e1d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -5,7 +5,6 @@ using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.Features; using Volo.Abp.AspNetCore.Mvc.ModelBinding; -using Volo.Abp.AspNetCore.Mvc.ModelBinding.Metadata; using Volo.Abp.AspNetCore.Mvc.Response; using Volo.Abp.AspNetCore.Mvc.Uow; using Volo.Abp.AspNetCore.Mvc.Validation; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/AbpValidationAttributeAdapterProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/AbpValidationAttributeAdapterProvider.cs new file mode 100644 index 0000000000..eaceddba70 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/AbpValidationAttributeAdapterProvider.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.DataAnnotations; +using Microsoft.Extensions.Localization; +using Volo.Abp.Validation; + +namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations +{ + public class AbpValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider + { + private readonly ValidationAttributeAdapterProvider _defaultAdapter; + + public AbpValidationAttributeAdapterProvider(ValidationAttributeAdapterProvider defaultAdapter) + { + _defaultAdapter = defaultAdapter; + } + + public virtual IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer) + { + var type = attribute.GetType(); + + if (type == typeof(DynamicStringLengthAttribute)) + { + return new DynamicStringLengthAttributeAdapter((DynamicStringLengthAttribute) attribute, stringLocalizer); + } + + return _defaultAdapter.GetAttributeAdapter(attribute, stringLocalizer); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicStringLengthAttributeAdapter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicStringLengthAttributeAdapter.cs new file mode 100644 index 0000000000..d4c46b85f3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicStringLengthAttributeAdapter.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; +using Microsoft.AspNetCore.Mvc.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.Localization; +using Volo.Abp.Validation; + +namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations +{ + public class DynamicStringLengthAttributeAdapter : AttributeAdapterBase + { + private readonly string _max; + private readonly string _min; + + public DynamicStringLengthAttributeAdapter( + DynamicStringLengthAttribute attribute, + IStringLocalizer stringLocalizer) + : base(attribute, stringLocalizer) + { + _max = Attribute.MaximumLength.ToString(CultureInfo.InvariantCulture); + _min = Attribute.MinimumLength.ToString(CultureInfo.InvariantCulture); + } + + public override void AddValidation(ClientModelValidationContext context) + { + Check.NotNull(context, nameof(context)); + + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-length", GetErrorMessage(context)); + + if (Attribute.MaximumLength != int.MaxValue) + { + MergeAttribute(context.Attributes, "data-val-length-max", _max); + } + + if (Attribute.MinimumLength != 0) + { + MergeAttribute(context.Attributes, "data-val-length-min", _min); + } + } + + public override string GetErrorMessage(ModelValidationContextBase validationContext) + { + Check.NotNull(validationContext, nameof(validationContext)); + + return GetErrorMessage( + validationContext.ModelMetadata, + validationContext.ModelMetadata.GetDisplayName(), + Attribute.MaximumLength, + Attribute.MinimumLength + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Validation/DynamicStringLengthAttribute.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Validation/DynamicStringLengthAttribute.cs new file mode 100644 index 0000000000..d27a04b1ba --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Validation/DynamicStringLengthAttribute.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Reflection; +using JetBrains.Annotations; + +namespace Volo.Abp.Validation +{ + /// + /// Used to determine and + /// properties on the runtime. + /// + public class DynamicStringLengthAttribute : StringLengthAttribute + { + private static readonly FieldInfo MaximumLengthField; + + static DynamicStringLengthAttribute() + { + MaximumLengthField = typeof(StringLengthAttribute).GetField( + "k__BackingField", + BindingFlags.Instance | BindingFlags.NonPublic + ); + Debug.Assert(MaximumLengthField != null, nameof(MaximumLengthField) + " != null"); + } + + /// A type to get the values of the properties + /// The name of the public static property for the + /// The name of the public static property for the + public DynamicStringLengthAttribute( + [NotNull] Type sourceType, + [CanBeNull] string maximumLengthPropertyName, + [CanBeNull] string minimumLengthPropertyName = null) + : base(0) + { + Check.NotNull(sourceType, nameof(sourceType)); + + if (maximumLengthPropertyName != null) + { + var maximumLengthProperty = sourceType.GetProperty( + maximumLengthPropertyName, + BindingFlags.Static | BindingFlags.Public + ); + Debug.Assert(maximumLengthProperty != null, nameof(maximumLengthProperty) + " != null"); + MaximumLengthField.SetValue(this, (int) maximumLengthProperty.GetValue(null)); + } + + if (minimumLengthPropertyName != null) + { + var minimumLengthProperty = sourceType.GetProperty( + minimumLengthPropertyName, + BindingFlags.Static | BindingFlags.Public + ); + Debug.Assert(minimumLengthProperty != null, nameof(minimumLengthProperty) + " != null"); + MinimumLength = (int) minimumLengthProperty.GetValue(null); + } + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController.cs index 57cb40219b..ae622acee2 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Shouldly; +using Volo.Abp.Validation; namespace Volo.Abp.AspNetCore.Mvc.Validation { @@ -31,6 +32,14 @@ namespace Volo.Abp.AspNetCore.Mvc.Validation { return Content("ModelState.IsValid: " + ModelState.IsValid.ToString().ToLowerInvariant()); } + + [HttpGet] + [Route("object-result-action-dynamic-length")] + public Task ObjectResultActionDynamicLength(ValidationDynamicTestModel model) + { + ModelState.IsValid.ShouldBeTrue(); //AbpValidationFilter throws exception otherwise + return Task.FromResult(model.Value1); + } public class ValidationTest1Model { @@ -38,6 +47,18 @@ namespace Volo.Abp.AspNetCore.Mvc.Validation [StringLength(5, MinimumLength = 2)] public string Value1 { get; set; } } + + public class ValidationDynamicTestModel + { + [DynamicStringLength(typeof(Consts), nameof(Consts.MaxValue2Length), nameof(Consts.MinValue2Length))] + public string Value1 { get; set; } + + public static class Consts + { + public static int MinValue2Length { get; set; } = 2; + public static int MaxValue2Length { get; set; } = 7; + } + } public class CustomValidateModel : IValidatableObject { @@ -51,6 +72,5 @@ namespace Volo.Abp.AspNetCore.Mvc.Validation } } } - } -} +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs index 99e027f85a..767ff7aa3e 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs @@ -58,5 +58,21 @@ namespace Volo.Abp.AspNetCore.Mvc.Validation result.Error.ValidationErrors.ShouldContain(x => x.Message == "Value1 should be hello"); } + [Fact] + public async Task Should_Validate_Dynamic_Length_Object_Result_Success() + { + var result = await GetResponseAsStringAsync("/api/validation-test/object-result-action-dynamic-length?value1=hello"); + result.ShouldBe("hello"); + } + + [Fact] + public async Task Should_Validate_Dynamic_Length_Object_Result_Failing() + { + var result = await GetResponseAsObjectAsync("/api/validation-test/object-result-action-dynamic-length?value1=a", HttpStatusCode.BadRequest); //value1 has min length of 2 chars. + result.Error.ValidationErrors.Length.ShouldBeGreaterThan(0); + + result = await GetResponseAsObjectAsync("/api/validation-test/object-result-action-dynamic-length?value1=12345678", HttpStatusCode.BadRequest); //value1 has max length of 7 chars. + result.Error.ValidationErrors.Length.ShouldBeGreaterThan(0); + } } }