88using Microsoft . AspNetCore . Components ;
99using Microsoft . AspNetCore . Components . Forms ;
1010using Microsoft . Extensions . DependencyInjection ;
11+ using Microsoft . Extensions . Logging ;
1112using System ;
1213using System . Collections . Generic ;
1314using System . Linq ;
15+ using System . Linq . Expressions ;
1416using System . Threading . Tasks ;
15- using static FluentValidation . AssemblyScanner ;
1617
1718namespace 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