From 4d634c8c1eb72f7c61a54385ad9cd5cc62808196 Mon Sep 17 00:00:00 2001 From: Yuriy Durov Date: Tue, 22 Oct 2024 10:47:38 +0400 Subject: [PATCH] ActionType override (#13) --- ...BitzArt.FluentValidation.Extensions.csproj | 1 + .../AddActionValidatorExtensions.cs | 94 ++++++++++++++++--- .../Interfaces/IActionValidator.cs | 8 ++ .../Services/ActionValidatorFactory.cs | 37 ++++---- .../Services/IActionValidatorFactory.cs | 6 +- .../Services/ValidatorMapEntry.cs | 18 ++++ .../Validators/ActionValidator.cs | 1 + ...ActionValidatorFactoryHierarchicalTests.cs | 54 ++++++++++- .../Tests/ActionValidatorFactoryTests.cs | 68 ++++++++++++-- 9 files changed, 245 insertions(+), 42 deletions(-) create mode 100644 src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ValidatorMapEntry.cs diff --git a/src/FluentValidation/BitzArt.FluentValidation.Extensions/BitzArt.FluentValidation.Extensions.csproj b/src/FluentValidation/BitzArt.FluentValidation.Extensions/BitzArt.FluentValidation.Extensions.csproj index 2e29046..06ab164 100644 --- a/src/FluentValidation/BitzArt.FluentValidation.Extensions/BitzArt.FluentValidation.Extensions.csproj +++ b/src/FluentValidation/BitzArt.FluentValidation.Extensions/BitzArt.FluentValidation.Extensions.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true FluentValidation BitzArt.FluentValidation.Extensions diff --git a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Extensions/AddActionValidatorExtensions.cs b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Extensions/AddActionValidatorExtensions.cs index 8eaa9a3..77845ff 100644 --- a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Extensions/AddActionValidatorExtensions.cs +++ b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Extensions/AddActionValidatorExtensions.cs @@ -6,13 +6,32 @@ namespace FluentValidation; public static class AddActionValidatorExtensions { - public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, Func? actionTypeResolver = null) + public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, ActionType actionType) + => services.AddActionValidatorsFromAssemblyContaining(typeof(TAssemblyPointer), actionType); + + public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, Type type, ActionType actionType) + => services.AddActionValidatorsFromAssembly(type.Assembly, actionType); + + public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, Func actionTypeResolver) => services.AddActionValidatorsFromAssemblyContaining(typeof(TAssemblyPointer), actionTypeResolver); - public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, Type type, Func? actionTypeResolver = null) + public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, Type type, Func actionTypeResolver) => services.AddActionValidatorsFromAssembly(type.Assembly, actionTypeResolver); - public static IServiceCollection AddActionValidatorsFromAssembly(this IServiceCollection services, Assembly assembly, Func? actionTypeResolver = null) + public static IServiceCollection AddActionValidatorsFromAssembly(this IServiceCollection services, Assembly assembly, ActionType actionType) + { + var validators = assembly + .DefinedTypes + .Where(x => x.IsClass && !x.IsAbstract) + .Where(x => x.GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IActionValidator<>))); + + foreach (var validator in validators) services.AddActionValidator(validator, actionType); + + return services; + } + + public static IServiceCollection AddActionValidatorsFromAssembly(this IServiceCollection services, Assembly assembly, Func actionTypeResolver) { var validators = assembly .DefinedTypes @@ -25,13 +44,12 @@ public static IServiceCollection AddActionValidatorsFromAssembly(this IServiceCo return services; } - public static IServiceCollection AddActionValidator(this IServiceCollection services, Func? actionTypeResolver = null) - => services.AddActionValidator(typeof(TValidator), actionTypeResolver); + public static IServiceCollection AddActionValidator(this IServiceCollection services, ActionType actionType) + => services.AddActionValidator(typeof(TValidator), actionType); - public static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Func? actionTypeResolver = null) + public static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, ActionType actionType) { if (validatorType is null) throw new ArgumentException($"{nameof(validatorType)} must not be null"); - if (validatorType.BaseType!.GetGenericTypeDefinition() != typeof(ActionValidator<>)) throw new ArgumentException($"{validatorType.Name} is not assignable to ActionValidator"); services.TryAddScoped(serviceProvider => new ActionValidatorFactory(serviceProvider)); @@ -39,22 +57,68 @@ public static IServiceCollection AddActionValidator(this IServiceCollection serv if (interfaceDefinitions.Count == 0) throw new ArgumentException($"{validatorType.Name} does not implement IActionValidator"); services.AddTransient(validatorType); - ActionValidatorFactory.ValidatorTypeMap[validatorType] = validatorType; + var mapEntry = new ValidatorMapEntry(validatorType, DefinedActionType: actionType); + services.AddKeyedSingleton(serviceKey: validatorType, implementationInstance: mapEntry); + + foreach (var interfaceDefinition in interfaceDefinitions) + { + var validationObjectType = interfaceDefinition.GetGenericArguments().First(); + services.AddActionValidator(validatorType, validationObjectType, actionType, mapEntry); + } + + return services; + } + + public static IServiceCollection AddActionValidator(this IServiceCollection services, Func actionTypeResolver) + => services.AddActionValidator(typeof(TValidator), actionTypeResolver); + + public static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Func actionTypeResolver) + { + if (validatorType is null) throw new ArgumentException($"{nameof(validatorType)} must not be null"); - Func finalActionTypeResolver = actionTypeResolver is not null ? - x => actionTypeResolver(x) : - x => null; + services.TryAddScoped(serviceProvider => new ActionValidatorFactory(serviceProvider)); + + var interfaceDefinitions = validatorType.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IActionValidator<>)).ToList(); + if (interfaceDefinitions.Count == 0) throw new ArgumentException($"{validatorType.Name} does not implement IActionValidator"); + + services.AddTransient(validatorType); + var mapEntry = new ValidatorMapEntry(validatorType, ActionTypeResolver: actionTypeResolver); + services.AddKeyedSingleton(serviceKey: validatorType, implementationInstance: mapEntry); foreach (var interfaceDefinition in interfaceDefinitions) { var validationObjectType = interfaceDefinition.GetGenericArguments().First(); - services.AddActionValidator(validatorType, validationObjectType, finalActionTypeResolver); + services.AddActionValidator(validatorType, validationObjectType, actionTypeResolver, mapEntry); + } + + return services; + } + + private static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Type validationObjectType, ActionType actionType, ValidatorMapEntry mapEntry) + { + List registrationInterfaces = + [ + typeof(IValidator<>).MakeGenericType(validationObjectType), + typeof(IActionValidator<>).MakeGenericType(validationObjectType) + ]; + + foreach (var registrationInterface in registrationInterfaces) + { + services.AddScoped(registrationInterface, x => + { + var factory = x.GetRequiredService(); + var validator = factory.GetValidatorInternal(validatorType); + + return validator; + }); + + services.AddKeyedSingleton(serviceKey: registrationInterface, implementationInstance: mapEntry); } return services; } - private static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Type validationObjectType, Func getActionType) + private static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Type validationObjectType, Func getActionType, ValidatorMapEntry mapEntry) { List registrationInterfaces = [ @@ -67,12 +131,12 @@ private static IServiceCollection AddActionValidator(this IServiceCollection ser services.AddScoped(registrationInterface, x => { var factory = x.GetRequiredService(); - var validator = factory.GetValidatorInternal(validatorType, getActionType: getActionType); + var validator = factory.GetValidatorInternal(validatorType); return validator; }); - ActionValidatorFactory.ValidatorTypeMap[registrationInterface] = validatorType; + services.AddKeyedSingleton(serviceKey: registrationInterface, implementationInstance: mapEntry); } return services; diff --git a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Interfaces/IActionValidator.cs b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Interfaces/IActionValidator.cs index 621f147..87e89bb 100644 --- a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Interfaces/IActionValidator.cs +++ b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Interfaces/IActionValidator.cs @@ -1,11 +1,19 @@ namespace FluentValidation; +/// +/// The type of object being validated. public interface IActionValidator : IValidator, IActionValidator { } +/// +/// An that can be used to validate an object based on the action being performed. +/// public interface IActionValidator { + /// + /// Current action type. + /// public ActionType? Action { get; internal set; } } diff --git a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ActionValidatorFactory.cs b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ActionValidatorFactory.cs index a9fa2da..5060a25 100644 --- a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ActionValidatorFactory.cs +++ b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ActionValidatorFactory.cs @@ -1,37 +1,42 @@ using Microsoft.Extensions.DependencyInjection; -using System.Collections.Concurrent; namespace FluentValidation; internal class ActionValidatorFactory(IServiceProvider serviceProvider) : IActionValidatorFactory { - internal static ConcurrentDictionary ValidatorTypeMap = []; - internal IServiceProvider _serviceProvider = serviceProvider; private ActionType? _actionType = null; - public IActionValidator GetValidator(ActionType actionType) - => (IActionValidator)GetValidatorInternal(typeof(IActionValidator), definedActionType: actionType); + public IActionValidator GetValidator(ActionType? actionType = null) + => (IActionValidator)GetValidatorInternal(typeof(IActionValidator), actionType); - public IActionValidator GetValidator(Type objectType, ActionType actionType) - => GetValidatorInternal(typeof(IActionValidator<>).MakeGenericType(objectType), definedActionType: actionType); + public IActionValidator GetValidator(Type objectType, ActionType? actionType = null) + => GetValidatorInternal(typeof(IActionValidator<>).MakeGenericType(objectType), actionType); - public IActionValidator GetValidatorInternal(Type validatorType, Func? actionTypeResolver = null, ActionType? definedActionType = null) + public IActionValidator GetValidatorInternal(Type validatorType, ActionType? actionTypeOverride = null) { bool cleanup = false; + try { - var implementationType = ValidatorTypeMap[validatorType] - ?? throw new ArgumentException($"{validatorType.Name} is not registered as ActionValidator"); + var validatorInfo = _serviceProvider.GetRequiredKeyedService(validatorType); - if (definedActionType.HasValue) + if (!_actionType.HasValue) { - _actionType = definedActionType; - cleanup = true; + if (actionTypeOverride.HasValue) + { + _actionType = actionTypeOverride; + cleanup = true; + } + else if (validatorInfo.DefinedActionType.HasValue) + { + _actionType = validatorInfo.DefinedActionType!.Value; + cleanup = true; + } } - var validator = (IActionValidator)_serviceProvider.GetRequiredService(implementationType); + var validator = (IActionValidator)_serviceProvider.GetRequiredService(validatorInfo.ImplementationType); if (_actionType.HasValue) { @@ -39,9 +44,9 @@ public IActionValidator GetValidatorInternal(Type validatorType, Func GetValidator(ActionType actionType); + public IActionValidator GetValidator(ActionType? actionType = null); - public IActionValidator GetValidator(Type objectType, ActionType actionType); + public IActionValidator GetValidator(Type objectType, ActionType? actionType = null); - internal IActionValidator GetValidatorInternal(Type validatorType, Func getActionType, ActionType? actionType = null); + internal IActionValidator GetValidatorInternal(Type validatorType, ActionType? actionType = null); } diff --git a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ValidatorMapEntry.cs b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ValidatorMapEntry.cs new file mode 100644 index 0000000..92620de --- /dev/null +++ b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Services/ValidatorMapEntry.cs @@ -0,0 +1,18 @@ +namespace FluentValidation; + +internal record ValidatorMapEntry +{ + public Type ImplementationType { get; } + public Func? ActionTypeResolver { get; } + public ActionType? DefinedActionType { get; } + + public ValidatorMapEntry(Type ImplementationType, Func? ActionTypeResolver = null, ActionType? DefinedActionType = null) + { + ArgumentNullException.ThrowIfNull(ImplementationType, nameof(ImplementationType)); + if (ActionTypeResolver is null && DefinedActionType is null) throw new ArgumentException("Either ActionTypeResolver or DefinedActionType must be provided"); + + this.ImplementationType = ImplementationType; + this.ActionTypeResolver = ActionTypeResolver; + this.DefinedActionType = DefinedActionType; + } +} diff --git a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Validators/ActionValidator.cs b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Validators/ActionValidator.cs index 789a87f..32c19e6 100644 --- a/src/FluentValidation/BitzArt.FluentValidation.Extensions/Validators/ActionValidator.cs +++ b/src/FluentValidation/BitzArt.FluentValidation.Extensions/Validators/ActionValidator.cs @@ -1,5 +1,6 @@ namespace FluentValidation; +/// public abstract class ActionValidator : AbstractValidator, IActionValidator { public ActionType? Action { get; set; } diff --git a/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryHierarchicalTests.cs b/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryHierarchicalTests.cs index 1144235..af9f2d4 100644 --- a/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryHierarchicalTests.cs +++ b/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryHierarchicalTests.cs @@ -38,8 +38,8 @@ public void GetValidator_OnValidatorHierarchy_ShouldSetActionTypeForAllValidator { // Arrange var services = new ServiceCollection(); - services.AddActionValidator(); - services.AddActionValidator(); + services.AddActionValidator(ActionType.Get); + services.AddActionValidator(ActionType.Get); var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService(); @@ -56,4 +56,54 @@ public void GetValidator_OnValidatorHierarchy_ShouldSetActionTypeForAllValidator Assert.True(descriptionValidator is TestDescriptionValidator); Assert.Equal(ActionType.Create, descriptionValidator.Action); } + + [Fact] + public void GetValidator_OnValidatorHierarchyWithDifferentActionTypes_ShouldSetActionTypeFromFirstDefinedInHierarchy() + { + // Arrange + var services = new ServiceCollection(); + services.AddActionValidator(ActionType.Get); + services.AddActionValidator(ActionType.Create); + + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + + // Act + var validator = factory.GetValidator(); + + // Assert + Assert.NotNull(validator); + Assert.True(validator is TestHierarchyParentValidator); + Assert.Equal(ActionType.Get, ((TestHierarchyParentValidator)validator).Action); + + var descriptionValidator = ((TestHierarchyParentValidator)validator).DescriptionValidator; + Assert.NotNull(descriptionValidator); + Assert.True(descriptionValidator is TestDescriptionValidator); + Assert.Equal(ActionType.Get, descriptionValidator.Action); + } + + [Fact] + public void GetValidator_WithActionTypeOverride_ShouldOverride() + { + // Arrange + var services = new ServiceCollection(); + services.AddActionValidator(ActionType.Get); + services.AddActionValidator(ActionType.Create); + + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + + // Act + var validator = factory.GetValidator(ActionType.Update); + + // Assert + Assert.NotNull(validator); + Assert.True(validator is TestHierarchyParentValidator); + Assert.Equal(ActionType.Update, ((TestHierarchyParentValidator)validator).Action); + + var descriptionValidator = ((TestHierarchyParentValidator)validator).DescriptionValidator; + Assert.NotNull(descriptionValidator); + Assert.True(descriptionValidator is TestDescriptionValidator); + Assert.Equal(ActionType.Update, descriptionValidator.Action); + } } diff --git a/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryTests.cs b/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryTests.cs index 793ecc9..54451ea 100644 --- a/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryTests.cs +++ b/tests/FluentValidation/BitzArt.FluentValidation.Extensions.UnitTests/Tests/ActionValidatorFactoryTests.cs @@ -19,13 +19,13 @@ public void GetValidatorInternal_OnDefinedActionValidator_ShouldReturnValidator( var validatorType = typeof(TestEntityValidator); // Act - services.AddActionValidator(validatorType); + services.AddActionValidator(validatorType, actionType); // Assert var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService(); - var validator = factory.GetValidatorInternal(validatorType, (_) => null, actionType: actionType); + var validator = factory.GetValidatorInternal(validatorType); Assert.NotNull(validator); Assert.True(validator is TestEntityValidator); @@ -39,26 +39,54 @@ public void GetValidatorInternal_OnDefinedActionValidator_ShouldReturnValidator( [InlineData(ActionType.Patch)] [InlineData(ActionType.Options)] [InlineData(ActionType.Delete)] - public void GetValidatorReflexion_OnDefinedActionValidator_ShouldReturnValidator(ActionType actionType) + public void GetValidatorReflection_OnDefinedActionValidator_ShouldReturnValidator(ActionType actionType) { // Arrange IServiceCollection services = new ServiceCollection(); var validatorType = typeof(TestEntityValidator); // Act - services.AddActionValidator(validatorType); + services.AddActionValidator(validatorType, actionType); // Assert var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService(); - var validator = factory.GetValidator(typeof(TestEntity), actionType); + var validator = factory.GetValidator(typeof(TestEntity)); Assert.NotNull(validator); Assert.True(validator is TestEntityValidator); Assert.Equal(actionType, ((TestEntityValidator)validator).Action); } + [Theory] + [InlineData(ActionType.Get)] + [InlineData(ActionType.Create)] + [InlineData(ActionType.Update)] + [InlineData(ActionType.Patch)] + [InlineData(ActionType.Options)] + [InlineData(ActionType.Delete)] + public void GetValidatorReflection_WithActionTypeOverride_ShouldOverrideActionType(ActionType actionTypeOverride) + { + // Arrange + IServiceCollection services = new ServiceCollection(); + var validatorType = typeof(TestEntityValidator); + + // Act + services.AddActionValidator(validatorType, 0); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + + var validator = factory.GetValidator(typeof(TestEntity), actionTypeOverride); + + Assert.NotNull(validator); + Assert.True(validator is TestEntityValidator); + Assert.NotEqual((byte)0, (byte)((TestEntityValidator)validator).Action!.Value); + Assert.Equal(actionTypeOverride, ((TestEntityValidator)validator).Action); + } + [Theory] [InlineData(ActionType.Get)] [InlineData(ActionType.Create)] @@ -73,7 +101,7 @@ public void GetValidatorGeneric_OnDefinedActionValidator_ShouldReturnValidator(A var validatorType = typeof(TestEntityValidator); // Act - services.AddActionValidator(validatorType); + services.AddActionValidator(validatorType, actionType); // Assert var serviceProvider = services.BuildServiceProvider(); @@ -85,4 +113,32 @@ public void GetValidatorGeneric_OnDefinedActionValidator_ShouldReturnValidator(A Assert.True(validator is TestEntityValidator); Assert.Equal(actionType, ((TestEntityValidator)validator).Action); } + + [Theory] + [InlineData(ActionType.Get)] + [InlineData(ActionType.Create)] + [InlineData(ActionType.Update)] + [InlineData(ActionType.Patch)] + [InlineData(ActionType.Options)] + [InlineData(ActionType.Delete)] + public void GetValidatorGeneric_WithActionTypeOverride_ShouldOverrideActionType(ActionType actionTypeOverride) + { + // Arrange + IServiceCollection services = new ServiceCollection(); + var validatorType = typeof(TestEntityValidator); + + // Act + services.AddActionValidator(validatorType, 0); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + + var validator = factory.GetValidator(actionTypeOverride); + + Assert.NotNull(validator); + Assert.True(validator is TestEntityValidator); + Assert.NotEqual((byte)0, (byte)((TestEntityValidator)validator).Action!.Value); + Assert.Equal(actionTypeOverride, ((TestEntityValidator)validator).Action); + } }