Browse Source

Added existing validation infrastructure for application services.

pull/113/head
Halil İbrahim Kalkan 9 years ago
parent
commit
24ae92fcee
  1. 2
      src/Volo.Abp/Volo/Abp/AbpKernelModule.cs
  2. 10
      src/Volo.Abp/Volo/Abp/Application/Services/ApplicationService.cs
  3. 78
      src/Volo.Abp/Volo/Abp/Aspects/AbpCrossCuttingConcerns.cs
  4. 9
      src/Volo.Abp/Volo/Abp/Aspects/IAvoidDuplicateCrossCuttingConcerns.cs
  5. 13
      src/Volo.Abp/Volo/Abp/Validation/DisableValidationAttribute.cs
  6. 13
      src/Volo.Abp/Volo/Abp/Validation/EnableValidationAttribute.cs
  7. 13
      src/Volo.Abp/Volo/Abp/Validation/IShouldNormalize.cs
  8. 15
      src/Volo.Abp/Volo/Abp/Validation/IValidationConfiguration.cs
  9. 250
      src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidator.cs
  10. 36
      src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs
  11. 17
      src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptorRegistrar.cs
  12. 1
      test/Volo.Abp.Tests/Volo.Abp.Tests.csproj
  13. 352
      test/Volo.Abp.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs

2
src/Volo.Abp/Volo/Abp/AbpKernelModule.cs

