From 24ae92fcee8b87351a8c7dc5ec8d3f2805404706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 21 Sep 2017 16:36:19 +0300 Subject: [PATCH] Added existing validation infrastructure for application services. --- src/Volo.Abp/Volo/Abp/AbpKernelModule.cs | 2 + .../Services/ApplicationService.cs | 10 +- .../Abp/Aspects/AbpCrossCuttingConcerns.cs | 78 ++++ .../IAvoidDuplicateCrossCuttingConcerns.cs | 9 + .../Validation/DisableValidationAttribute.cs | 13 + .../Validation/EnableValidationAttribute.cs | 13 + .../Volo/Abp/Validation/IShouldNormalize.cs | 13 + .../Validation/IValidationConfiguration.cs | 15 + .../Validation/MethodInvocationValidator.cs | 250 +++++++++++++ .../Abp/Validation/ValidationInterceptor.cs | 36 ++ .../ValidationInterceptorRegistrar.cs | 17 + test/Volo.Abp.Tests/Volo.Abp.Tests.csproj | 1 + .../ApplicationService_Validation_Tests.cs | 352 ++++++++++++++++++ 13 files changed, 808 insertions(+), 1 deletion(-) create mode 100644 src/Volo.Abp/Volo/Abp/Aspects/AbpCrossCuttingConcerns.cs create mode 100644 src/Volo.Abp/Volo/Abp/Aspects/IAvoidDuplicateCrossCuttingConcerns.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/DisableValidationAttribute.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/EnableValidationAttribute.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/IShouldNormalize.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/IValidationConfiguration.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/MethodInvocationValidator.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs create mode 100644 src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptorRegistrar.cs create mode 100644 test/Volo.Abp.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs diff --git a/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs b/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs index 8b772e5597..702155a3e1 100644 --- a/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs +++ b/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 => diff --git a/src/Volo.Abp/Volo/Abp/Application/Services/ApplicationService.cs b/src/Volo.Abp/Volo/Abp/Application/Services/ApplicationService.cs index 684d90b8af..e5e7012aa6 100644 --- a/src/Volo.Abp/Volo/Abp/Application/Services/ApplicationService.cs +++ b/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" }; + /// + /// Gets the applied cross cutting concerns. + /// + public List AppliedCrossCuttingConcerns { get; } = new List(); + /* Will be added when implemented - AbpSession - ... diff --git a/src/Volo.Abp/Volo/Abp/Aspects/AbpCrossCuttingConcerns.cs b/src/Volo.Abp/Volo/Abp/Aspects/AbpCrossCuttingConcerns.cs new file mode 100644 index 0000000000..87319241e7 --- /dev/null +++ b/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(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Aspects/IAvoidDuplicateCrossCuttingConcerns.cs b/src/Volo.Abp/Volo/Abp/Aspects/IAvoidDuplicateCrossCuttingConcerns.cs new file mode 100644 index 0000000000..2eceab9f80 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Aspects/IAvoidDuplicateCrossCuttingConcerns.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Aspects +{ + public interface IAvoidDuplicateCrossCuttingConcerns + { + List AppliedCrossCuttingConcerns { get; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/DisableValidationAttribute.cs b/src/Volo.Abp/Volo/Abp/Validation/DisableValidationAttribute.cs new file mode 100644 index 0000000000..6c78cdb8d8 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/DisableValidationAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Volo.Abp.Validation +{ + /// + /// Can be added to a method to disable auto validation. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property)] + public class DisableValidationAttribute : Attribute + { + + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/EnableValidationAttribute.cs b/src/Volo.Abp/Volo/Abp/Validation/EnableValidationAttribute.cs new file mode 100644 index 0000000000..7c13e36b6b --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/EnableValidationAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Volo.Abp.Validation +{ + /// + /// Can be added to a method to enable auto validation if validation is disabled for it's class. + /// + [AttributeUsage(AttributeTargets.Method)] + public class EnableValidationAttribute : Attribute + { + + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/IShouldNormalize.cs b/src/Volo.Abp/Volo/Abp/Validation/IShouldNormalize.cs new file mode 100644 index 0000000000..bf543bba6e --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Validation/IShouldNormalize.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.Validation +{ + /// + /// This interface is used to normalize inputs before method execution. + /// + public interface IShouldNormalize + { + /// + /// This method is called lastly before method execution (after validation if exists). + /// + void Normalize(); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Validation/IValidationConfiguration.cs b/src/Volo.Abp/Volo/Abp/Validation/IValidationConfiguration.cs new file mode 100644 index 0000000000..7040be55bc --- /dev/null +++ b/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 IgnoredTypes { get; } + + public AbpValidationOptions() + { + IgnoredTypes = 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 new file mode 100644 index 0000000000..fa2a887fe6 --- /dev/null +++ b/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 +{ + /// + /// This class is used to validate a method call (invocation) for method arguments. + /// + 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 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) + { + Check.NotNull(method, nameof(method)); + Check.NotNull(parameterValues, nameof(parameterValues)); + + Method = method; + ParameterValues = parameterValues; + Parameters = method.GetParameters(); + } + + /// + /// Validates the method invocation. + /// + 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(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); + } + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs b/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptor.cs new file mode 100644 index 0000000000..bb81a02bba --- /dev/null +++ b/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(); + validator.Initialize(invocation.Method, invocation.Arguments); + validator.Validate(); + } + + invocation.Proceed(); + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptorRegistrar.cs b/src/Volo.Abp/Volo/Abp/Validation/ValidationInterceptorRegistrar.cs new file mode 100644 index 0000000000..85e6c07bb6 --- /dev/null +++ b/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(); + } + } + } +} \ No newline at end of file diff --git a/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj b/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj index 70f48c988b..6f578e566d 100644 --- a/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj +++ b/test/Volo.Abp.Tests/Volo.Abp.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/test/Volo.Abp.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Validation/ApplicationService_Validation_Tests.cs new file mode 100644 index 0000000000..a72c84d54b --- /dev/null +++ b/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 + { + private readonly IMyAppService _myAppService; + + public ApplicationService_Validation_Tests() + { + _myAppService = ServiceProvider.GetRequiredService(); + } + + 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(() => _myAppService.MyMethod(new MyMethodInput())); //MyStringValue is not supplied! + Assert.Throws(() => _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(() => + _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(() => + _myAppService.MyMethod2(new MyMethod2Input //Input1 is not set + { + MyStringValue2 = "test 1" + })); + } + + [Fact] + public void Should_Not_Work_With_Wrong_List_Input_1() + { + Assert.Throws(() => + _myAppService.MyMethod3( + new MyMethod3Input + { + MyStringValue2 = "test 1", + ListItems = new List + { + new MyClassInList {ValueInList = null} + } + })); + } + + [Fact] + public void Should_Not_Work_With_Wrong_Array_Input_1() + { + Assert.Throws(() => + _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(() => + _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(() => + { + _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(); + } + } + + 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 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 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 + } + } +} \ No newline at end of file