Skip to content

Commit 56bda31

Browse files
update FluentValidator, remove deprecated attribute package
1 parent 54527bb commit 56bda31

File tree

8 files changed

+141
-82
lines changed

8 files changed

+141
-82
lines changed

CommandDotNet.Example/CommandDotNet.Example.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
<ProjectReference Include="..\CommandDotNet\CommandDotNet.csproj" />
1212
</ItemGroup>
1313
<ItemGroup>
14-
<PackageReference Include="FluentValidation" Version="8.0.0" />
15-
<PackageReference Include="FluentValidation.ValidatorAttribute" Version="8.0.0" />
14+
<PackageReference Include="FluentValidation" Version="10.3.4" />
1615
<PackageReference Include="Humanizer.Core" Version="2.11.10" />
1716
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
1817
</ItemGroup>

CommandDotNet.Example/Commands/Models.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Diagnostics.CodeAnalysis;
44
using CommandDotNet.Rendering;
55
using FluentValidation;
6-
using FluentValidation.Attributes;
76
using Newtonsoft.Json;
87
using Newtonsoft.Json.Serialization;
98

@@ -66,8 +65,6 @@ public class VerbosityArgs : IArgumentModel
6665
public bool Quite { get; set; }
6766
}
6867

69-
70-
[Validator(typeof(NotificationValidator))]
7168
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
7269
public class Notification : IArgumentModel
7370
{

CommandDotNet.FluentValidation/CommandDotNet.FluentValidation.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
<None Remove="output\**" />
1616
</ItemGroup>
1717
<ItemGroup>
18-
<PackageReference Include="FluentValidation" Version="8.0.0" />
19-
<PackageReference Include="FluentValidation.ValidatorAttribute" Version="8.0.0" />
18+
<PackageReference Include="FluentValidation" Version="10.3.4" />
2019
</ItemGroup>
2120
<ItemGroup>
2221
<ProjectReference Include="..\CommandDotNet\CommandDotNet.csproj" />

CommandDotNet.FluentValidation/FluentValidationMiddleware.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
1-
using System.Linq;
1+
using System;
2+
using System.Linq;
23
using System.Threading.Tasks;
34
using CommandDotNet.Execution;
5+
using FluentValidation;
6+
using FluentValidation.Internal;
7+
using FluentValidation.Results;
48

59
namespace CommandDotNet.FluentValidation
610
{
711
public static class FluentValidationMiddleware
812
{
13+
private static readonly DefaultValidatorSelector Selector = new();
14+
915
/// <summary>Enables FluentValidation for <see cref="IArgumentModel"/>s</summary>
1016
/// <param name="appRunner">the <see cref="AppRunner"/></param>
1117
/// <param name="showHelpOnError">when true, help will be display for the target command after the validation errors</param>
18+
/// <param name="validatorFactory">
19+
/// Override how validators are resolved.
20+
/// By default, checks the <see cref="CommandContext"/>.<see cref="CommandContext.DependencyResolver"/> for <see cref="IValidator{TModel}"/>
21+
/// If not found, the assembly of the model is scanned (only once).
22+
/// </param>
1223
/// <param name="resourcesOverride">
1324
/// use with <see cref="ResourcesProxy"/> to localize output this plugin.
1425
/// This does not cover FluentValidation validations.
1526
/// </param>
16-
public static AppRunner UseFluentValidation(this AppRunner appRunner, bool showHelpOnError = false,
27+
public static AppRunner UseFluentValidation(this AppRunner appRunner, bool showHelpOnError = false,
28+
Func<IArgumentModel, IValidator?>? validatorFactory = null,
1729
Resources? resourcesOverride = null)
1830
{
1931
return appRunner.Configure(c =>
@@ -27,32 +39,41 @@ public static AppRunner UseFluentValidation(this AppRunner appRunner, bool showH
2739
Resources.A = new ResourcesProxy(appRunner.AppSettings.Localize);
2840
}
2941
c.UseMiddleware(FluentValidationForModels, MiddlewareSteps.FluentValidation);
30-
c.Services.Add(new Config(showHelpOnError));
42+
c.Services.Add(new Config(showHelpOnError, validatorFactory));
3143
});
3244
}
3345

3446
private class Config
3547
{
36-
public bool ShowHelpOnError { get; }
48+
internal readonly bool ShowHelpOnError;
49+
internal readonly Func<IArgumentModel, IValidator?>? ValidatorFactory;
3750

38-
public Config(bool showHelpOnError)
51+
public Config(bool showHelpOnError, Func<IArgumentModel, IValidator?>? validatorFactory)
3952
{
4053
ShowHelpOnError = showHelpOnError;
54+
ValidatorFactory = validatorFactory;
4155
}
4256
}
4357

4458
private static Task<int> FluentValidationForModels(CommandContext ctx, ExecutionDelegate next)
4559
{
46-
var modelValidator = new ModelValidator(ctx.AppConfig.DependencyResolver);
60+
var config = ctx.Services.GetOrThrow<Config>();
4761

48-
var paramValues = ctx.InvocationPipeline
49-
.All
50-
.SelectMany(i => i.Invocation.ParameterValues.OfType<IArgumentModel>());
62+
var validatorFactory = config.ValidatorFactory ?? new ValidatorFactory(ctx).Resolve;
63+
ValidationResult? Validate(IArgumentModel argumentModel)
64+
{
65+
return validatorFactory(argumentModel)?
66+
.Validate(new ValidationContext<object>(argumentModel, new PropertyChain(), Selector));
67+
}
5168

5269
try
5370
{
71+
var paramValues = ctx.InvocationPipeline
72+
.All
73+
.SelectMany(i => i.Invocation.ParameterValues.OfType<IArgumentModel>());
74+
5475
var failureResults = paramValues
55-
.Select(model => new { model, result = modelValidator.ValidateModel(model) })
76+
.Select(model => new { model, result = Validate(model) })
5677
.Where(v => v.result is { IsValid: false })
5778
.ToList();
5879

CommandDotNet.FluentValidation/ModelValidator.cs

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using FluentValidation;
6+
7+
namespace CommandDotNet.FluentValidation
8+
{
9+
internal class ValidatorFactory
10+
{
11+
// caching to avoid multiple assembly scanning and improved perf for REPL sessions.
12+
// this could be skipped when SourceGenerators are used.
13+
private static readonly HashSet<Assembly> ScannedAssemblies = new();
14+
private static readonly Dictionary<Type, Type> ValidatorTypesByModel = new();
15+
16+
private readonly CommandContext _ctx;
17+
18+
public ValidatorFactory(CommandContext ctx)
19+
{
20+
_ctx = ctx;
21+
}
22+
23+
public IValidator? Resolve(IArgumentModel model)
24+
{
25+
var modelType = model.GetType();
26+
27+
if (!ValidatorTypesByModel.TryGetValue(modelType, out var validatorType))
28+
{
29+
var genericType = typeof(IValidator<>).MakeGenericType(modelType);
30+
if (_ctx.DependencyResolver is not null && _ctx.DependencyResolver.TryResolve(genericType, out var validator))
31+
{
32+
return validator as IValidator;
33+
}
34+
35+
if (!ScannedAssemblies.Contains(modelType.Assembly))
36+
{
37+
AssemblyScanner
38+
.FindValidatorsInAssembly(modelType.Assembly)
39+
.ForEach(r =>
40+
{
41+
var validatorModelType = r.InterfaceType.GenericTypeArguments.First();
42+
ValidatorTypesByModel.Add(validatorModelType, r.ValidatorType);
43+
if (validatorModelType == modelType)
44+
{
45+
validatorType = r.ValidatorType;
46+
}
47+
});
48+
49+
ScannedAssemblies.Add(modelType.Assembly);
50+
}
51+
}
52+
53+
if (validatorType is null)
54+
{
55+
return null;
56+
}
57+
58+
try
59+
{
60+
return Activator.CreateInstance(validatorType) as IValidator;
61+
}
62+
catch (Exception e)
63+
{
64+
var msg = Resources.A.Error_Could_not_create_instance_of(validatorType.Name);
65+
throw new InvalidValidatorException(msg, e);
66+
}
67+
}
68+
}
69+
}

CommandDotNet.Tests/CommandDotNet.FluentValidation/ModelValidationTests.cs

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using CommandDotNet.TestTools;
55
using CommandDotNet.TestTools.Scenarios;
66
using FluentValidation;
7-
using FluentValidation.Attributes;
87
using Xunit;
98
using Xunit.Abstractions;
109

@@ -54,8 +53,8 @@ public void Exec_WithInvalidData_PrintsValidationError()
5453
ExitCode = 2,
5554
Output = @"'Person' is invalid
5655
'Id' must be greater than '0'.
57-
'Name' should not be empty.
58-
'Email' should not be empty.
56+
'Name' must not be empty.
57+
'Email' must not be empty.
5958
"
6059
}
6160
});
@@ -77,8 +76,8 @@ public void Exec_WithInvalidData_PrintsValidationError_UsingValidatorFromDI()
7776
ExitCode = 2,
7877
Output = @"'Person' is invalid
7978
'Id' must be greater than '0'.
80-
'Name' should not be empty.
81-
'Email' should not be empty.
79+
'Name' must not be empty.
80+
'Email' must not be empty.
8281
"
8382
}
8483
});
@@ -100,6 +99,26 @@ public void Exec_WithValidData_Succeeds()
10099
});
101100
}
102101

