Browse Source

Implement fluent validation as object validation contributor.

pull/1030/head
Halil ibrahim Kalkan 7 years ago
parent
commit
7d74829f03
  1. 8
      framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/AbpFluentValidationModule.cs
  2. 55
      framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentMethodInvocationValidator.cs
  3. 41
      framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentObjectValidationContributor.cs
  4. 21
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationModule.cs
  5. 4
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationOptions.cs
  6. 122
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs
  7. 71
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationValidator.cs
  8. 12
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IDataAnnotationValidator.cs
  9. 7
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IObjectValidationContributor.cs
  10. 6
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IObjectValidator.cs
  11. 20
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidationContext.cs
  12. 92
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidator.cs
  13. 33
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptor.cs
  14. 3
      framework/test/Volo.Abp.FluentValidation.Tests/Volo/Abp/FluentValidation/ApplicationService_FluentValidation_Tests.cs
  15. 2
      framework/test/Volo.Abp.Validation.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs

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

@ -13,13 +13,5 @@ namespace Volo.Abp.FluentValidation
{
context.Services.AddConventionalRegistrar(new AbpFluentValidationConventionalRegistrar());
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpValidationOptions>(options =>
{
options.MethodValidationContributors.Add<FluentMethodInvocationValidator>();
});
}
}
}

55
framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentMethodInvocationValidator.cs

@ -1,55 +0,0 @@
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 serviceType = typeof(IValidator<>).MakeGenericType(parameterValue.GetType());
var validator = _serviceProvider.GetService(serviceType) as IValidator;
if (validator == null)
{
continue;
}
var result = validator.Validate(parameterValue);
if (!result.IsValid)
{
validationResult.Errors.AddRange(
result.Errors.Select(
error =>
new ValidationResult(error.ErrorMessage)
)
);
}
}
if (validationResult.Errors.Any())
{
//TODO: How to localize messages?
throw new AbpValidationException(
"Method arguments are not valid! See ValidationErrors for details.",
context.Errors
);
}
}
}
}

41
framework/src/Volo.Abp.FluentValidation/Volo/Abp/FluentValidation/FluentObjectValidationContributor.cs

@ -0,0 +1,41 @@
using FluentValidation;
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Validation;
namespace Volo.Abp.FluentValidation
{
public class FluentObjectValidationContributor : IObjectValidationContributor, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
public FluentObjectValidationContributor(
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void AddErrors(ObjectValidationContext context)
{
var serviceType = typeof(IValidator<>).MakeGenericType(context.ValidatingObject.GetType());
var validator = _serviceProvider.GetService(serviceType) as IValidator;
if (validator == null)
{
return;
}
var result = validator.Validate(context.ValidatingObject);
if (!result.IsValid)
{
context.Errors.AddRange(
result.Errors.Select(
error =>
new ValidationResult(error.ErrorMessage)
)
);
}
}
}
}

21
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationModule.cs

@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
namespace Volo.Abp.Validation
@ -8,13 +10,24 @@ namespace Volo.Abp.Validation
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(ValidationInterceptorRegistrar.RegisterIfNeeded);
AutoAddObjectValidationContributors(context.Services);
}
public override void ConfigureServices(ServiceConfigurationContext context)
private static void AutoAddObjectValidationContributors(IServiceCollection services)
{
Configure<AbpValidationOptions>(options =>
var contributorTypes = new List<Type>();
services.OnRegistred(context =>
{
if (typeof(IObjectValidationContributor).IsAssignableFrom(context.ImplementationType))
{
contributorTypes.Add(context.ImplementationType);
}
});
services.Configure<AbpValidationOptions>(options =>
{
options.MethodValidationContributors.Add<MethodInvocationValidator>();
options.ObjectValidationContributors.AddIfNotContains(contributorTypes);
});
}
}

4
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationOptions.cs

@ -8,12 +8,12 @@ namespace Volo.Abp.Validation
{
public List<Type> IgnoredTypes { get; }
public ITypeList<IMethodInvocationValidator> MethodValidationContributors { get; set; }
public ITypeList<IObjectValidationContributor> ObjectValidationContributors { get; set; }
public AbpValidationOptions()
{
IgnoredTypes = new List<Type>();
MethodValidationContributors = new TypeList<IMethodInvocationValidator>();
ObjectValidationContributors = new TypeList<IObjectValidationContributor>();
}
}
}

122
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs

