Skip to content
This repository was archived by the owner on Nov 7, 2018. It is now read-only.

Commit 7d5862c

Browse files
authored
Merge pull request #270 from aspnet/haok/val2
Polish for preview1
2 parents cea619f + 2d4030e commit 7d5862c

File tree

4 files changed

+149
-35
lines changed

4 files changed

+149
-35
lines changed

src/Microsoft.Extensions.Options/OptionsBuilder.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,22 +258,16 @@ public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3, TDep4
258258
}
259259

260260
public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation)
261-
=> Validate(name: Options.DefaultName, validation: validation, failureMessage: "A validation error has occured.");
262-
263-
public virtual OptionsBuilder<TOptions> Validate(string name, Func<TOptions, bool> validation)
264-
=> Validate(name: name, validation: validation, failureMessage: "A validation error has occured.");
261+
=> Validate(validation: validation, failureMessage: "A validation error has occured.");
265262

266263
public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation, string failureMessage)
267-
=> Validate(name: Options.DefaultName, validation: validation, failureMessage: failureMessage);
268-
269-
public virtual OptionsBuilder<TOptions> Validate(string name, Func<TOptions, bool> validation, string failureMessage)
270264
{
271265
if (validation == null)
272266
{
273267
throw new ArgumentNullException(nameof(validation));
274268
}
275269

276-
Services.AddSingleton<IValidateOptions<TOptions>>(new ValidateOptions<TOptions>(name, validation, failureMessage));
270+
Services.AddSingleton<IValidateOptions<TOptions>>(new ValidateOptions<TOptions>(Name, validation, failureMessage));
277271
return this;
278272
}
279273
}

src/Microsoft.Extensions.Options/OptionsFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public TOptions Create(string name)
6868
}
6969
if (failures.Count > 0)
7070
{
71-
throw new OptionsValidationException(failures);
71+
throw new OptionsValidationException(name, typeof(TOptions), failures);
7272
}
7373
}
7474

src/Microsoft.Extensions.Options/OptionsValidationException.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ public class OptionsValidationException : Exception
1414
/// <summary>
1515
/// Constructor.
1616
/// </summary>
17+
/// <param name="optionsName">The name of the options instance that failed.</param>
18+
/// <param name="optionsType">The options type that failed.</param>
1719
/// <param name="failureMessages">The validation failure messages.</param>
18-
public OptionsValidationException(IEnumerable<string> failureMessages)
19-
=> Failures = failureMessages ?? new List<string>();
20+
public OptionsValidationException(string optionsName, Type optionsType, IEnumerable<string> failureMessages)
21+
{
22+
Failures = failureMessages ?? new List<string>();
23+
OptionsType = optionsType ?? throw new ArgumentNullException(nameof(optionsType));
24+
OptionsName = optionsName ?? throw new ArgumentNullException(nameof(optionsName));
25+
}
26+
27+
public string OptionsName { get; }
28+
29+
public Type OptionsType { get; }
2030

2131
/// <summary>
2232
/// The validation failures.

test/Microsoft.Extensions.Options.Test/OptionsBuilderTest.cs

Lines changed: 134 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,18 @@ public void CanValidateOptionsWithCustomError()
240240
var services = new ServiceCollection();
241241
services.AddOptions<ComplexOptions>()
242242
.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.");
244247
var sp = services.BuildServiceProvider();
245248
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.");
247252
}
248253

254+
249255
[Fact]
250256
public void CanValidateOptionsWithDefaultError()
251257
{
@@ -255,7 +261,7 @@ public void CanValidateOptionsWithDefaultError()
255261
.Validate(o => o.Boolean);
256262
var sp = services.BuildServiceProvider();
257263
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);
259265
}
260266

261267
[Fact]
@@ -273,10 +279,7 @@ public void CanValidateOptionsWithMultipleDefaultErrors()
273279

274280
var sp = services.BuildServiceProvider();
275281
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.");
280283
}
281284

282285
[Fact]
@@ -291,16 +294,29 @@ public void CanValidateOptionsWithMixedOverloads()
291294
o.Virtual = "wut";
292295
})
293296
.Validate(o => o.Boolean)
294-
.Validate(Options.DefaultName, o => o.Virtual == null, "Virtual")
297+
.Validate(o => o.Virtual == null, "Virtual")
295298
.Validate(o => o.Integer > 12, "Integer");
296299

297300
var sp = services.BuildServiceProvider();
298301
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);
304320
}
305321

306322
private class MultiOptionValidator : IValidateOptions<ComplexOptions>, IValidateOptions<FakeOptions>
@@ -342,12 +358,10 @@ public void CanValidateMultipleOptionsWithOneValidator()
342358

343359
var sp = services.BuildServiceProvider();
344360
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");
347362

348363
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");
351365

352366
var fake = sp.GetRequiredService<IOptionsMonitor<FakeOptions>>().Get("fake");
353367
Assert.Equal("real", fake.Message);
@@ -371,6 +385,17 @@ public ValidateOptionsResult Validate(string name, ComplexOptions options)
371385
}
372386
}
373387

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+
374399
[Fact]
375400
public void CanValidateOptionsThatDependOnOptions()
376401
{
@@ -388,20 +413,105 @@ public void CanValidateOptionsThatDependOnOptions()
388413
var sp = services.BuildServiceProvider();
389414

390415
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");
393417

394418
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");
397420

398421
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");
401423

402424
var op = sp.GetRequiredService<IOptionsMonitor<ComplexOptions>>().Get("yes");
403425
Assert.Equal("target", op.Virtual);
404426
}
405427

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+
406516
}
407-
}
517+
}

0 commit comments

Comments
 (0)