102+
[Fact]
103+
public void Exec_WithValidatorFactory_UsesValidatorFromFactory()
104+
{
105+
new AppRunner<App>()
106+
.UseFluentValidation(validatorFactory: m => new PersonValidator("lala"))
107+
.Verify(new Scenario
108+
{
109+
When = { Args = "Save" },
110+
Then =
111+
{
112+
ExitCode = 2,
113+
Output = @"'Person' is invalid
114+
lala
115+
lala
116+
lala
117+
"
118+
}
119+
});
120+
}
121+
103122
[Fact]
104123
public void Exec_WhenNoDependencyResolver_ValidatorIsCreated()
105124
{
@@ -177,8 +196,8 @@ public void Exec_IfShowHelpOnError_ShowsHelpAfterValidationMessage()
177196
{
178197
@"'Person' is invalid
179198
'Id' must be greater than '0'.
180-
'Name' should not be empty.
181-
'Email' should not be empty.
199+
'Name' must not be empty.
200+
'Email' must not be empty.
182201
183202
Usage: testhost.dll Save <Id> <Name> <Email>"
184203
}
@@ -196,39 +215,41 @@ public void InvalidSave(InvalidPerson person)
196215
{
197216
}
198217
}
199-
200-
[Validator(typeof(PersonValidator))]
201-
private class Person : IArgumentModel
218+
219+
public class Person : IArgumentModel
202220
{
203221
[Operand] public int Id { get; set; }
204222
[Operand] public string Name { get; set; } = null!;
205223
[Operand] public string Email { get; set; } = null!;
206224
}
207225