@ -0,0 +1,122 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Reflection;
namespace Volo.Abp.Validation
{
public class DataAnnotationObjectValidationContributor : IObjectValidationContributor, ITransientDependency
{
public const int MaxRecursiveParameterValidationDepth = 8;
protected AbpValidationOptions Options { get; }
public DataAnnotationObjectValidationContributor(IOptions<AbpValidationOptions> options)
{
Options = options.Value;
}
public void AddErrors(ObjectValidationContext context)
{
ValidateObjectRecursively(context.Errors, context.ValidatingObject, currentDepth: 1);
}
protected virtual void ValidateObjectRecursively(List<ValidationResult> errors, object validatingObject, int currentDepth)
{
if (currentDepth > MaxRecursiveParameterValidationDepth)
{
return;
}
if (validatingObject == null)
{
return;
}
AddErrors(errors, validatingObject);
//Validate items of enumerable
if (validatingObject is IEnumerable)
{
if (!(validatingObject is IQueryable))
{
foreach (var item in (validatingObject as IEnumerable))
{
ValidateObjectRecursively(errors, item, currentDepth + 1);
}
}
return;
}
var validatingObjectType = validatingObject.GetType();
//Do not recursively validate for primitive objects
if (TypeHelper.IsPrimitiveExtended(validatingObjectType))
{
return;
}
if (Options.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject)))
{
return;
}
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
if (property.Attributes.OfType<DisableValidationAttribute>().Any())
{
continue;
}
ValidateObjectRecursively(errors, property.GetValue(validatingObject), currentDepth + 1);
}
}
public void AddErrors(List<ValidationResult> errors, object validatingObject)
{
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
AddPropertyErrors(validatingObject, property, errors);
}
if (validatingObject is IValidatableObject validatableObject)
{
errors.AddRange(
validatableObject.Validate(new ValidationContext(validatableObject))
);
}
}
protected virtual void AddPropertyErrors(object validatingObject, PropertyDescriptor property, List<ValidationResult> errors)
{
var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();
if (validationAttributes.IsNullOrEmpty())
{
return;
}
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)
{
errors.Add(result);
}
}
}
}
}

71
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationValidator.cs

@ -1,71 +0,0 @@
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
{
public void Validate(object validatingObject)
{
var errors = GetErrors(validatingObject);
if (errors.Any())
{
throw new AbpValidationException(
"Object state is not valid! See ValidationErrors for details.",
errors
);
}
}
/// <summary>
/// Gets all errors from properties for DataAnnotations attributes and IValidatableObject interface.
/// </summary>
public virtual List<ValidationResult> GetErrors(object validatingObject)
{
var errors = new List<ValidationResult>();
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
AddPropertyErrors(validatingObject, property, errors);
}
if (validatingObject is IValidatableObject validatableObject)
{
errors.AddRange(
validatableObject.Validate(new ValidationContext(validatableObject))
);
}
return errors;
}
protected virtual void AddPropertyErrors(object validatingObject, PropertyDescriptor property, List<ValidationResult> errors)
{
var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();
if (validationAttributes.IsNullOrEmpty())
{
return;
}
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)
{
errors.Add(result);
}
}
}
}
}

12
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IDataAnnotationValidator.cs

@ -1,12 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Volo.Abp.Validation
{
public interface IDataAnnotationValidator
{
void Validate(object validatingObject);
List<ValidationResult> GetErrors(object validatingObject);
}
}

7
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IObjectValidationContributor.cs

@ -0,0 +1,7 @@
namespace Volo.Abp.Validation
{
public interface IObjectValidationContributor
{
void AddErrors(ObjectValidationContext context);
}
}

6
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/IObjectValidator.cs

@ -11,6 +11,10 @@ namespace Volo.Abp.Validation
bool allowNull = false
);
List<ValidationResult> GetErrors(object validatingObject, string name = null, bool allowNull = false);
List<ValidationResult> GetErrors(
object validatingObject,
string name = null,
bool allowNull = false
);
}
}

20
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidationContext.cs

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
namespace Volo.Abp.Validation
{
public class ObjectValidationContext
{
[NotNull]
public object ValidatingObject { get; }
public List<ValidationResult> Errors { get; }
public ObjectValidationContext([NotNull] object validatingObject)
{
ValidatingObject = Check.NotNull(validatingObject, nameof(validatingObject));
Errors = new List<ValidationResult>();
}
}
}

92
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidator.cs

