Skip to content

Commit 52fac03

Browse files
authored
Merge pull request #10 from Liero/feature/v1.1
Feature/v1.1
2 parents eff3aa1 + c57e45f commit 52fac03

6 files changed

Lines changed: 184 additions & 94 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# FluentValidation
2-
A library for using FluentValidation with Blazor that supports **async validation, severity levels** and more.
2+
A battle tested library for using FluentValidation with Blazor that supports **async validation, severity levels** and more.
33

44
For introduction, see this [blog post](https://blog.vyvojari.dev/advanced-validation-in-blazor-using-fluentvalidation/)
55

@@ -88,7 +88,7 @@ or per FluentValidationValidator component
8888
}
8989
```
9090

91-
See [DefaultValidatorFactory.cs](vNext.BlazorComponents.FluentValidation\DefaultValidatorFactory.cs) for more info.
91+
See [DefaultValidatorFactory.cs](vNext.BlazorComponents.FluentValidation/DefaultValidatorFactory.cs) for more info.
9292

9393
## Validating Complex Models
9494

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#nullable enable
2+
3+
using FluentValidation;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
9+
namespace vNext.BlazorComponents.FluentValidation
10+
{
11+
public class AssemblyScannerValidatorFactory : IValidatorFactory
12+
{
13+
static readonly List<string> ScannedAssembly = new();
14+
static readonly List<AssemblyScanner.AssemblyScanResult> AssemblyScanResults = new();
15+
16+
public IValidator? CreateValidator(ValidatorFactoryContext context)
17+
{
18+
19+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName)))
20+
{
21+
try
22+
{
23+
AssemblyScanResults.AddRange(AssemblyScanner.FindValidatorsInAssembly(assembly, false));
24+
}
25+
catch (Exception)
26+
{
27+
}
28+
29+
ScannedAssembly.Add(assembly.FullName!);
30+
}
31+
32+
33+
Type modelType = context.Model.GetType();
34+
35+
static int CommonPrefixLength(string? str1, string? str2) =>
36+
(str1 ?? string.Empty).TakeWhile((c, i) => str2?.Length < i && c == str2[c]).Count();
37+
38+
39+
Type? modelValidatorType = AssemblyScanResults.Where(i => context.ValidatorType.IsAssignableFrom(i.InterfaceType))
40+
.OrderByDescending(e => e.ValidatorType.Assembly == modelType.Assembly) //prefer current assebly
41+
.ThenByDescending(e => CommonPrefixLength(e.ValidatorType.FullName, modelType.FullName)) //prefer similar namespace
42+
.ThenBy(e => e.ValidatorType.Namespace?.Length)
43+
.FirstOrDefault()?.ValidatorType;
44+
45+
if (modelValidatorType != null)
46+
{
47+
return (IValidator)ActivatorUtilities.CreateInstance(context.ServiceProvider, modelValidatorType);
48+
}
49+
return null;
50+
}
51+
}
52+
}
Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,35 @@
11
#nullable enable
22

33
using FluentValidation;
4-
using Microsoft.Extensions.DependencyInjection;
5-
using System;
6-
using System.Collections.Generic;
7-
using System.Linq;
84

95
namespace vNext.BlazorComponents.FluentValidation
106
{
7+
118
/// <summary>
129
/// Returns validator from ServiceProvider or by scanning assemlies
1310
/// </summary>
1411
public class DefaultValidatorFactory : IValidatorFactory
1512
{
16-
static readonly List<string> ScannedAssembly = new();
17-
static readonly List<AssemblyScanner.AssemblyScanResult> AssemblyScanResults = new();
13+
AssemblyScannerValidatorFactory? _assemblyScannerValidatorFactory;
14+
ServiceProviderValidatorFactory? _serviceProviderValidatorFactory;
1815

1916
public bool DisableAssemblyScanning { get; set; }
17+
public bool DisableServiceProvider { get; set; }
2018

2119
public IValidator? CreateValidator(ValidatorFactoryContext context)
2220
{
23-
if (context.ServiceProvider.GetService(context.ValidatorType) is IValidator validator)
21+
IValidator? result = null;
22+
if (!DisableServiceProvider)
2423
{
25-
return validator;
24+
_serviceProviderValidatorFactory ??= new();
25+
result = _serviceProviderValidatorFactory.CreateValidator(context);
2626
}
2727
if (!DisableAssemblyScanning)
2828
{
29-
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(i => i.FullName is not null && !ScannedAssembly.Contains(i.FullName)))
30-
{
31-
try
32-
{
33-
AssemblyScanResults.AddRange(AssemblyScanner.FindValidatorsInAssembly(assembly, false));
34-
}
35-
catch (Exception)
36-
{
37-
}
38-
39-
ScannedAssembly.Add(assembly.FullName!);
40-
}
41-
42-
43-
Type modelType = context.Model.GetType();
44-
45-
static int CommonPrefixLength(string? str1, string? str2) =>
46-
(str1 ?? string.Empty).TakeWhile((c, i) => str2?.Length < i && c == str2[c]).Count();
47-
48-
49-
Type? modelValidatorType = AssemblyScanResults.Where(i => context.ValidatorType.IsAssignableFrom(i.InterfaceType))
50-
.OrderByDescending(e => e.ValidatorType.Assembly == modelType.Assembly) //prefer current assebly
51-
.ThenByDescending(e => CommonPrefixLength(e.ValidatorType.FullName, modelType.FullName)) //prefer similar namespace
52-
.ThenBy(e => e.ValidatorType.Namespace?.Length)
53-
.FirstOrDefault()?.ValidatorType;
54-
55-
if (modelValidatorType != null)
56-
{
57-
return (IValidator)ActivatorUtilities.CreateInstance(context.ServiceProvider, modelValidatorType);
58-
}
29+
_assemblyScannerValidatorFactory ??= new();
30+
result ??= _assemblyScannerValidatorFactory.CreateValidator(context);
5931
}
60-
61-
return null;
32+
return result;
6233
}
6334
}
6435
}

vNext.BlazorComponents.FluentValidation/FluentValidationValidator.cs

Lines changed: 97 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
using Microsoft.AspNetCore.Components;
99
using Microsoft.AspNetCore.Components.Forms;
1010
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
1112
using System;
1213
using System.Collections.Generic;
1314
using System.Linq;
15+
using System.Linq.Expressions;
1416
using System.Threading.Tasks;
15-
using static FluentValidation.AssemblyScanner;
1617

1718
namespace vNext.BlazorComponents.FluentValidation
1819
{
@@ -28,27 +29,61 @@ public class FluentValidationValidator : ComponentBase
2829

2930
[Parameter] public IValidator? Validator { get; set; }
3031

32+
/// <summary>
33+
/// Minimum severity to be treated as an error.
34+
/// For example, if Severity == Error, then any validation messages with Severity warning will be ignored
35+
/// </summary>
3136
[Parameter] public Severity Severity { get; set; } = Severity.Info;
37+
38+
/// <summary>
39+
/// Determines how validator are resolved for <see cref="EditContext.Model"/>, or <see cref="FieldIdentifier.Model"/> in case of complex models
40+
/// </summary>
41+
/// <seealso cref="DefaultValidatorFactory"/>
3242
[Parameter] public IValidatorFactory ValidatorFactory { get; set; } = default!;
3343
[Parameter] public Action<ValidationStrategy<object>>? ValidationStrategyOptions { get; set; }
3444

3545
public EditContext EditContext => CurrentEditContext ?? throw new InvalidOperationException($"{nameof(FluentValidationValidator)} requires a cascading " +
3646
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidationValidator)} " +
3747
$"inside an {nameof(EditForm)}.");
3848

49+
public ValidationMessageStore ValidationMessageStore => _validationMessageStore ?? throw new InvalidOperationException("FluentValidationValidator not initialized.");
3950

4051
public virtual async Task<bool> Validate()
4152
{
4253
return await EditContext.ValidateAsync();
4354
}
4455

56+
public virtual Task<ValidationResult> ValidateModelAsync(bool updateValidationState = true)
57+
=> ValidateModel(ValidationMessageStore, updateValidationState);
58+
59+
public virtual Task<ValidationResult> ValidateFieldAsync(Expression<Func<object>> accessor, bool updateValidationState = true)
60+
=> ValidateFieldAsync(FieldIdentifier.Create(accessor), updateValidationState);
61+
62+
public virtual async Task<ValidationResult> ValidateFieldAsync(FieldIdentifier fieldIdentifier, bool updateValidationState = true)
63+
=> await ValidateField(ValidationMessageStore, fieldIdentifier, updateValidationState);
64+
65+
4566
public virtual void ClearMessages()
4667
{
47-
_validationMessageStore!.Clear();
68+
_validationMessageStore?.Clear();
4869
validationResults?.Errors?.Clear();
4970
EditContext.NotifyValidationStateChanged();
5071
}
5172

73+
/// <summary>
74+
/// get validator for <see cref="FieldIdentifier.Model"/> of <paramref name="fieldIdentifier"/>.
75+
/// If <paramref name="fieldIdentifier"/> is default, return <see cref="EditContext.Model"/>
76+
/// </summary>
77+
/// <seealso cref="ValidatorFactory"/>
78+
public virtual IValidator? ResolveValidator(FieldIdentifier fieldIdentifier = default)
79+
{
80+
if (EditContext == null) throw new InvalidOperationException("EditContext is null");
81+
object model = fieldIdentifier.Model ?? EditContext.Model;
82+
Type interfaceValidatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
83+
var ctx = new ValidatorFactoryContext(interfaceValidatorType, ServiceProvider, EditContext, model, fieldIdentifier);
84+
return ValidatorFactory.CreateValidator(ctx);
85+
}
86+
5287
protected override void OnInitialized()
5388
{
5489
ValidatorFactory ??= ServiceProvider.GetService<IValidatorFactory>() ?? new DefaultValidatorFactory();
@@ -57,10 +92,10 @@ protected override void OnInitialized()
5792
EditContext.Properties["ValidationMessageStore"] = _validationMessageStore;
5893

5994
EditContext.OnValidationRequested +=
60-
async (sender, eventArgs) => await ValidateModel(_validationMessageStore);
95+
async (sender, eventArgs) => await ValidateModel(ValidationMessageStore, true);
6196

6297
EditContext.OnFieldChanged +=
63-
async (sender, eventArgs) => await ValidateField(_validationMessageStore, eventArgs.FieldIdentifier);
98+
async (sender, eventArgs) => await ValidateField(ValidationMessageStore, eventArgs.FieldIdentifier, true);
6499
}
65100

66101
protected virtual string MapValidationFailureToMessage(ValidationFailure failure, ValidationResult result, ValidationContext<object> validationContext)
@@ -72,62 +107,79 @@ protected virtual string MapValidationFailureToMessage(ValidationFailure failure
72107
return $"[{failure.Severity}] {failure.ErrorMessage}";
73108
}
74109

75-
protected virtual IValidator? GetValidator(FieldIdentifier fieldIdentifier = default)
110+
protected virtual async Task<ValidationResult> ValidateModel(ValidationMessageStore messages, bool updateValidationState)
76111
{
77-
if (EditContext == null) throw new InvalidOperationException("EditContext is null");
78-
object model = fieldIdentifier.Model ?? EditContext.Model;
79-
Type interfaceValidatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
80-
var ctx = new ValidatorFactoryContext(interfaceValidatorType, ServiceProvider, EditContext, model, fieldIdentifier);
81-
return ValidatorFactory.CreateValidator(ctx);
82-
}
83-
84-
85-
protected virtual async Task ValidateModel(ValidationMessageStore messages)
86-
{
87-
if (EditContext == null) throw new InvalidOperationException("EditContext is null");
88-
89-
IValidator? validator = GetValidator();
112+
IValidator? validator = ResolveValidator();
90113

91114
if (validator is not null)
92115
{
93116
ValidationContext<object> context = CreateValidationContext(validator);
94117

95-
Task<ValidationResult> validateAsyncTask = validator.ValidateAsync(context);
96-
EditContext.Properties[EditContextExtensions.PROPERTY_VALIDATEASYNCTASK] = validateAsyncTask;
97-
validationResults = await validateAsyncTask;
98118

99-
messages.Clear();
100-
foreach (var failure in validationResults.Errors.Where(f => f.Severity <= Severity))
119+
Task<ValidationResult> validateAsyncTask = validator.ValidateAsync(context);
120+
if (updateValidationState)
101121
{
102-
var fieldIdentifier = ToFieldIdentifier(EditContext, failure.PropertyName);
103-
string errorMessage = MapValidationFailureToMessage(failure, validationResults, context);
104-
messages.Add(fieldIdentifier, errorMessage);
122+
EditContext.Properties[EditContextExtensions.PROPERTY_VALIDATEASYNCTASK] = validateAsyncTask;
105123
}
124+
var validationResults = await validateAsyncTask;
125+
if (updateValidationState)
126+
{
127+
this.validationResults = validationResults;
128+
messages.Clear();
129+
foreach (var failure in validationResults.Errors.Where(f => f.Severity <= Severity))
130+
{
131+
try
132+
{
133+
var fieldIdentifier = ToFieldIdentifier(EditContext, failure.PropertyName);
134+
string errorMessage = MapValidationFailureToMessage(failure, validationResults, context);
135+
messages.Add(fieldIdentifier, errorMessage);
136+
}
137+
catch (InvalidOperationException ex)
138+
{
139+
ServiceProvider.GetService<ILogger<FluentValidationValidator>>()?.LogError(ex, $"An error occured while parsing ValidationFailure(PropertyName={failure.PropertyName})");
140+
}
141+
}
106142

107-
EditContext.NotifyValidationStateChanged();
143+
EditContext.NotifyValidationStateChanged();
144+
}
145+
return validationResults;
146+
}
147+
else
148+
{
149+
var emptyValidationResult = new ValidationResult();
150+
if (updateValidationState)
151+
{
152+
EditContext.Properties[EditContextExtensions.PROPERTY_VALIDATEASYNCTASK] = Task.FromResult(emptyValidationResult);
153+
}
154+
return emptyValidationResult;
108155
}
109156
}
110157

111-
protected virtual async Task ValidateField(ValidationMessageStore messages, FieldIdentifier fieldIdentifier)
158+
protected virtual async Task<ValidationResult> ValidateField(ValidationMessageStore messages, FieldIdentifier fieldIdentifier, bool updateValidationState)
112159
{
113160
var properties = new[] { fieldIdentifier.FieldName };
114161

115-
IValidator? validator = GetValidator(fieldIdentifier);
162+
IValidator? validator = ResolveValidator(fieldIdentifier);
116163

117164
if (validator is not null)
118165
{
119166
var context = CreateValidationContext(validator, fieldIdentifier);
120167
var validationResults = await validator.ValidateAsync(context);
121168

122-
messages.Clear(fieldIdentifier);
123-
var fieldMessages = validationResults.Errors
124-
.Where(failure => failure.Severity <= Severity)
125-
.Select(failure => MapValidationFailureToMessage(failure, validationResults, context));
169+
if (updateValidationState)
170+
{
171+
messages.Clear(fieldIdentifier);
172+
var fieldMessages = validationResults.Errors
173+
.Where(failure => failure.Severity <= Severity)
174+
.Select(failure => MapValidationFailureToMessage(failure, validationResults, context));
126175

127-
messages.Add(fieldIdentifier, fieldMessages);
176+
messages.Add(fieldIdentifier, fieldMessages);
177+
EditContext.NotifyValidationStateChanged();
178+
}
128179

129-
EditContext.NotifyValidationStateChanged();
180+
return validationResults;
130181
}
182+
return new ValidationResult();
131183
}
132184

133185
protected virtual ValidationContext<object> CreateValidationContext(IValidator validator, FieldIdentifier fieldIdentifier = default)
@@ -145,12 +197,12 @@ protected virtual void ConfigureValidationStrategy(ValidationStrategy<object> op
145197
{
146198
ValidationStrategyOptions(options);
147199
}
148-
200+
149201
if (fieldIdentifier.FieldName is not null)
150202
{
151203
options.IncludeProperties(fieldIdentifier.FieldName);
152204
}
153-
205+
154206
}
155207

156208
protected static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
@@ -182,7 +234,9 @@ protected static FieldIdentifier ToFieldIdentifier(EditContext editContext, stri
182234
// It's an indexer
183235
// This code assumes C# conventions (one indexer named Item with one param)
184236
nextToken = nextToken.Substring(0, nextToken.Length - 1);
185-
var prop = obj.GetType().GetProperty("Item");
237+
238+
var prop = obj.GetType().GetProperties().Where(e => e.Name == "Item" && e.GetIndexParameters().Length == 1).FirstOrDefault()
239+
?? obj.GetType().GetInterfaces().FirstOrDefault(e => e.IsGenericType && e.GetGenericTypeDefinition() == typeof(IReadOnlyList<>) || e.GetGenericTypeDefinition() == typeof(IList<>))?.GetProperty("Item"); //e.g. arrays
186240

187241
if (prop is not null)
188242
{
@@ -191,19 +245,13 @@ protected static FieldIdentifier ToFieldIdentifier(EditContext editContext, stri
191245
var indexerValue = Convert.ChangeType(nextToken, indexerType);
192246
newObj = prop.GetValue(obj, new object[] { indexerValue });
193247
}
248+
else if (obj is IEnumerable<object> objEnumerable && int.TryParse(nextToken, out int indexerValue)) //e.g. hashset
249+
{
250+
newObj = objEnumerable.ElementAt(indexerValue);
251+
}
194252
else
195253
{
196-
// If there is no Item property
197-
// Try to cast the object to array
198-
if (obj is object[] array)
199-
{
200-
var indexerValue = Convert.ToInt32(nextToken);
201-
newObj = array[indexerValue];
202-
}
203-
else
204-
{
205-
throw new InvalidOperationException($"Could not find indexer on object of type {obj.GetType().FullName}.");
206-
}
254+
throw new InvalidOperationException($"Could not find indexer on object of type {obj.GetType().FullName}.");
207255
}
208256
}
209257
else

0 commit comments

Comments
 (0)