Skip to content

Commit

Permalink
Rework ActionValidator
Browse files Browse the repository at this point in the history
  • Loading branch information
YuriyDurov committed Dec 20, 2023
1 parent 769d1b9 commit 58d2646
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 75 deletions.
10 changes: 10 additions & 0 deletions Miscellaneous.sln
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenTelemetry", "OpenTeleme
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Probes", "src\BitzArt.Probes\BitzArt.Probes.csproj", "{014EF2B0-4D13-4A35-ADE7-006689C5EC3D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FluentValidation", "FluentValidation", "{B8DD59B3-F647-4761-97AD-EEBB972B8557}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.FluentValidation.Extensions.UnitTests", "tests\FluentValidation\BitzArt.FluentValidation.Extensions.UnitTests\BitzArt.FluentValidation.Extensions.UnitTests.csproj", "{9FDF435B-8ACD-48D5-986A-1B526D7F6E38}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -94,6 +98,10 @@ Global
{014EF2B0-4D13-4A35-ADE7-006689C5EC3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{014EF2B0-4D13-4A35-ADE7-006689C5EC3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{014EF2B0-4D13-4A35-ADE7-006689C5EC3D}.Release|Any CPU.Build.0 = Release|Any CPU
{9FDF435B-8ACD-48D5-986A-1B526D7F6E38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9FDF435B-8ACD-48D5-986A-1B526D7F6E38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FDF435B-8ACD-48D5-986A-1B526D7F6E38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FDF435B-8ACD-48D5-986A-1B526D7F6E38}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -114,6 +122,8 @@ Global
{A258E652-1BB2-4A90-9ED4-6248A09FD379} = {E47B8516-234F-4D06-957C-B24278490706}
{E47B8516-234F-4D06-957C-B24278490706} = {2638AACB-7314-4962-87BE-93876EEED788}
{014EF2B0-4D13-4A35-ADE7-006689C5EC3D} = {2638AACB-7314-4962-87BE-93876EEED788}
{B8DD59B3-F647-4761-97AD-EEBB972B8557} = {6C59FADC-BAEA-46B7-A69F-DC2AA0A124D6}
{9FDF435B-8ACD-48D5-986A-1B526D7F6E38} = {B8DD59B3-F647-4761-97AD-EEBB972B8557}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {49BCA0AC-45AF-4DE6-B691-30A0F2959200}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FluentValidation</RootNamespace>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FluentValidation</RootNamespace>

<PackageId>BitzArt.FluentValidation.Extensions</PackageId>
<Authors>BitzArt</Authors>
<Description>Extensions for FluentValidation</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/BitzArt/Miscellaneous</RepositoryUrl>
<PackageProjectUrl>https://github.com/BitzArt/Miscellaneous</PackageProjectUrl>
</PropertyGroup>
<PackageId>BitzArt.FluentValidation.Extensions</PackageId>
<Authors>BitzArt</Authors>
<Description>Extensions for FluentValidation</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/BitzArt/Miscellaneous</RepositoryUrl>
<PackageProjectUrl>https://github.com/BitzArt/Miscellaneous</PackageProjectUrl>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.5.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.8.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>BitzArt.FluentValidation.Extensions.UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace FluentValidation;

public enum ActionType : byte
{
Get = 1,
Create = 2,
Update = 3,
Patch = 4,
Options = 5,
Delete = 6
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;

namespace FluentValidation;

public static class AddActionValidatorsExtension
{
public static IServiceCollection AddActionValidatorsFromAssemblyContaining<TAssemblyPointer>(this IServiceCollection services, Func<IServiceProvider, ActionType> getActionType)
=> services.AddActionValidatorsFromAssemblyContaining(typeof(TAssemblyPointer), getActionType);

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

public static IServiceCollection AddActionValidatorsFromAssembly(this IServiceCollection services, Assembly assembly, Func<IServiceProvider, ActionType> getActionType)
{
var validators = assembly
.DefinedTypes
.Where(x => x.BaseType is not null && x.BaseType.IsGenericType)
.Where(x => x.BaseType!.GetGenericTypeDefinition() == typeof(ActionValidator<>));

foreach (var validator in validators) services.AddActionValidator(validator, getActionType);

return services;
}

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

var baseClassDefinition = validatorType.BaseType!;
var validationObjectType = baseClassDefinition.GetGenericArguments().Single();

var registrationType = typeof(IValidator<>).MakeGenericType(validationObjectType);

services.AddTransient(validatorType);
services.AddScoped(registrationType, x =>
{
var validator = x.GetRequiredService(validatorType);
(validator as IActionValidator)!.ActionType = getActionType.Invoke(x);
return validator;
});

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace FluentValidation;

internal interface IActionValidator
{
public ActionType ActionType { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
namespace FluentValidation;

public abstract class ActionValidator<T> : JsonValidator<T>
public abstract class ActionValidator<T> : AbstractValidator<T>, IActionValidator
{
protected readonly ActionType ActionType;

protected ActionValidator(ActionType actionType)
private ActionType? _actionType;
public ActionType ActionType
{
ActionType = actionType;
get => _actionType is not null ?
_actionType!.Value :
throw new ArgumentException("ActionType is not configured for this ActionValidator.");

set => _actionType = value;
}

public IConditionBuilder When(ActionType actionType, Action action)
=> When(x => ActionType == actionType, action);

public IConditionBuilder Unless(ActionType actionType, Action action)
=> Unless(x => ActionType == actionType, action);
}

public enum ActionType : byte
{
Get = 1,
Create = 2,
Update = 3,
Patch = 4,
Options = 5,
Delete = 6
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>FluentValidation</RootNamespace>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\FluentValidation\BitzArt.FluentValidation.Extensions\BitzArt.FluentValidation.Extensions.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace FluentValidation;

public class TestEntity
{
public string? Name { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace FluentValidation.Models;

public class TestEntityValidator : ActionValidator<TestEntity>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace FluentValidation;

internal class TestOptions(ActionType actionType)
{
public ActionType ActionType { get; set; } = actionType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using FluentValidation.Models;
using Microsoft.Extensions.DependencyInjection;

namespace FluentValidation;

public class AddActionValidatorsExtensionTests
{
[Theory]
[InlineData(ActionType.Get)]
[InlineData(ActionType.Create)]
[InlineData(ActionType.Patch)]
[InlineData(ActionType.Update)]
public void AddActionValidator_WithFlatActionType_AddsValidator(ActionType actionType)
{
IServiceCollection services = new ServiceCollection();
var validatorType = typeof(TestEntityValidator);

services.AddActionValidator(validatorType, x => actionType);

var serviceProvider = services.BuildServiceProvider();

var validator = serviceProvider.GetService<IValidator<TestEntity>>();

Assert.NotNull(validator);
Assert.True(validator is TestEntityValidator);
var validatorCasted = (TestEntityValidator)validator;
Assert.Equal(actionType, validatorCasted.ActionType);
}

[Theory]
[InlineData(ActionType.Get)]
[InlineData(ActionType.Create)]
[InlineData(ActionType.Patch)]
[InlineData(ActionType.Update)]
public void AddActionValidator_WithActionTypeFromDi_AddsValidator(ActionType actionType)
{
IServiceCollection services = new ServiceCollection();
var validatorType = typeof(TestEntityValidator);

var options = new TestOptions(actionType);
services.AddSingleton(options);
services.AddActionValidator(validatorType, x => x.GetRequiredService<TestOptions>().ActionType);

var serviceProvider = services.BuildServiceProvider();

var validator = serviceProvider.GetService<IValidator<TestEntity>>();

Assert.NotNull(validator);
Assert.True(validator is TestEntityValidator);
var validatorCasted = (TestEntityValidator)validator;
Assert.Equal(actionType, validatorCasted.ActionType);
}

[Fact]
public void AddActionValidatorsFromAssembly_WithFlatActionType_AddsTestEntityValidator()
{
IServiceCollection services = new ServiceCollection();
var validatorType = typeof(TestEntityValidator);
var actionType = ActionType.Get;

services.AddActionValidatorsFromAssembly(typeof(AddActionValidatorsExtensionTests).Assembly, x => actionType);

var serviceProvider = services.BuildServiceProvider();

var validator = serviceProvider.GetService<IValidator<TestEntity>>();

Assert.NotNull(validator);
Assert.True(validator is TestEntityValidator);
var validatorCasted = (TestEntityValidator)validator;
Assert.Equal(actionType, validatorCasted.ActionType);
}

[Fact]
public void AddActionValidatorsFromAssemblyContaining_WithFlatActionType_AddsTestEntityValidator()
{
IServiceCollection services = new ServiceCollection();
var validatorType = typeof(TestEntityValidator);
var actionType = ActionType.Get;

services.AddActionValidatorsFromAssemblyContaining(typeof(AddActionValidatorsExtensionTests), x => actionType);

var serviceProvider = services.BuildServiceProvider();

var validator = serviceProvider.GetService<IValidator<TestEntity>>();

Assert.NotNull(validator);
Assert.True(validator is TestEntityValidator);
var validatorCasted = (TestEntityValidator)validator;
Assert.Equal(actionType, validatorCasted.ActionType);
}

[Fact]
public void AddActionValidatorsFromAssemblyContainingGeneric_WithFlatActionType_AddsTestEntityValidator()
{
IServiceCollection services = new ServiceCollection();
var validatorType = typeof(TestEntityValidator);
var actionType = ActionType.Get;

services.AddActionValidatorsFromAssemblyContaining<AddActionValidatorsExtensionTests>(x => actionType);

var serviceProvider = services.BuildServiceProvider();

var validator = serviceProvider.GetService<IValidator<TestEntity>>();

Assert.NotNull(validator);
Assert.True(validator is TestEntityValidator);
var validatorCasted = (TestEntityValidator)validator;
Assert.Equal(actionType, validatorCasted.ActionType);
}
}

0 comments on commit 58d2646

Please sign in to comment.