@ -1,25 +1,21 @@
using System.Collections;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Reflection;
namespace Volo.Abp.Validation
{
public class ObjectValidator : IObjectValidator, ITransientDependency
{
private const int MaxRecursiveParameterValidationDepth = 8;
protected IHybridServiceScopeFactory ServiceScopeFactory { get; }
protected AbpValidationOptions Options { get; }
private readonly AbpValidationOptions _options;
private readonly IDataAnnotationValidator _dataAnnotationValidator;
public ObjectValidator(IOptions<AbpValidationOptions> options, IDataAnnotationValidator dataAnnotationValidator)
public ObjectValidator(IOptions<AbpValidationOptions> options, IHybridServiceScopeFactory serviceScopeFactory)
{
_dataAnnotationValidator = dataAnnotationValidator;
_options = options.Value;
ServiceScopeFactory = serviceScopeFactory;
Options = options.Value;
}
public virtual void Validate(object validatingObject, string name = null, bool allowNull = false)
@ -37,75 +33,35 @@ namespace Volo.Abp.Validation
public virtual List<ValidationResult> GetErrors(object validatingObject, string name = null, bool allowNull = false)
{
var errors = new List<ValidationResult>();
if (validatingObject == null && !allowNull)
{
errors.Add(
name == null
? new ValidationResult("Given object is null!")
: new ValidationResult(name + " is null!", new[] { name })
);
return errors;
}
ValidateObjectRecursively(errors, validatingObject, currentDepth: 1);
return errors;
}
protected virtual void ValidateObjectRecursively(List<ValidationResult> errors, object validatingObject, int currentDepth)
{
if (currentDepth > MaxRecursiveParameterValidationDepth)
{
return;
}
if (validatingObject == null)
{
return;
}
errors.AddRange(_dataAnnotationValidator.GetErrors(validatingObject));
//Validate items of enumerable
if (validatingObject is IEnumerable)
{
if (!(validatingObject is IQueryable))
if (allowNull)
{
foreach (var item in (validatingObject as IEnumerable))
return new List<ValidationResult>(); //TODO: Returning an array would be more performent
}
else
{
return new List<ValidationResult>
{
ValidateObjectRecursively(errors, item, currentDepth + 1);
}
name == null
? new ValidationResult("Given object is null!")
: new ValidationResult(name + " is null!", new[] {name})
};
}
return;
}
var validatingObjectType = validatingObject.GetType();
var context = new ObjectValidationContext(validatingObject);
//Do not recursively validate for primitive objects
if (TypeHelper.IsPrimitiveExtended(validatingObjectType))
using (var scope = ServiceScopeFactory.CreateScope())
{
return;
}
if (_options.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject)))
{
return;
}
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
if (property.Attributes.OfType<DisableValidationAttribute>().Any())
foreach (var contributorType in Options.ObjectValidationContributors)
{
continue;
var contributor = (IObjectValidationContributor) scope.ServiceProvider.GetRequiredService(contributorType);
contributor.AddErrors(context);
}
ValidateObjectRecursively(errors, property.GetValue(validatingObject), currentDepth + 1);
}
return context.Errors;
}
}
}

33
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ValidationInterceptor.cs

@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Threading.Tasks;
using Volo.Abp.Aspects;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
@ -10,13 +10,11 @@ namespace Volo.Abp.Validation
{
public class ValidationInterceptor : AbpInterceptor, ITransientDependency
{
private readonly AbpValidationOptions _abpValidationOptions;
private readonly IServiceProvider _serviceProvider;
private readonly IMethodInvocationValidator _methodInvocationValidator;
public ValidationInterceptor(IServiceProvider serviceProvider, IOptions<AbpValidationOptions> abpValidationOptions)
public ValidationInterceptor(IMethodInvocationValidator methodInvocationValidator)
{
_serviceProvider = serviceProvider;
_abpValidationOptions = abpValidationOptions.Value;
_methodInvocationValidator = methodInvocationValidator;
}
public override void Intercept(IAbpMethodInvocation invocation)
@ -47,18 +45,13 @@ namespace Volo.Abp.Validation
protected virtual void Validate(IAbpMethodInvocation invocation)
{
foreach (var validationContributor in _abpValidationOptions.MethodValidationContributors)
{
var validator = (IMethodInvocationValidator) _serviceProvider.GetRequiredService(validationContributor);
validator.Validate(
new MethodInvocationValidationContext(
invocation.TargetObject,
invocation.Method,
invocation.Arguments
)
);
}
_methodInvocationValidator.Validate(
new MethodInvocationValidationContext(
invocation.TargetObject,
invocation.Method,
invocation.Arguments
)
);
}
}
}

3
framework/test/Volo.Abp.FluentValidation.Tests/Volo/Abp/FluentValidation/ApplicationService_FluentValidation_Tests.cs

@ -101,8 +101,7 @@ namespace Volo.Abp.FluentValidation
output.ShouldBe("444");
}
[DependsOn(typeof(AbpAutofacModule))]
[DependsOn(typeof(AbpFluentValidationModule))]
public class TestModule : AbpModule

2
framework/test/Volo.Abp.Validation.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs

@ -16,7 +16,7 @@ namespace Volo.Abp.Validation
public ApplicationService_Validation_Tests()
{
_myAppService = ServiceProvider.GetRequiredService<IMyAppService>();
_myAppService = GetRequiredService<IMyAppService>();
}
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)

Loading…
Cancel
Save