@@ -240,12 +240,18 @@ public void CanValidateOptionsWithCustomError()
240
240
var services = new ServiceCollection ( ) ;
241
241
services . AddOptions < ComplexOptions > ( )
242
242
. Configure ( o => o . Boolean = false )
243
- . Validate ( Options . DefaultName , o => o . Boolean , "Boolean must be true." ) ;
243
+ . Validate ( o => o . Boolean , "Boolean must be true." ) ;
244
+ services . AddOptions < ComplexOptions > ( "named" )
245
+ . Configure ( o => o . Boolean = true )
246
+ . Validate ( o => ! o . Boolean , "named Boolean must be false." ) ;
244
247
var sp = services . BuildServiceProvider ( ) ;
245
248
var error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptions < ComplexOptions > > ( ) . Value ) ;
246
- Assert . Equal ( "Boolean must be true." , error . Failures . First ( ) ) ;
249
+ ValidateFailure < ComplexOptions > ( error , Options . DefaultName , "Boolean must be true." ) ;
250
+ error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptionsMonitor < ComplexOptions > > ( ) . Get ( "named" ) ) ;
251
+ ValidateFailure < ComplexOptions > ( error , "named" , "named Boolean must be false." ) ;
247
252
}
248
253
254
+
249
255
[ Fact ]
250
256
public void CanValidateOptionsWithDefaultError ( )
251
257
{
@@ -255,7 +261,7 @@ public void CanValidateOptionsWithDefaultError()
255
261
. Validate ( o => o . Boolean ) ;
256
262
var sp = services . BuildServiceProvider ( ) ;
257
263
var error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptions < ComplexOptions > > ( ) . Value ) ;
258
- Assert . Equal ( "A validation error has occured." , error . Failures . First ( ) ) ;
264
+ ValidateFailure < ComplexOptions > ( error ) ;
259
265
}
260
266
261
267
[ Fact ]
@@ -273,10 +279,7 @@ public void CanValidateOptionsWithMultipleDefaultErrors()
273
279
274
280
var sp = services . BuildServiceProvider ( ) ;
275
281
var error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptions < ComplexOptions > > ( ) . Value ) ;
276
- var errors = error . Failures . ToArray ( ) ;
277
- Assert . Equal ( 2 , errors . Length ) ;
278
- Assert . Equal ( "A validation error has occured." , errors [ 0 ] ) ;
279
- Assert . Equal ( "A validation error has occured." , errors [ 1 ] ) ;
282
+ ValidateFailure < ComplexOptions > ( error , Options . DefaultName , "A validation error has occured." , "A validation error has occured." ) ;
280
283
}
281
284
282
285
[ Fact ]
@@ -291,16 +294,29 @@ public void CanValidateOptionsWithMixedOverloads()
291
294
o . Virtual = "wut" ;
292
295
} )
293
296
. Validate ( o => o . Boolean )
294
- . Validate ( Options . DefaultName , o => o . Virtual == null , "Virtual" )
297
+ . Validate ( o => o . Virtual == null , "Virtual" )
295
298
. Validate ( o => o . Integer > 12 , "Integer" ) ;
296
299
297
300
var sp = services . BuildServiceProvider ( ) ;
298
301
var error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptions < ComplexOptions > > ( ) . Value ) ;
299
- var errors = error . Failures . ToArray ( ) ;
300
- Assert . Equal ( 3 , errors . Length ) ;
301
- Assert . Equal ( "A validation error has occured." , errors [ 0 ] ) ;
302
- Assert . Equal ( "Virtual" , errors [ 1 ] ) ;
303
- Assert . Equal ( "Integer" , errors [ 2 ] ) ;
302
+ ValidateFailure < ComplexOptions > ( error , Options . DefaultName , "A validation error has occured." , "Virtual" , "Integer" ) ;
303
+ }
304
+
305
+ public class BadValidator : IValidateOptions < FakeOptions >
306
+ {
307
+ public ValidateOptionsResult Validate ( string name , FakeOptions options )
308
+ {
309
+ throw new NotImplementedException ( ) ;
310
+ }
311
+ }
312
+
313
+ [ Fact ]
314
+ public void BadValidatorFailsGracefully ( )
315
+ {
316
+ var services = new ServiceCollection ( ) . AddOptions ( ) ;
317
+ services . AddSingleton < IValidateOptions < FakeOptions > , BadValidator > ( ) ;
318
+ var sp = services . BuildServiceProvider ( ) ;
319
+ var error = Assert . Throws < NotImplementedException > ( ( ) => sp . GetRequiredService < IOptions < FakeOptions > > ( ) . Value ) ;
304
320
}
305
321
306
322
private class MultiOptionValidator : IValidateOptions < ComplexOptions > , IValidateOptions < FakeOptions >
@@ -342,12 +358,10 @@ public void CanValidateMultipleOptionsWithOneValidator()
342
358
343
359
var sp = services . BuildServiceProvider ( ) ;
344
360
var error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptions < ComplexOptions > > ( ) . Value ) ;
345
- Assert . Single ( error . Failures ) ;
346
- Assert . Equal ( "Virtual != real" , error . Failures . First ( ) ) ;
361
+ ValidateFailure < ComplexOptions > ( error , Options . DefaultName , "Virtual != real" ) ;
347
362
348
363
error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptions < FakeOptions > > ( ) . Value ) ;
349
- Assert . Single ( error . Failures ) ;
350
- Assert . Equal ( "Message != real" , error . Failures . First ( ) ) ;
364
+ ValidateFailure < FakeOptions > ( error , Options . DefaultName , "Message != real" ) ;
351
365
352
366
var fake = sp . GetRequiredService < IOptionsMonitor < FakeOptions > > ( ) . Get ( "fake" ) ;
353
367
Assert . Equal ( "real" , fake . Message ) ;
@@ -371,6 +385,17 @@ public ValidateOptionsResult Validate(string name, ComplexOptions options)
371
385
}
372
386
}
373
387
388
+ private void ValidateFailure < TOptions > ( OptionsValidationException e , string name = "" , params string [ ] errors )
389
+ {
390
+ Assert . Equal ( typeof ( TOptions ) , e . OptionsType ) ;
391
+ Assert . Equal ( name , e . OptionsName ) ;
392
+ if ( errors . Length == 0 )
393
+ {
394
+ errors = new string [ ] { "A validation error has occured." } ;
395
+ }
396
+ Assert . True ( errors . SequenceEqual ( e . Failures ) ) ;
397
+ }
398
+
374
399
[ Fact ]
375
400
public void CanValidateOptionsThatDependOnOptions ( )
376
401
{
@@ -388,20 +413,105 @@ public void CanValidateOptionsThatDependOnOptions()
388
413
var sp = services . BuildServiceProvider ( ) ;
389
414
390
415
var error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptions < ComplexOptions > > ( ) . Value ) ;
391
- Assert . Single ( error . Failures ) ;
392
- Assert . Equal ( "Virtual != target" , error . Failures . First ( ) ) ;
416
+ ValidateFailure < ComplexOptions > ( error , Options . DefaultName , "Virtual != target" ) ;
393
417
394
418
error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptionsMonitor < ComplexOptions > > ( ) . Get ( Options . DefaultName ) ) ;
395
- Assert . Single ( error . Failures ) ;
396
- Assert . Equal ( "Virtual != target" , error . Failures . First ( ) ) ;
419
+ ValidateFailure < ComplexOptions > ( error , Options . DefaultName , "Virtual != target" ) ;
397
420
398
421
error = Assert . Throws < OptionsValidationException > ( ( ) => sp . GetRequiredService < IOptionsMonitor < ComplexOptions > > ( ) . Get ( "no" ) ) ;
399
- Assert . Single ( error . Failures ) ;
400
- Assert . Equal ( "Virtual != target" , error . Failures . First ( ) ) ;
422
+ ValidateFailure < ComplexOptions > ( error , "no" , "Virtual != target" ) ;
401
423
402
424
var op = sp . GetRequiredService < IOptionsMonitor < ComplexOptions > > ( ) . Get ( "yes" ) ;
403
425
Assert . Equal ( "target" , op . Virtual ) ;
404
426
}
405
427
428
+ // Prototype of startup validation
429
+
430
+ public interface IStartupValidator
431
+ {
432
+ void Validate ( ) ;
433
+ }
434
+
435
+ public class StartupValidationOptions
436
+ {
437
+ private Dictionary < Type , IList < string > > _targets = new Dictionary < Type , IList < string > > ( ) ;
438
+
439
+ public IDictionary < Type , IList < string > > ValidationTargets { get => _targets ; }
440
+
441
+ public void Validate < TOptions > ( string name ) where TOptions : class
442
+ {
443
+ if ( ! _targets . ContainsKey ( typeof ( TOptions ) ) )
444
+ {
445
+ _targets [ typeof ( TOptions ) ] = new List < string > ( ) ;
446
+ }
447
+ _targets [ typeof ( TOptions ) ] . Add ( name ?? Options . DefaultName ) ;
448
+ }
449
+
450
+ public void Validate < TOptions > ( ) where TOptions : class => Validate < TOptions > ( Options . DefaultName ) ;
451
+ }
452
+
453
+ public class OptionsStartupValidator : IStartupValidator
454
+ {
455
+ private IServiceProvider _services ;
456
+ private StartupValidationOptions _options ;
457
+
458
+ public OptionsStartupValidator ( IOptions < StartupValidationOptions > options , IServiceProvider services )
459
+ {
460
+ _services = services ;
461
+ _options = options . Value ;
462
+ }
463
+
464
+ public void Validate ( )
465
+ {
466
+ var errors = new List < string > ( ) ;
467
+ foreach ( var targetType in _options . ValidationTargets . Keys )
468
+ {
469
+ var optionsType = typeof ( IOptionsMonitor < > ) . MakeGenericType ( targetType ) ;
470
+ var monitor = _services . GetRequiredService ( optionsType ) ;
471
+ var getMethod = optionsType . GetMethod ( "Get" ) ;
472
+ foreach ( var namedInstance in _options . ValidationTargets [ targetType ] )
473
+ {
474
+ // TODO: maybe aggregate and catch all options instead of one at a time?
475
+ try
476
+ {
477
+ getMethod . Invoke ( monitor , new object [ ] { namedInstance } ) ;
478
+ } catch ( Exception e )
479
+ {
480
+ if ( e . InnerException is OptionsValidationException )
481
+ {
482
+ throw e . InnerException ;
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+
490
+ [ Fact ]
491
+ public void CanValidateOptionsEagerly ( )
492
+ {
493
+ var services = new ServiceCollection ( ) ;
494
+ services . AddOptions < ComplexOptions > ( )
495
+ . Configure ( o =>
496
+ {
497
+ o . Boolean = false ;
498
+ o . Integer = 11 ;
499
+ o . Virtual = "wut" ;
500
+ } )
501
+ . Validate ( o => o . Boolean )
502
+ . Validate ( o => o . Virtual == null , "Virtual" )
503
+ . Validate ( o => o . Integer > 12 , "Integer" ) ;
504
+
505
+ services . Configure < StartupValidationOptions > ( o => o . Validate < ComplexOptions > ( ) ) ;
506
+ services . AddSingleton < IStartupValidator , OptionsStartupValidator > ( ) ;
507
+
508
+ var sp = services . BuildServiceProvider ( ) ;
509
+
510
+ var startupValidator = sp . GetRequiredService < IStartupValidator > ( ) ;
511
+
512
+ var error = Assert . Throws < OptionsValidationException > ( ( ) => startupValidator . Validate ( ) ) ;
513
+ ValidateFailure < ComplexOptions > ( error , Options . DefaultName , "A validation error has occured." , "Virtual" , "Integer" ) ;
514
+ }
515
+
406
516
}
407
- }
517
+ }
0 commit comments