@ -5,6 +5,7 @@ using Volo.Abp.Modularity;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Reflection;
using Volo.Abp.Uow;
using Volo.Abp.Validation;
namespace Volo.Abp
{
@ -13,6 +14,7 @@ namespace Volo.Abp
public override void PreConfigureServices(IServiceCollection services)
{
services.OnRegistred(UnitOfWorkInterceptorRegistrar.RegisterIfNeeded);
services.OnRegistred(ValidationInterceptorRegistrar.RegisterIfNeeded);
//TODO: Move to a dedicated class
services.OnExposing(context =>

10
src/Volo.Abp/Volo/Abp/Application/Services/ApplicationService.cs

@ -1,9 +1,17 @@
using System.Collections.Generic;
using Volo.Abp.Aspects;
namespace Volo.Abp.Application.Services
{
public abstract class ApplicationService : AbpServiceBase, IApplicationService
public abstract class ApplicationService : AbpServiceBase, IApplicationService, IAvoidDuplicateCrossCuttingConcerns
{
public static string[] CommonPostfixes { get; set; } = { "AppService", "ApplicationService", "Service" };
/// <summary>
/// Gets the applied cross cutting concerns.
/// </summary>
public List<string> AppliedCrossCuttingConcerns { get; } = new List<string>();
/* Will be added when implemented
- AbpSession
- ...

78
src/Volo.Abp/Volo/Abp/Aspects/AbpCrossCuttingConcerns.cs

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Volo.Abp.Aspects
{
internal static class AbpCrossCuttingConcerns
{
public const string Auditing = "AbpAuditing";
public const string Validation = "AbpValidation";
public const string UnitOfWork = "AbpUnitOfWork";
public const string Authorization = "AbpAuthorization";
public static void AddApplied(object obj, params string[] concerns)
{
if (concerns.IsNullOrEmpty())
{
throw new ArgumentNullException(nameof(concerns), $"{nameof(concerns)} should be provided!");
}
(obj as IAvoidDuplicateCrossCuttingConcerns)?.AppliedCrossCuttingConcerns.AddRange(concerns);
}
public static void RemoveApplied(object obj, params string[] concerns)
{
if (concerns.IsNullOrEmpty())
{
throw new ArgumentNullException(nameof(concerns), $"{nameof(concerns)} should be provided!");
}
var crossCuttingEnabledObj = obj as IAvoidDuplicateCrossCuttingConcerns;
if (crossCuttingEnabledObj == null)
{
return;
}
foreach (var concern in concerns)
{
crossCuttingEnabledObj.AppliedCrossCuttingConcerns.RemoveAll(c => c == concern);
}
}
public static bool IsApplied([NotNull] object obj, [NotNull] string concern)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
if (concern == null)
{
throw new ArgumentNullException(nameof(concern));
}
return (obj as IAvoidDuplicateCrossCuttingConcerns)?.AppliedCrossCuttingConcerns.Contains(concern) ?? false;
}
public static IDisposable Applying(object obj, params string[] concerns)
{
AddApplied(obj, concerns);
return new DisposeAction(() =>
{
RemoveApplied(obj, concerns);
});
}
public static string[] GetApplieds(object obj)
{
var crossCuttingEnabledObj = obj as IAvoidDuplicateCrossCuttingConcerns;
if (crossCuttingEnabledObj == null)
{
return new string[0];
}
return crossCuttingEnabledObj.AppliedCrossCuttingConcerns.ToArray();
}
}
}

9
src/Volo.Abp/Volo/Abp/Aspects/IAvoidDuplicateCrossCuttingConcerns.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Volo.Abp.Aspects
{
public interface IAvoidDuplicateCrossCuttingConcerns
{
List<string> AppliedCrossCuttingConcerns { get; }
}
}

13
src/Volo.Abp/Volo/Abp/Validation/DisableValidationAttribute.cs

@ -0,0 +1,13 @@
using System;
namespace Volo.Abp.Validation
{
/// <summary>
/// Can be added to a method to disable auto validation.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property)]
public class DisableValidationAttribute : Attribute
{
}
}

13
src/Volo.Abp/Volo/Abp/Validation/EnableValidationAttribute.cs

@ -0,0 +1,13 @@
using System;
namespace Volo.Abp.Validation
{
/// <summary>
/// Can be added to a method to enable auto validation if validation is disabled for it's class.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class EnableValidationAttribute : Attribute
{
}
}

13
src/Volo.Abp/Volo/Abp/Validation/IShouldNormalize.cs

@ -0,0 +1,13 @@
namespace Volo.Abp.Validation
{
/// <summary>
/// This interface is used to normalize inputs before method execution.
/// </summary>
public interface IShouldNormalize
{
/// <summary>
/// This method is called lastly before method execution (after validation if exists).
/// </summary>
void Normalize();
}
}

15
src/Volo.Abp/Volo/Abp/Validation/IValidationConfiguration.cs

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Validation
{
public class AbpValidationOptions
{
public List<Type> IgnoredTypes { get; }
public AbpValidationOptions()
{
IgnoredTypes = new List<Type>();
}
}
}

250
src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidator.cs

@ -0,0 +1,250 @@
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
{
/// <summary>
/// This class is used to validate a method call (invocation) for method arguments.
/// </summary>
public class MethodInvocationValidator : ITransientDependency
{
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<ValidationResult> ValidationErrors { get; }
protected List<IShouldNormalize> ObjectsToBeNormalized { get; }
private readonly AbpValidationOptions _options;
/// <summary>
/// Creates a new <see cref="MethodInvocationValidator"/> instance.
/// </summary>
public MethodInvocationValidator(IOptions<AbpValidationOptions> options)
{
_options = options.Value;
ValidationErrors = new List<ValidationResult>();
ObjectsToBeNormalized = new List<IShouldNormalize>();
}
/// <param name="method">Method to be validated</param>
/// <param name="parameterValues">List of arguments those are used to call the <paramref name="method"/>.</param>
public virtual void Initialize(MethodInfo method, object[] parameterValues)
{
Check.NotNull(method, nameof(method));
Check.NotNull(parameterValues, nameof(parameterValues));
Method = method;
ParameterValues = parameterValues;
Parameters = method.GetParameters();
}
/// <summary>
/// Validates the method invocation.
/// </summary>
public void Validate()
{
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<DisableValidationAttribute>(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
);
}
/// <summary>
/// Validates given parameter for given value.
/// </summary>
/// <param name="parameterInfo">Parameter of the method to validate</param>
/// <param name="parameterValue">Value to validate</param>
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<PropertyDescriptor>();
foreach (var property in properties)
{
if (property.Attributes.OfType<DisableValidationAttribute>().Any())
{
continue;
}
ValidateObjectRecursively(property.GetValue(validatingObject), currentDepth + 1);
}
}
/// <summary>
/// Checks all properties for DataAnnotations attributes.
/// </summary>
protected virtual void SetDataAnnotationAttributeErrors(object validatingObject)
{
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
var validationAttributes = property.Attributes.OfType<ValidationAttribute>().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);
}
}
}
}

36
src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs

@ -0,0 +1,36 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Aspects;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Validation
{
public class ValidationInterceptor : AbpInterceptor, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
public ValidationInterceptor(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override void Intercept(IAbpMethodInvocation invocation)
{
if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Validation))
{
invocation.Proceed();
return;
}
using (var scope = _serviceProvider.CreateScope())
{
var validator = scope.ServiceProvider.GetRequiredService<MethodInvocationValidator>();
validator.Initialize(invocation.Method, invocation.Arguments);
validator.Validate();
}
invocation.Proceed();
}
}
}

17
src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptorRegistrar.cs

@ -0,0 +1,17 @@
using Volo.Abp.Application.Services;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Validation
{
public static class ValidationInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (typeof(IApplicationService).IsAssignableFrom(context.ImplementationType))
{
//TODO: Notice that it may add the interceptor more than one for every exposed service type!?
context.Interceptors.Add<ValidationInterceptor>();
}
}
}
}

1
test/Volo.Abp.Tests/Volo.Abp.Tests.csproj

@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp\Volo.Abp.csproj" />
</ItemGroup>

352
test/Volo.Abp.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs

@ -0,0 +1,352 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Application.Services;
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
using Volo.Abp.TestBase;
using Xunit;
namespace Volo.Abp.Validation
{
public class ApplicationService_Validation_Tests : AbpIntegratedTest<ApplicationService_Validation_Tests.ValidationTestModule>
{
private readonly IMyAppService _myAppService;
public ApplicationService_Validation_Tests()
{
_myAppService = ServiceProvider.GetRequiredService<IMyAppService>();
}
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
{
options.UseAutofac();
}
[Fact]
public void Should_Work_Proper_With_Right_Inputs()
{
var output = _myAppService.MyMethod(new MyMethodInput { MyStringValue = "test" });
output.Result.ShouldBe(42);
}
[Fact]
public void Should_Not_Work_With_Wrong_Inputs()
{
Assert.Throws<AbpValidationException>(() => _myAppService.MyMethod(new MyMethodInput())); //MyStringValue is not supplied!
Assert.Throws<AbpValidationException>(() => _myAppService.MyMethod(new MyMethodInput { MyStringValue = "a" })); //MyStringValue's min length should be 3!
}
[Fact]
public void Should_Work_With_Right_Nesned_Inputs()
{
var output = _myAppService.MyMethod2(new MyMethod2Input
{
MyStringValue2 = "test 1",
Input1 = new MyMethodInput { MyStringValue = "test 2" },
DateTimeValue = DateTime.Now
});
output.Result.ShouldBe(42);
}
[Fact]
public void Should_Not_Work_With_Wrong_Nesned_Inputs_1()
{
Assert.Throws<AbpValidationException>(() =>
_myAppService.MyMethod2(new MyMethod2Input
{
MyStringValue2 = "test 1",
Input1 = new MyMethodInput() //MyStringValue is not set
}));
}
[Fact]
public void Should_Not_Work_With_Wrong_Nesned_Inputs_2()
{
Assert.Throws<AbpValidationException>(() =>
_myAppService.MyMethod2(new MyMethod2Input //Input1 is not set
{
MyStringValue2 = "test 1"
}));
}
[Fact]
public void Should_Not_Work_With_Wrong_List_Input_1()
{
Assert.Throws<AbpValidationException>(() =>
_myAppService.MyMethod3(
new MyMethod3Input
{
MyStringValue2 = "test 1",
ListItems = new List<MyClassInList>
{
new MyClassInList {ValueInList = null}
}
}));
}
[Fact]
public void Should_Not_Work_With_Wrong_Array_Input_1()
{
Assert.Throws<AbpValidationException>(() =>
_myAppService.MyMethod3(
new MyMethod3Input
{
MyStringValue2 = "test 1",
ArrayItems = new[]
{
new MyClassInList {ValueInList = null}
}
}));
}
[Fact]
public void Should_Not_Work_If_Array_Is_Null()
{
Assert.Throws<AbpValidationException>(() =>
_myAppService.MyMethod4(new MyMethod4Input()) //ArrayItems is null!
);
}
[Fact]
public void Should_Work_If_Array_Is_Null_But_DisabledValidation_For_Method()
{
_myAppService.MyMethod4_2(new MyMethod4Input());
}
[Fact]
public void Should_Work_If_Array_Is_Null_But_DisabledValidation_For_Property()
{
_myAppService.MyMethod5(new MyMethod5Input());
}
[Fact]
public void Should_Use_IValidatableObject()
{
Assert.Throws<AbpValidationException>(() =>
{
_myAppService.MyMethod6(new MyMethod6Input
{
MyStringValue = "test value" //MyIntValue has not set!
});
});
}
[Fact]
public void Should_Normalize_Nested_Dtos()
{
var input = new MyMethod7Input
{
Inner = new MyMethod7Input.MyMethod7InputInner
{
Value = 10
}
};
_myAppService.MyMethod7(input);
input.Inner.Value.ShouldBe(12);
}
[Fact]
public void Should_Stop_Recursive_Validation_In_A_Constant_Depth()
{
_myAppService.MyMethod8(new MyClassWithRecursiveReference { Value = "42" }).Result.ShouldBe(42);
}
[Fact]
public void Should_Allow_Null_For_Nullable_Enums()
{
_myAppService.MyMethodWithNullableEnum(null);
}
[DependsOn(typeof(AbpAutofacModule))]
public class ValidationTestModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddType<MyAppService>();
}
}
public interface IMyAppService : IApplicationService
{
MyMethodOutput MyMethod(MyMethodInput input);
MyMethodOutput MyMethod2(MyMethod2Input input);
MyMethodOutput MyMethod3(MyMethod3Input input);
MyMethodOutput MyMethod4(MyMethod4Input input);
MyMethodOutput MyMethod4_2(MyMethod4Input input);
MyMethodOutput MyMethod5(MyMethod5Input input);
MyMethodOutput MyMethod6(MyMethod6Input input);
MyMethodOutput MyMethod7(MyMethod7Input input);
MyMethodOutput MyMethod8(MyClassWithRecursiveReference input);
void MyMethodWithNullableEnum(MyEnum? value);
}
public class MyAppService : ApplicationService, IMyAppService
{
public MyMethodOutput MyMethod(MyMethodInput input)
{
return new MyMethodOutput { Result = 42 };
}
public MyMethodOutput MyMethod2(MyMethod2Input input)
{
return new MyMethodOutput { Result = 42 };
}
public MyMethodOutput MyMethod3(MyMethod3Input input)
{
return new MyMethodOutput { Result = 42 };
}
public MyMethodOutput MyMethod4(MyMethod4Input input)
{
return new MyMethodOutput { Result = 42 };
}
[DisableValidation]
public MyMethodOutput MyMethod4_2(MyMethod4Input input)
{
return new MyMethodOutput { Result = 42 };
}
public MyMethodOutput MyMethod5(MyMethod5Input input)
{
return new MyMethodOutput { Result = 42 };
}
public MyMethodOutput MyMethod6(MyMethod6Input input)
{
return new MyMethodOutput { Result = 42 };
}
public MyMethodOutput MyMethod7(MyMethod7Input input)
{
return new MyMethodOutput { Result = 42 };
}
public MyMethodOutput MyMethod8(MyClassWithRecursiveReference input)
{
return new MyMethodOutput { Result = 42 };
}
public void MyMethodWithNullableEnum(MyEnum? value)
{
}
}
public class MyMethodInput
{
[Required]
[MinLength(3)]
public string MyStringValue { get; set; }
}
public class MyMethod2Input
{
[Required]
[MinLength(2)]
public string MyStringValue2 { get; set; }
public DateTime DateTimeValue { get; set; }
[Required]
public MyMethodInput Input1 { get; set; }
}
public class MyMethod3Input
{
[Required]
[MinLength(2)]
public string MyStringValue2 { get; set; }
public List<MyClassInList> ListItems { get; set; }
public MyClassInList[] ArrayItems { get; set; }
}
public class MyMethod4Input
{
[Required]
public MyClassInList[] ArrayItems { get; set; }
}
public class MyMethod5Input
{
[DisableValidation]
public MyClassInList[] ArrayItems { get; set; }
}
public class MyMethod6Input : IValidatableObject
{
[Required]
[MinLength(2)]
public string MyStringValue { get; set; }
public int MyIntValue { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (MyIntValue < 18)
{
yield return new ValidationResult("MyIntValue must be greather than or equal to 18");
}
}
}
public class MyMethod7Input : IShouldNormalize
{
public MyMethod7InputInner Inner { get; set; }
public void Normalize()
{
Inner.Value++;
}
public class MyMethod7InputInner : IShouldNormalize
{
public int Value { get; set; }
public void Normalize()
{
Value++;
}
}
}
public class MyClassInList
{
[Required]
[MinLength(3)]
public string ValueInList { get; set; }
}
public class MyMethodOutput
{
public int Result { get; set; }
}
public class MyClassWithRecursiveReference
{
public MyClassWithRecursiveReference Reference { get; }
[Required]
public string Value { get; set; }
public MyClassWithRecursiveReference()
{
Reference = this;
}
}
public enum MyEnum
{
Value1,
Value2
}
}
}
Loading…
Cancel
Save