Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Eager Options Validation: ValidateOnStart API #47821

Merged
merged 9 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>();
maryamariyan marked this conversation as resolved.
Show resolved Hide resolved
}
}
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