diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index eb04229f94..10be5a757d 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -1,7 +1,7 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2036 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.156 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}" EndProject @@ -221,6 +221,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Authent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Cli", "src\Volo.Abp.Cli\Volo.Abp.Cli.csproj", "{69168816-4394-4DDA-BB6B-C21983D37F0B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.FluentValidation", "src\Volo.Abp.FluentValidation\Volo.Abp.FluentValidation.csproj", "{43D5FE61-ECBF-4B16-AD95-0043E18EB93A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.FluentValidation.Tests", "test\Volo.Abp.FluentValidation.Tests\Volo.Abp.FluentValidation.Tests.csproj", "{E9E1714F-7ED2-4BD1-BA4A-BA06E398288A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -651,6 +655,14 @@ Global {46C6336C-A1D8-4858-98CE-6F4C698C5A77}.Debug|Any CPU.Build.0 = Debug|Any CPU {46C6336C-A1D8-4858-98CE-6F4C698C5A77}.Release|Any CPU.ActiveCfg = Release|Any CPU {46C6336C-A1D8-4858-98CE-6F4C698C5A77}.Release|Any CPU.Build.0 = Release|Any CPU + {43D5FE61-ECBF-4B16-AD95-0043E18EB93A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43D5FE61-ECBF-4B16-AD95-0043E18EB93A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43D5FE61-ECBF-4B16-AD95-0043E18EB93A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43D5FE61-ECBF-4B16-AD95-0043E18EB93A}.Release|Any CPU.Build.0 = Release|Any CPU + {E9E1714F-7ED2-4BD1-BA4A-BA06E398288A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9E1714F-7ED2-4BD1-BA4A-BA06E398288A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9E1714F-7ED2-4BD1-BA4A-BA06E398288A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9E1714F-7ED2-4BD1-BA4A-BA06E398288A}.Release|Any CPU.Build.0 = Release|Any CPU {69168816-4394-4DDA-BB6B-C21983D37F0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {69168816-4394-4DDA-BB6B-C21983D37F0B}.Debug|Any CPU.Build.0 = Debug|Any CPU {69168816-4394-4DDA-BB6B-C21983D37F0B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -766,6 +778,8 @@ Global {575BEFA1-19C2-49B1-8D31-B5D4472328DE} = {447C8A77-E5F0-4538-8687-7383196D04EA} {6C161F55-54B6-42A5-B177-3B0ED50323C1} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {46C6336C-A1D8-4858-98CE-6F4C698C5A77} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {43D5FE61-ECBF-4B16-AD95-0043E18EB93A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {E9E1714F-7ED2-4BD1-BA4A-BA06E398288A} = {447C8A77-E5F0-4538-8687-7383196D04EA} {69168816-4394-4DDA-BB6B-C21983D37F0B} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj b/framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj new file mode 100644 index 0000000000..12084177a9 --- /dev/null +++ b/framework/src/Volo.Abp.FluentValidation/Volo.Abp.FluentValidation.csproj @@ -0,0 +1,24 @@ + + + + + + netstandard2.0 + Volo.Abp.FluentValidation + Volo.Abp.FluentValidation + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationConventionalRegistrar.cs b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationConventionalRegistrar.cs new file mode 100644 index 0000000000..e16450c213 --- /dev/null +++ b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationConventionalRegistrar.cs @@ -0,0 +1,41 @@ +using System; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.FluentValidation +{ + public class AbpFluentValidationConventionalRegistrar : DefaultConventionalRegistrar + { + public override void AddType(IServiceCollection services, Type type) + { + if (typeof(IValidator).IsAssignableFrom(type)) + { + var dtoType = GetFirstGenericArgumentOrNull(type, 1); + if (dtoType != null) + { + var serverType = typeof(IValidator<>).MakeGenericType(dtoType); + var serviceDescriptor = ServiceDescriptor.Describe(serverType, type, ServiceLifetime.Transient); + + services.Add(serviceDescriptor); + } + } + } + + private static Type GetFirstGenericArgumentOrNull(Type type, int depth) + { + const int maxFindDepth = 8; + + if (depth >= maxFindDepth) + { + return null; + } + if (type.IsGenericType && type.GetGenericArguments().Length >= 1) + { + return type.GetGenericArguments()[0]; + } + + return GetFirstGenericArgumentOrNull(type.BaseType, depth + 1); + } + } +} diff --git a/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs new file mode 100644 index 0000000000..e48404a36d --- /dev/null +++ b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; +using Volo.Abp.Validation; + +namespace Volo.Abp.FluentValidation +{ + [DependsOn(typeof(AbpValidationModule))] + public class AbpFluentValidationModule : AbpModule + { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddConventionalRegistrar(new AbpFluentValidationConventionalRegistrar()); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.ValidationContributor.Add(); + }); + } + } +} diff --git a/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentMethodInvocationValidator.cs b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentMethodInvocationValidator.cs new file mode 100644 index 0000000000..4749802cb1 --- /dev/null +++ b/framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentMethodInvocationValidator.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using FluentValidation; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Validation; + +namespace Volo.Abp.FluentValidation +{ + public class FluentMethodInvocationValidator : IMethodInvocationValidator, ITransientDependency + { + private readonly IServiceProvider _serviceProvider; + + public FluentMethodInvocationValidator(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public void Validate(MethodInvocationValidationContext context) + { + var validationResult = new AbpValidationResult(); + + foreach (var parameterValue in context.ParameterValues) + { + var serverType = typeof(IValidator<>).MakeGenericType(parameterValue.GetType()); + + if (_serviceProvider.GetService(serverType) is IValidator validator) + { + var result = validator.Validate(parameterValue); + if (!result.IsValid) + { + validationResult.Errors.AddRange(result.Errors.Select(error => + new ValidationResult(error.ErrorMessage))); + } + } + } + + if (validationResult.Errors.Any()) + { + throw new AbpValidationException( + "Method arguments are not valid! See ValidationErrors for details.", + context.Errors + ); + } + } + } +} diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationModule.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationModule.cs index 03efe9d66a..39041e044c 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationModule.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationModule.cs @@ -9,5 +9,13 @@ namespace Volo.Abp.Validation { context.Services.OnRegistred(ValidationInterceptorRegistrar.RegisterIfNeeded); } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.ValidationContributor.Add(); + }); + } } } diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IValidationConfiguration.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationOptions.cs similarity index 58% rename from framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IValidationConfiguration.cs rename to framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationOptions.cs index 7040be55bc..69737e5fb6 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IValidationConfiguration.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationOptions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Volo.Abp.Collections; namespace Volo.Abp.Validation { @@ -7,9 +8,12 @@ namespace Volo.Abp.Validation { public List IgnoredTypes { get; } + public ITypeList ValidationContributor { get; set; } + public AbpValidationOptions() { IgnoredTypes = new List(); + ValidationContributor = new TypeList(); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IMethodInvocationValidator.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IMethodInvocationValidator.cs index 218ad9313d..fd413bc498 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IMethodInvocationValidator.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IMethodInvocationValidator.cs @@ -1,7 +1,7 @@ -namespace Volo.Abp.Validation +namespace Volo.Abp.Validation { public interface IMethodInvocationValidator { void Validate(MethodInvocationValidationContext context); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptor.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptor.cs index 86d58e89a3..f15554d1c7 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptor.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptor.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Volo.Abp.Aspects; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; @@ -7,11 +10,13 @@ namespace Volo.Abp.Validation { public class ValidationInterceptor : AbpInterceptor, ITransientDependency { - private readonly IMethodInvocationValidator _validator; + private readonly AbpValidationOptions _abpValidationOptions; + private readonly IServiceProvider _serviceProvider; - public ValidationInterceptor(IMethodInvocationValidator validator) + public ValidationInterceptor(IServiceProvider serviceProvider, IOptions abpValidationOptions) { - _validator = validator; + _serviceProvider = serviceProvider; + _abpValidationOptions = abpValidationOptions.Value; } public override void Intercept(IAbpMethodInvocation invocation) @@ -42,13 +47,18 @@ namespace Volo.Abp.Validation protected virtual void Validate(IAbpMethodInvocation invocation) { - _validator.Validate( - new MethodInvocationValidationContext( - invocation.TargetObject, - invocation.Method, - invocation.Arguments - ) - ); + foreach (var validationContributor in _abpValidationOptions.ValidationContributor) + { + var validator = (IMethodInvocationValidator) _serviceProvider.GetRequiredService(validationContributor); + + validator.Validate( + new MethodInvocationValidationContext( + invocation.TargetObject, + invocation.Method, + invocation.Arguments + ) + ); + } } } } diff --git a/framework/test/Volo.Abp.FluentValidation.Tests/Volo.Abp.FluentValidation.Tests.csproj b/framework/test/Volo.Abp.FluentValidation.Tests/Volo.Abp.FluentValidation.Tests.csproj new file mode 100644 index 0000000000..ab2f82eefa --- /dev/null +++ b/framework/test/Volo.Abp.FluentValidation.Tests/Volo.Abp.FluentValidation.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.2 + Volo.Abp.FluentValidation.Tests + Volo.Abp.FluentValidation.Tests + true + false + false + false + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.FluentValidation.Tests/Volo/Abp/FluentValidation/ApplicationService_FluentValidation_Tests.cs b/framework/test/Volo.Abp.FluentValidation.Tests/Volo/Abp/FluentValidation/ApplicationService_FluentValidation_Tests.cs new file mode 100644 index 0000000000..467a45a999 --- /dev/null +++ b/framework/test/Volo.Abp.FluentValidation.Tests/Volo/Abp/FluentValidation/ApplicationService_FluentValidation_Tests.cs @@ -0,0 +1,206 @@ +using System.Threading.Tasks; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Autofac; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Modularity; +using Volo.Abp.Validation; +using Xunit; + +namespace Volo.Abp.FluentValidation +{ + public class ApplicationService_FluentValidation_Tests : AbpIntegratedTest + { + private readonly IMyAppService _myAppService; + + public ApplicationService_FluentValidation_Tests() + { + _myAppService = ServiceProvider.GetRequiredService(); + } + + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + [Fact] + public async Task Should_Work_Proper_With_Right_Inputs() + { + // MyStringValue should be aaa, MyStringValue2 should be bbb. MyStringValue3 should be ccc + var output = _myAppService.MyMethod(new MyMethodInput + { + MyStringValue = "aaa", + MyMethodInput2 = new MyMethodInput2 + { + MyStringValue2 = "bbb" + }, + MyMethodInput3 = new MyMethodInput3 + { + MyStringValue3 = "ccc" + } + }); + output.ShouldBe("aaabbbccc"); + + var asyncOutput = await _myAppService.MyMethodAsync(new MyMethodInput + { + MyStringValue = "aaa", + MyMethodInput2 = new MyMethodInput2 + { + MyStringValue2 = "bbb" + }, + MyMethodInput3 = new MyMethodInput3 + { + MyStringValue3 = "ccc" + } + }); + + asyncOutput.ShouldBe("aaabbbccc"); + } + + [Fact] + public async Task Should_Not_Work_With_Wrong_Inputs() + { + // MyStringValue should be aaa, MyStringValue2 should be bbb. MyStringValue3 should be ccc + + Assert.Throws(() => _myAppService.MyMethod(new MyMethodInput + { + MyStringValue = "a", + MyMethodInput2 = new MyMethodInput2 + { + MyStringValue2 = "b" + }, + MyMethodInput3 = new MyMethodInput3 + { + MyStringValue3 = "c" + } + })); + + await Assert.ThrowsAsync(async () => await _myAppService.MyMethodAsync( + new MyMethodInput + { + MyStringValue = "a", + MyMethodInput2 = new MyMethodInput2 + { + MyStringValue2 = "b" + }, + MyMethodInput3 = new MyMethodInput3 + { + MyStringValue3 = "c" + } + })); + } + + [Fact] + public void NotValidateMyMethod_Test() + { + var output = _myAppService.NotValidateMyMethod(new MyMethodInput4 + { + MyStringValue4 = "444" + }); + + output.ShouldBe("444"); + } + + + [DependsOn(typeof(AbpAutofacModule))] + [DependsOn(typeof(AbpFluentValidationModule))] + public class TestModule : AbpModule + { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.OnRegistred(onServiceRegistredContext => + { + if (typeof(IMyAppService).IsAssignableFrom(onServiceRegistredContext.ImplementationType)) + { + onServiceRegistredContext.Interceptors.TryAdd(); + } + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddType(); + } + } + + public interface IMyAppService + { + string MyMethod(MyMethodInput input); + + Task MyMethodAsync(MyMethodInput input); + + string NotValidateMyMethod(MyMethodInput4 input); + } + + public class MyAppService : IMyAppService, ITransientDependency + { + public string MyMethod(MyMethodInput input) + { + return input.MyStringValue + input.MyMethodInput2.MyStringValue2 + input.MyMethodInput3.MyStringValue3; + } + + public Task MyMethodAsync(MyMethodInput input) + { + return Task.FromResult(input.MyStringValue + input.MyMethodInput2.MyStringValue2 + + input.MyMethodInput3.MyStringValue3); + } + + public string NotValidateMyMethod(MyMethodInput4 input) + { + return input.MyStringValue4; + } + } + + public class MyMethodInput + { + public string MyStringValue { get; set; } + + public MyMethodInput2 MyMethodInput2 { get; set; } + + public MyMethodInput3 MyMethodInput3 { get; set; } + } + + public class MyMethodInput2 + { + public string MyStringValue2 { get; set; } + } + + public class MyMethodInput3 + { + + public string MyStringValue3 { get; set; } + } + + public class MyMethodInput4 + { + public string MyStringValue4 { get; set; } + } + + public class MyMethodInputValidator : AbstractValidator + { + public MyMethodInputValidator() + { + RuleFor(x => x.MyStringValue).Equal("aaa"); + RuleFor(x => x.MyMethodInput2.MyStringValue2).Equal("bbb"); + RuleFor(customer => customer.MyMethodInput3).SetValidator(new MyMethodInput3Validator()); + } + } + + public class MethodInputBaseValidator : AbstractValidator + { + public MethodInputBaseValidator() + { + RuleFor(x => x.MyStringValue3).NotNull(); + } + } + + public class MyMethodInput3Validator : MethodInputBaseValidator + { + public MyMethodInput3Validator() + { + RuleFor(x => x.MyStringValue3).Equal("ccc"); + } + } + } +} \ No newline at end of file