Skip to content

Commit

Permalink
Add Eager Options Validation: ValidateOnStart API (dotnet#47821)
Browse files Browse the repository at this point in the history
  • Loading branch information
maryamariyan authored Feb 17, 2021
1 parent ada53f0 commit bec81bd
Show file tree
Hide file tree
Showing 10 changed files with 651 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
// Changes to this file must follow the https://aka.ms/api-review process.
// ------------------------------------------------------------------------------

namespace Microsoft.Extensions.DependencyInjection
{
public static partial class OptionsBuilderExtensions
{
public static Microsoft.Extensions.Options.OptionsBuilder<TOptions> ValidateOnStart<TOptions>(this Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder) where TOptions : class { throw null; }
}
}
namespace Microsoft.Extensions.Hosting
{
public partial class ConsoleLifetimeOptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods for adding configuration related options services to the DI container via <see cref="OptionsBuilder{TOptions}"/>.
/// </summary>
public static class OptionsBuilderExtensions
{
/// <summary>
/// Enforces options validation check on start rather than in runtime.
/// </summary>
/// <typeparam name="TOptions">The type of options.</typeparam>
/// <param name="optionsBuilder">The <see cref="OptionsBuilder{TOptions}"/> to configure options instance.</param>
/// <returns>The <see cref="OptionsBuilder{TOptions}"/> so that additional calls can be chained.</returns>
public static OptionsBuilder<TOptions> ValidateOnStart<TOptions>(this OptionsBuilder<TOptions> optionsBuilder)
where TOptions : class
{
if (optionsBuilder == null)
{
throw new ArgumentNullException(nameof(optionsBuilder));
}

optionsBuilder.Services.AddHostedService<ValidationHostedService>();
optionsBuilder.Services.AddOptions<ValidatorOptions>()
.Configure<IOptionsMonitor<TOptions>>((vo, options) =>
{
// This adds an action that resolves the options value to force evaluation
// We don't care about the result as duplicates are not important
vo.Validators[typeof(TOptions)] = () => options.Get(optionsBuilder.Name);
});

return optionsBuilder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection
{
internal class ValidationHostedService : IHostedService
{
private readonly IDictionary<Type, Action> _validators;

public ValidationHostedService(IOptions<ValidatorOptions> validatorOptions)
{
_validators = validatorOptions?.Value?.Validators ?? throw new ArgumentNullException(nameof(validatorOptions));
}

public Task StartAsync(CancellationToken cancellationToken)
{
var exceptions = new List<Exception>();

foreach (var validate in _validators.Values)
{
try
{
// Execute the validation method and catch the validation error
validate();
}
catch (OptionsValidationException ex)
{
exceptions.Add(ex);
}
}

if (exceptions.Count == 1)
{
// Rethrow if it's a single error
ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
}

if (exceptions.Count > 1)
{
// Aggregate if we have many errors
throw new AggregateException(exceptions);
}

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
14 changes: 14 additions & 0 deletions src/libraries/Microsoft.Extensions.Hosting/src/ValidatorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;

namespace Microsoft.Extensions.DependencyInjection
{
internal class ValidatorOptions
{
// Maps each options type to a method that forces its evaluation, e.g. IOptionsMonitor<TOptions>.Get(name)
public IDictionary<Type, Action> Validators { get; } = new Dictionary<Type, Action>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.Extensions.Hosting.Tests
{
public class ComplexOptions
{
public ComplexOptions()
{
Nested = new NestedOptions();
Virtual = "complex";
}
public NestedOptions Nested { get; set; }
public int Integer { get; set; }
public bool Boolean { get; set; }
public virtual string Virtual { get; set; }

public string PrivateSetter { get; private set; }
public string ProtectedSetter { get; protected set; }
public string InternalSetter { get; internal set; }
public static string StaticProperty { get; set; }

public string ReadOnly
{
get { return null; }
}
}

public class NestedOptions
{
public int Integer { get; set; }
}

public class DerivedOptions : ComplexOptions
{
public override string Virtual
{
get
{
return base.Virtual;
}
set
{
base.Virtual = "Derived:" + value;
}
}
}
}
Loading

0 comments on commit bec81bd

Please sign in to comment.