From d52b2e776c7780919c45bd05249617d0e9f8aaa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 14 Dec 2017 16:00:45 +0300 Subject: [PATCH] Completely revised method validation. --- .../Validation/AbpValidationActionFilter.cs | 21 +- .../Validation/IMvcModelObjectValidator.cs | 9 + .../MvcActionInvocationValidationContext.cs | 33 +++ .../MvcActionInvocationValidator.cs | 59 +---- .../Mvc/Validation/MvcModelObjectValidator.cs | 15 ++ .../Validation/NullDataAnnotationValidator.cs | 19 ++ .../Abp/Validation/DataAnnotationValidator.cs | 48 ++++ .../Abp/Validation/IAbpValidationResult.cs | 12 + .../Validation/IDataAnnotationValidator.cs | 7 + .../Volo/Abp/Validation/IObjectValidator.cs | 9 + .../MethodInvocationValidationContext.cs | 29 +++ .../Validation/MethodInvocationValidator.cs | 241 +----------------- .../MethodInvocationValidatorBase.cs | 113 ++++++++ .../Volo/Abp/Validation/ObjectValidator.cs | 101 ++++++++ .../Abp/Validation/ValidationInterceptor.cs | 28 +- 15 files changed, 435 insertions(+), 309 deletions(-) create mode 100644 src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/IMvcModelObjectValidator.cs create mode 100644 src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidationContext.cs create mode 100644 src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcModelObjectValidator.cs create mode 100644 src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/NullDataAnnotationValidator.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/DataAnnotationValidator.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/IAbpValidationResult.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/IDataAnnotationValidator.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/IObjectValidator.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidationContext.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidatorBase.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/ObjectValidator.cs diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs index 51853466a6..41b1d2471b 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs @@ -1,8 +1,6 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Aspects; using Volo.Abp.DependencyInjection; @@ -10,17 +8,18 @@ namespace Volo.Abp.AspNetCore.Mvc.Validation { public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency { - private readonly IServiceProvider _serviceProvider; + private readonly MvcActionInvocationValidator _validator; - public AbpValidationActionFilter(IServiceProvider serviceProvider) + public AbpValidationActionFilter(MvcActionInvocationValidator validator) { - _serviceProvider = serviceProvider; + _validator = validator; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { //TODO: Configuration to disable validation for controllers..? - if (!context.ActionDescriptor.IsControllerAction() || + + if (!context.ActionDescriptor.IsControllerAction() || !ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType)) { await next(); @@ -29,13 +28,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Validation using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Validation)) { - using (var scope = _serviceProvider.CreateScope()) - { - var validator = scope.ServiceProvider.GetRequiredService(); - validator.Initialize(context); - validator.Validate(); - } - + _validator.Validate(new MvcActionInvocationValidationContext(context)); await next(); } } diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/IMvcModelObjectValidator.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/IMvcModelObjectValidator.cs new file mode 100644 index 0000000000..ec1c777b8d --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/IMvcModelObjectValidator.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Validation; + +namespace Volo.Abp.AspNetCore.Mvc.Validation +{ + public interface IMvcModelObjectValidator : IObjectValidator + { + + } +} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidationContext.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidationContext.cs new file mode 100644 index 0000000000..b5b7740e17 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidationContext.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Volo.Abp.Validation; + +namespace Volo.Abp.AspNetCore.Mvc.Validation +{ + public class MvcActionInvocationValidationContext : MethodInvocationValidationContext + { + public ActionExecutingContext ActionContext { get; } + + public MvcActionInvocationValidationContext(ActionExecutingContext actionContext) + : base(actionContext.ActionDescriptor.GetMethodInfo(), GetParameterValues(actionContext)) + { + ActionContext = actionContext; + } + + private static object[] GetParameterValues(ActionExecutingContext actionContext) + { + var methodInfo = actionContext.ActionDescriptor.GetMethodInfo(); + + var parameters = methodInfo.GetParameters(); + var parameterValues = new object[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + parameterValues[i] = actionContext.ActionArguments.GetOrDefault(parameters[i].Name); + } + + return parameterValues; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs index 04e9ab0373..9d904b1c21 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs @@ -1,72 +1,37 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; using Volo.Abp.Validation; namespace Volo.Abp.AspNetCore.Mvc.Validation { - public class MvcActionInvocationValidator : MethodInvocationValidator + public class MvcActionInvocationValidator : MethodInvocationValidatorBase { - protected ActionExecutingContext ActionContext { get; private set; } - - private bool _isValidatedBefore; - - public MvcActionInvocationValidator(IOptions options) - : base(options) + // ReSharper disable once SuggestBaseTypeForParameter + public MvcActionInvocationValidator(IMvcModelObjectValidator objectValidator) + : base(objectValidator) { } - public void Initialize(ActionExecutingContext actionContext) - { - ActionContext = actionContext; - - SetDataAnnotationAttributeErrors(); - - base.Initialize( - actionContext.ActionDescriptor.GetMethodInfo(), - GetParameterValues(actionContext) - ); - } - - protected override void SetDataAnnotationAttributeErrors(object validatingObject) + public virtual void Validate(MvcActionInvocationValidationContext context) { - SetDataAnnotationAttributeErrors(); + AddModelStateErrors(context); + ValidateInternal(context); } - protected virtual void SetDataAnnotationAttributeErrors() + public virtual void AddModelStateErrors(MvcActionInvocationValidationContext context) { - if (_isValidatedBefore || ActionContext.ModelState.IsValid) + if (context.ActionContext.ModelState.IsValid) { return; } - foreach (var state in ActionContext.ModelState) + foreach (var state in context.ActionContext.ModelState) { foreach (var error in state.Value.Errors) { - ValidationErrors.Add(new ValidationResult(error.ErrorMessage, new[] { state.Key })); + context.Errors.Add(new ValidationResult(error.ErrorMessage, new[] { state.Key })); } } - - _isValidatedBefore = true; - } - - protected virtual object[] GetParameterValues(ActionExecutingContext actionContext) - { - var methodInfo = actionContext.ActionDescriptor.GetMethodInfo(); - - var parameters = methodInfo.GetParameters(); - var parameterValues = new object[parameters.Length]; - - for (var i = 0; i < parameters.Length; i++) - { - parameterValues[i] = actionContext.ActionArguments.GetOrDefault(parameters[i].Name); - } - - return parameterValues; } } } \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcModelObjectValidator.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcModelObjectValidator.cs new file mode 100644 index 0000000000..5b5ed97939 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcModelObjectValidator.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Validation; + +namespace Volo.Abp.AspNetCore.Mvc.Validation +{ + [ExposeServices(typeof(IMvcModelObjectValidator))] + public class MvcModelObjectValidator : ObjectValidator, IMvcModelObjectValidator + { + public MvcModelObjectValidator(IOptions options) + : base(options, NullDataAnnotationValidator.Instance) + { + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/NullDataAnnotationValidator.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/NullDataAnnotationValidator.cs new file mode 100644 index 0000000000..cb1e659a86 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/NullDataAnnotationValidator.cs @@ -0,0 +1,19 @@ +using Volo.Abp.Validation; + +namespace Volo.Abp.AspNetCore.Mvc.Validation +{ + public sealed class NullDataAnnotationValidator : IDataAnnotationValidator + { + public static IDataAnnotationValidator Instance { get; } = new DataAnnotationValidator(); + + private NullDataAnnotationValidator() + { + + } + + public void AddDataAnnotationAttributeErrors(IAbpValidationResult validationResult, object validatingObject) + { + + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/DataAnnotationValidator.cs b/src/Volo.Abp/Volo/Abp/Validation/DataAnnotationValidator.cs new file mode 100644 index 0000000000..0ecd1ae7a2 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/DataAnnotationValidator.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Validation +{ + public class DataAnnotationValidator : IDataAnnotationValidator, ITransientDependency + { + /// + /// Checks all properties for DataAnnotations attributes. + /// + public virtual void AddDataAnnotationAttributeErrors(IAbpValidationResult validationResult, object validatingObject) + { + var properties = TypeDescriptor.GetProperties(validatingObject).Cast(); + foreach (var property in properties) + { + var validationAttributes = property.Attributes.OfType().ToArray(); + if (validationAttributes.IsNullOrEmpty()) + { + continue; + } + + var validationContext = new ValidationContext(validatingObject) + { + DisplayName = property.DisplayName, + MemberName = property.Name + }; + + foreach (var attribute in validationAttributes) + { + var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext); + if (result != null) + { + validationResult.Errors.Add(result); + } + } + } + + if (validatingObject is IValidatableObject) + { + var results = (validatingObject as IValidatableObject).Validate(new ValidationContext(validatingObject)); + validationResult.Errors.AddRange(results); + } + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/IAbpValidationResult.cs b/src/Volo.Abp/Volo/Abp/Validation/IAbpValidationResult.cs new file mode 100644 index 0000000000..56e597e6f1 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/IAbpValidationResult.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.Validation +{ + public interface IAbpValidationResult + { + List Errors { get; } + + List ObjectsToBeNormalized { get; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/IDataAnnotationValidator.cs b/src/Volo.Abp/Volo/Abp/Validation/IDataAnnotationValidator.cs new file mode 100644 index 0000000000..fcf523dd16 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/IDataAnnotationValidator.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Validation +{ + public interface IDataAnnotationValidator + { + void AddDataAnnotationAttributeErrors(IAbpValidationResult validationResult, object validatingObject); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/IObjectValidator.cs b/src/Volo.Abp/Volo/Abp/Validation/IObjectValidator.cs new file mode 100644 index 0000000000..91b27405fd --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/IObjectValidator.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Validation +{ + public interface IObjectValidator + { + void Validate(IAbpValidationResult validationResult, object validatingObject); + + void AddValidatationErrors(IAbpValidationResult validationResult, object validatingObject); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidationContext.cs b/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidationContext.cs new file mode 100644 index 0000000000..5911b3a9c3 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidationContext.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace Volo.Abp.Validation +{ + public class MethodInvocationValidationContext : IAbpValidationResult + { + public MethodInfo Method { get; } + + public object[] ParameterValues { get; } + + public ParameterInfo[] Parameters { get; } + + public List Errors { get; } + + public List ObjectsToBeNormalized { get; } + + public MethodInvocationValidationContext(MethodInfo method, object[] parameterValues) + { + Method = method; + ParameterValues = parameterValues; + Parameters = method.GetParameters(); + + Errors = new List(); + ObjectsToBeNormalized = new List(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidator.cs b/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidator.cs index fa2a887fe6..cadae946dd 100644 --- a/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidator.cs +++ b/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidator.cs @@ -1,250 +1,19 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.Options; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Reflection; - namespace Volo.Abp.Validation { /// /// This class is used to validate a method call (invocation) for method arguments. /// - public class MethodInvocationValidator : ITransientDependency + public class MethodInvocationValidator : MethodInvocationValidatorBase { - private const int MaxRecursiveParameterValidationDepth = 8; - - protected MethodInfo Method { get; private set; } - protected object[] ParameterValues { get; private set; } - protected ParameterInfo[] Parameters { get; private set; } - protected List ValidationErrors { get; } - protected List ObjectsToBeNormalized { get; } - - private readonly AbpValidationOptions _options; - - /// - /// Creates a new instance. - /// - public MethodInvocationValidator(IOptions options) - { - _options = options.Value; - - ValidationErrors = new List(); - ObjectsToBeNormalized = new List(); - } - - /// Method to be validated - /// List of arguments those are used to call the . - public virtual void Initialize(MethodInfo method, object[] parameterValues) + public MethodInvocationValidator(IObjectValidator objectValidator) + : base(objectValidator) { - Check.NotNull(method, nameof(method)); - Check.NotNull(parameterValues, nameof(parameterValues)); - Method = method; - ParameterValues = parameterValues; - Parameters = method.GetParameters(); } - /// - /// Validates the method invocation. - /// - public void Validate() + public virtual void Validate(MethodInvocationValidationContext context) { - CheckInitialized(); - - if (Parameters.IsNullOrEmpty()) - { - return; - } - - if (!Method.IsPublic) - { - return; - } - - if (IsValidationDisabled()) - { - return; - } - - if (Parameters.Length != ParameterValues.Length) - { - throw new Exception("Method parameter count does not match with argument count!"); - } - - if (ValidationErrors.Any() && HasSingleNullArgument()) - { - ThrowValidationError(); - } - - for (var i = 0; i < Parameters.Length; i++) - { - ValidateMethodParameter(Parameters[i], ParameterValues[i]); - } - - if (ValidationErrors.Any()) - { - ThrowValidationError(); - } - - foreach (var objectToBeNormalized in ObjectsToBeNormalized) - { - objectToBeNormalized.Normalize(); - } - } - - protected virtual void CheckInitialized() - { - if (Method == null) - { - throw new AbpException("This object has not been initialized. Call Initialize method first."); - } - } - - protected virtual bool IsValidationDisabled() - { - if (Method.IsDefined(typeof(EnableValidationAttribute), true)) - { - return false; - } - - return ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(Method) != null; - } - - protected virtual bool HasSingleNullArgument() - { - return Parameters.Length == 1 && ParameterValues[0] == null; - } - - protected virtual void ThrowValidationError() - { - throw new AbpValidationException( - "Method arguments are not valid! See ValidationErrors for details.", - ValidationErrors - ); - } - - /// - /// Validates given parameter for given value. - /// - /// Parameter of the method to validate - /// Value to validate - protected virtual void ValidateMethodParameter(ParameterInfo parameterInfo, object parameterValue) - { - if (parameterValue == null) - { - if (!parameterInfo.IsOptional && - !parameterInfo.IsOut && - !TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType, includeEnums: true)) - { - ValidationErrors.Add(new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name })); - } - - return; - } - - ValidateObjectRecursively(parameterValue, 1); - } - - protected virtual void ValidateObjectRecursively(object validatingObject, int currentDepth) - { - if (currentDepth > MaxRecursiveParameterValidationDepth) - { - return; - } - - if (validatingObject == null) - { - return; - } - - SetDataAnnotationAttributeErrors(validatingObject); - - //Validate items of enumerable - if (validatingObject is IEnumerable && !(validatingObject is IQueryable)) - { - foreach (var item in (validatingObject as IEnumerable)) - { - ValidateObjectRecursively(item, currentDepth + 1); - } - } - - //Add list to be normalized later - if (validatingObject is IShouldNormalize) - { - ObjectsToBeNormalized.Add(validatingObject as IShouldNormalize); - } - - //Do not recursively validate for enumerable objects - if (validatingObject is IEnumerable) - { - return; - } - - var validatingObjectType = validatingObject.GetType(); - - //Do not recursively validate for primitive objects - if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType)) - { - return; - } - - if (_options.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject))) - { - return; - } - - var properties = TypeDescriptor.GetProperties(validatingObject).Cast(); - foreach (var property in properties) - { - if (property.Attributes.OfType().Any()) - { - continue; - } - - ValidateObjectRecursively(property.GetValue(validatingObject), currentDepth + 1); - } - } - - /// - /// Checks all properties for DataAnnotations attributes. - /// - protected virtual void SetDataAnnotationAttributeErrors(object validatingObject) - { - var properties = TypeDescriptor.GetProperties(validatingObject).Cast(); - foreach (var property in properties) - { - var validationAttributes = property.Attributes.OfType().ToArray(); - if (validationAttributes.IsNullOrEmpty()) - { - continue; - } - - var validationContext = new ValidationContext(validatingObject) - { - DisplayName = property.DisplayName, - MemberName = property.Name - }; - - foreach (var attribute in validationAttributes) - { - var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext); - if (result != null) - { - ValidationErrors.Add(result); - } - } - } - - if (validatingObject is IValidatableObject) - { - var results = (validatingObject as IValidatableObject).Validate(new ValidationContext(validatingObject)); - ValidationErrors.AddRange(results); - } + ValidateInternal(context); } } } diff --git a/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidatorBase.cs b/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidatorBase.cs new file mode 100644 index 0000000000..3be13600e4 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidatorBase.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Validation +{ + public abstract class MethodInvocationValidatorBase : ITransientDependency + { + private readonly IObjectValidator _objectValidator; + + protected MethodInvocationValidatorBase(IObjectValidator objectValidator) + { + _objectValidator = objectValidator; + } + + /// + /// Validates the method invocation. + /// + protected virtual void ValidateInternal(MethodInvocationValidationContext context) + { + Check.NotNull(context, nameof(context)); + + if (context.Parameters.IsNullOrEmpty()) + { + return; + } + + if (!context.Method.IsPublic) + { + return; + } + + if (IsValidationDisabled(context)) + { + return; + } + + if (context.Parameters.Length != context.ParameterValues.Length) + { + throw new Exception("Method parameter count does not match with argument count!"); + } + + if (context.Errors.Any() && HasSingleNullArgument(context)) + { + ThrowValidationError(context); + } + + AddMethodParameterValidationErrors(context); + + if (context.Errors.Any()) + { + ThrowValidationError(context); + } + + foreach (var objectToBeNormalized in context.ObjectsToBeNormalized) + { + objectToBeNormalized.Normalize(); + } + } + + protected virtual bool IsValidationDisabled(MethodInvocationValidationContext context) + { + if (context.Method.IsDefined(typeof(EnableValidationAttribute), true)) + { + return false; + } + + return ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(context.Method) != null; + } + + protected virtual bool HasSingleNullArgument(MethodInvocationValidationContext context) + { + return context.Parameters.Length == 1 && context.ParameterValues[0] == null; + } + + protected virtual void ThrowValidationError(MethodInvocationValidationContext context) + { + throw new AbpValidationException( + "Method arguments are not valid! See ValidationErrors for details.", + context.Errors + ); + } + + protected virtual void AddMethodParameterValidationErrors(MethodInvocationValidationContext context) + { + for (var i = 0; i < context.Parameters.Length; i++) + { + AddMethodParameterValidationErrors(context, context.Parameters[i], context.ParameterValues[i]); + } + } + + protected virtual void AddMethodParameterValidationErrors(IAbpValidationResult context, ParameterInfo parameterInfo, object parameterValue) + { + if (parameterValue == null) + { + if (!parameterInfo.IsOptional && + !parameterInfo.IsOut && + !TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType, includeEnums: true)) + { + context.Errors.Add(new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name })); + } + + return; + } + + _objectValidator.AddValidatationErrors(context, parameterValue); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/ObjectValidator.cs b/src/Volo.Abp/Volo/Abp/Validation/ObjectValidator.cs new file mode 100644 index 0000000000..7ebabdf58d --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/ObjectValidator.cs @@ -0,0 +1,101 @@ +using System.Collections; +using System.ComponentModel; +using System.Linq; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Validation +{ + public class ObjectValidator : ITransientDependency, IObjectValidator + { + private const int MaxRecursiveParameterValidationDepth = 8; + + private readonly AbpValidationOptions _options; + private readonly IDataAnnotationValidator _dataAnnotationValidator; + + public ObjectValidator(IOptions options, IDataAnnotationValidator dataAnnotationValidator) + { + _dataAnnotationValidator = dataAnnotationValidator; + _options = options.Value; + } + + public virtual void Validate(IAbpValidationResult validationResult, object validatingObject) + { + AddValidatationErrors(validationResult, validatingObject); + + if (validationResult.Errors.Any()) + { + throw new AbpValidationException( + "Method arguments are not valid! See ValidationErrors for details.", + validationResult.Errors + ); + } + } + + public virtual void AddValidatationErrors(IAbpValidationResult validationResult, object validatingObject) + { + ValidateObjectRecursively(validationResult, validatingObject, 1); + } + + protected virtual void ValidateObjectRecursively(IAbpValidationResult context, object validatingObject, int currentDepth) + { + if (currentDepth > MaxRecursiveParameterValidationDepth) + { + return; + } + + if (validatingObject == null) + { + return; + } + + _dataAnnotationValidator.AddDataAnnotationAttributeErrors(context, validatingObject); + + //Validate items of enumerable + if (validatingObject is IEnumerable && !(validatingObject is IQueryable)) + { + foreach (var item in (validatingObject as IEnumerable)) + { + ValidateObjectRecursively(context, item, currentDepth + 1); + } + } + + //Add list to be normalized later + if (validatingObject is IShouldNormalize) + { + context.ObjectsToBeNormalized.Add(validatingObject as IShouldNormalize); + } + + //Do not recursively validate for enumerable objects + if (validatingObject is IEnumerable) + { + return; + } + + var validatingObjectType = validatingObject.GetType(); + + //Do not recursively validate for primitive objects + if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType)) + { + return; + } + + if (_options.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject))) + { + return; + } + + var properties = TypeDescriptor.GetProperties(validatingObject).Cast(); + foreach (var property in properties) + { + if (property.Attributes.OfType().Any()) + { + continue; + } + + ValidateObjectRecursively(context, property.GetValue(validatingObject), currentDepth + 1); + } + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs b/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs index bb81a02bba..d09e5bccd8 100644 --- a/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs +++ b/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs @@ -1,6 +1,4 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Aspects; +using Volo.Abp.Aspects; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; @@ -8,11 +6,11 @@ namespace Volo.Abp.Validation { public class ValidationInterceptor : AbpInterceptor, ITransientDependency { - private readonly IServiceProvider _serviceProvider; + private readonly MethodInvocationValidator _validator; - public ValidationInterceptor(IServiceProvider serviceProvider) + public ValidationInterceptor(MethodInvocationValidator validator) { - _serviceProvider = serviceProvider; + _validator = validator; } public override void Intercept(IAbpMethodInvocation invocation) @@ -23,14 +21,20 @@ namespace Volo.Abp.Validation return; } - using (var scope = _serviceProvider.CreateScope()) - { - var validator = scope.ServiceProvider.GetRequiredService(); - validator.Initialize(invocation.Method, invocation.Arguments); - validator.Validate(); - } + Validate(invocation); invocation.Proceed(); } + + protected virtual void Validate(IAbpMethodInvocation invocation) + { + _validator + .Validate( + new MethodInvocationValidationContext( + invocation.Method, + invocation.Arguments + ) + ); + } } }