208-
private class PersonValidator : AbstractValidator<Person>
226+
public class PersonValidator : AbstractValidator<Person>
209227
{
210228
public PersonValidator()
211229
{
212230
RuleFor(x => x.Id).GreaterThan(0);
213231
RuleFor(x => x.Name).NotEmpty();
214232
RuleFor(x => x.Email).NotEmpty().EmailAddress();
215233
}
234+
public PersonValidator(string message)
235+
{
236+
RuleFor(x => x.Id).GreaterThan(0).WithMessage(message);
237+
RuleFor(x => x.Name).NotEmpty().WithMessage(message);
238+
RuleFor(x => x.Email).NotEmpty().WithMessage(message).EmailAddress().WithMessage(message);
239+
}
216240
}
217-
218-
[Validator(typeof(InvalidPersonValidator))]
219-
private class InvalidPerson : IArgumentModel
241+
242+
public class InvalidPerson : IArgumentModel
220243
{
221244
[Operand]
222245
public int Id { get; set; }
223246
}
224247

225-
private class InvalidPersonValidator : AbstractValidator<Person>
248+
public class InvalidPersonValidator : AbstractValidator<InvalidPerson>
226249
{
227250
public InvalidPersonValidator(bool nonDefaultCtor)
228251
{
229252
RuleFor(x => x.Id).GreaterThan(0);
230-
RuleFor(x => x.Name).NotEmpty();
231-
RuleFor(x => x.Email).NotEmpty().EmailAddress();
232253
}
233254
}
234255
}

CommandDotNet.Tests/CommandDotNet.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66
<ItemGroup>
77
<PackageReference Include="FluentAssertions" Version="6.2.0" />
8-
<PackageReference Include="FluentValidation" Version="8.0.0" />
8+
<PackageReference Include="FluentValidation" Version="10.3.4" />
99
<PackageReference Include="JsonDiffPatch.Net" Version="2.3.0" />
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
1111
<PackageReference Include="Namotion.Reflection" Version="2.0.5" />

0 commit comments

Comments
 (0)