Skip to content

Commit

Permalink
ActionType override (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
YuriyDurov authored Oct 22, 2024
1 parent 154bafc commit 4d634c8
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>FluentValidation</RootNamespace>

<PackageId>BitzArt.FluentValidation.Extensions</PackageId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,32 @@ namespace FluentValidation;

public static class AddActionValidatorExtensions
{
public static IServiceCollection AddActionValidatorsFromAssemblyContaining<TAssemblyPointer>(this IServiceCollection services, Func<IServiceProvider, ActionType>? actionTypeResolver = null)
public static IServiceCollection AddActionValidatorsFromAssemblyContaining<TAssemblyPointer>(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<TAssemblyPointer>(this IServiceCollection services, Func<IServiceProvider, ActionType> actionTypeResolver)
=> services.AddActionValidatorsFromAssemblyContaining(typeof(TAssemblyPointer), actionTypeResolver);

public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, Type type, Func<IServiceProvider, ActionType>? actionTypeResolver = null)
public static IServiceCollection AddActionValidatorsFromAssemblyContaining(this IServiceCollection services, Type type, Func<IServiceProvider, ActionType> actionTypeResolver)
=> services.AddActionValidatorsFromAssembly(type.Assembly, actionTypeResolver);

public static IServiceCollection AddActionValidatorsFromAssembly(this IServiceCollection services, Assembly assembly, Func<IServiceProvider, ActionType>? 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<IServiceProvider, ActionType> actionTypeResolver)
{
var validators = assembly
.DefinedTypes
Expand All @@ -25,36 +44,81 @@ public static IServiceCollection AddActionValidatorsFromAssembly(this IServiceCo
return services;
}

public static IServiceCollection AddActionValidator<TValidator>(this IServiceCollection services, Func<IServiceProvider, ActionType>? actionTypeResolver = null)
=> services.AddActionValidator(typeof(TValidator), actionTypeResolver);
public static IServiceCollection AddActionValidator<TValidator>(this IServiceCollection services, ActionType actionType)
=> services.AddActionValidator(typeof(TValidator), actionType);

public static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Func<IServiceProvider, ActionType>? 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<IActionValidatorFactory>(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<T>");

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<TValidator>(this IServiceCollection services, Func<IServiceProvider, ActionType> actionTypeResolver)
=> services.AddActionValidator(typeof(TValidator), actionTypeResolver);

public static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Func<IServiceProvider, ActionType> actionTypeResolver)
{
if (validatorType is null) throw new ArgumentException($"{nameof(validatorType)} must not be null");

Func<IServiceProvider, ActionType?> finalActionTypeResolver = actionTypeResolver is not null ?
x => actionTypeResolver(x) :
x => null;
services.TryAddScoped<IActionValidatorFactory>(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<T>");

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<Type> registrationInterfaces =
[
typeof(IValidator<>).MakeGenericType(validationObjectType),
typeof(IActionValidator<>).MakeGenericType(validationObjectType)
];

foreach (var registrationInterface in registrationInterfaces)
{
services.AddScoped(registrationInterface, x =>
{
var factory = x.GetRequiredService<IActionValidatorFactory>();
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<IServiceProvider, ActionType?> getActionType)
private static IServiceCollection AddActionValidator(this IServiceCollection services, Type validatorType, Type validationObjectType, Func<IServiceProvider, ActionType> getActionType, ValidatorMapEntry mapEntry)
{
List<Type> registrationInterfaces =
[
Expand All @@ -67,12 +131,12 @@ private static IServiceCollection AddActionValidator(this IServiceCollection ser
services.AddScoped(registrationInterface, x =>
{
var factory = x.GetRequiredService<IActionValidatorFactory>();
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
namespace FluentValidation;

/// <inheritdoc cref="IActionValidator"/>
/// <typeparam name="T">The type of object being validated.</typeparam>
public interface IActionValidator<T> : IValidator<T>, IActionValidator
{

}

/// <summary>
/// An <see cref="IValidator"/> that can be used to validate an object based on the action being performed.
/// </summary>
public interface IActionValidator
{
/// <summary>
/// Current action type.
/// </summary>
public ActionType? Action { get; internal set; }
}
Original file line number Diff line number Diff line change
@@ -1,47 +1,52 @@
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;

namespace FluentValidation;

internal class ActionValidatorFactory(IServiceProvider serviceProvider) : IActionValidatorFactory
{
internal static ConcurrentDictionary<Type, Type> ValidatorTypeMap = [];

internal IServiceProvider _serviceProvider = serviceProvider;

private ActionType? _actionType = null;

public IActionValidator<T> GetValidator<T>(ActionType actionType)
=> (IActionValidator<T>)GetValidatorInternal(typeof(IActionValidator<T>), definedActionType: actionType);
public IActionValidator<T> GetValidator<T>(ActionType? actionType = null)
=> (IActionValidator<T>)GetValidatorInternal(typeof(IActionValidator<T>), 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<IServiceProvider, ActionType?>? 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<ValidatorMapEntry>(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)
{
validator.Action = _actionType!.Value;
return validator;
}

if (actionTypeResolver is not null)
if (validatorInfo.ActionTypeResolver is not null)
{
validator.Action = actionTypeResolver(_serviceProvider);
validator.Action = validatorInfo.ActionTypeResolver.Invoke(_serviceProvider);
return validator;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ namespace FluentValidation;

public interface IActionValidatorFactory
{
public IActionValidator<T> GetValidator<T>(ActionType actionType);
public IActionValidator<T> GetValidator<T>(ActionType? actionType = null);

public IActionValidator GetValidator(Type objectType, ActionType actionType);
public IActionValidator GetValidator(Type objectType, ActionType? actionType = null);

internal IActionValidator GetValidatorInternal(Type validatorType, Func<IServiceProvider, ActionType?> getActionType, ActionType? actionType = null);
internal IActionValidator GetValidatorInternal(Type validatorType, ActionType? actionType = null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace FluentValidation;

internal record ValidatorMapEntry
{
public Type ImplementationType { get; }
public Func<IServiceProvider, ActionType>? ActionTypeResolver { get; }
public ActionType? DefinedActionType { get; }

public ValidatorMapEntry(Type ImplementationType, Func<IServiceProvider, ActionType>? 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;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace FluentValidation;

/// <inheritdoc/>
public abstract class ActionValidator<T> : AbstractValidator<T>, IActionValidator<T>
{
public ActionType? Action { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ public void GetValidator_OnValidatorHierarchy_ShouldSetActionTypeForAllValidator
{
// Arrange
var services = new ServiceCollection();
services.AddActionValidator<TestHierarchyParentValidator>();
services.AddActionValidator<TestDescriptionValidator>();
services.AddActionValidator<TestHierarchyParentValidator>(ActionType.Get);
services.AddActionValidator<TestDescriptionValidator>(ActionType.Get);

var serviceProvider = services.BuildServiceProvider();
var factory = serviceProvider.GetRequiredService<IActionValidatorFactory>();
Expand All @@ -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<TestHierarchyParentValidator>(ActionType.Get);
services.AddActionValidator<TestDescriptionValidator>(ActionType.Create);

var serviceProvider = services.BuildServiceProvider();
var factory = serviceProvider.GetRequiredService<IActionValidatorFactory>();

// Act
var validator = factory.GetValidator<TestHierarchyParent>();

// 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<TestHierarchyParentValidator>(ActionType.Get);
services.AddActionValidator<TestDescriptionValidator>(ActionType.Create);

var serviceProvider = services.BuildServiceProvider();
var factory = serviceProvider.GetRequiredService<IActionValidatorFactory>();

// Act
var validator = factory.GetValidator<TestHierarchyParent>(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);
}
}
Loading

0 comments on commit 4d634c8

Please sign in to comment.