Closed
Description
Background and motivation
When writing implementations of the IValidateOptions.Validate, it is necessary to return a ValidateOptionsResult instance describing the result of validating input. Producing a quality instance is currently fairly clumsy, leading to 'peel the onion' validation errors. That is, validation usually terminates on the first validation error. This means as a user, you end up having to do the "edit/run/edit/run/edit/run" cycle multiple times until all the errors are addressed.
The proposal introduces a builder object that makes it simple for validation code to proceed incrementally and accumulate all the validation errors discovered.
API Proposal
namespace Microsoft.Extensions.Options;
/// <summary>
/// Builds <see cref="ValidateOptionsResult"/> with support for multiple error messages.
/// </summary>
[DebuggerDisplay("{_errors.Count} errors")]
public readonly struct ValidateOptionsResultBuilder
{
/// <summary>
/// Initializes a new instance of the <see cref="ValidateOptionsResultBuilder"/> struct.
/// </summary>
/// <param name="errors">An enumeration of error strings to initialize the builder with.</param>
public ValidateOptionsResultBuilder(IEnumerable<string>? errors);
/// <summary>
/// Creates new instance of <see cref="ValidateOptionsResultBuilder"/> struct.
/// </summary>
/// <returns>New instance of <see cref="ValidateOptionsResultBuilder"/>.</returns>
public static ValidateOptionsResultBuilder Create();
/// <summary>
/// Adds validation error.
/// </summary>
/// <param name="error">Content of error message.</param>
public void AddError(string error);
/// <summary>
/// Adds validation error.
/// </summary>
/// <param name="propertyName">THe property in the option object which contains an error.</param>
/// <param name="error">Content of error message.</param>
public void AddError(string propertyName, string error);
/// <summary>
/// Adds any validation error carried by the <see cref="ValidationResult"/> instance to this instance.
/// </summary>
/// <param name="result">The instance to consume the errors from.</param>
public void AddError(ValidationResult? result);
/// <summary>
/// Adds any validation error carried by the enumeration of <see cref="ValidationResult"/> instances to this instance.
/// </summary>
/// <param name="results">The enumeration to consume the errors from.</param>
public void AddErrors(IEnumerable<ValidationResult>? results);
/// <summary>
/// Adds any validation errors carried by the <see cref="ValidateOptionsResult"/> instance to this instance.
/// </summary>
/// <param name="result">The instance to consume the errors from.</param>
public void AddErrors(ValidateOptionsResult? result);
/// <summary>
/// Builds <see cref="ValidateOptionsResult"/> based on provided data.
/// </summary>
/// <returns>New instance of <see cref="ValidateOptionsResult"/>.</returns>
public ValidateOptionsResult Build();
}
API Usage
internal sealed class HttpStandardResilienceOptionsCustomValidator : IValidateOptions<HttpStandardResilienceOptions>
{
private const int CircuitBreakerTimeoutMultiplier = 2;
public ValidateOptionsResult Validate(string name, HttpStandardResilienceOptions options)
{
var builder = ValidateOptionsResultBuilder.Create();
if (options.AttemptTimeoutOptions.TimeoutInterval > options.TotalRequestTimeoutOptions.TimeoutInterval)
{
builder.AddError($"Total request timeout policy must have a greater timeout than the attempt timeout policy. " +
$"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s, " +
$"Attempt Timeout: {options.AttemptTimeoutOptions.TimeoutInterval.TotalSeconds}s");
}
if (options.CircuitBreakerOptions.SamplingDuration < TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds * CircuitBreakerTimeoutMultiplier))
{
builder.AddError("The sampling duration of circuit breaker policy needs to be at least double of " +
$"an attempt timeout policy’s timeout interval, in order to be effective. " +
$"Sampling Duration: {options.CircuitBreakerOptions.SamplingDuration.TotalSeconds}s," +
$"Attempt Timeout: {options.AttemptTimeoutOptions.TimeoutInterval.TotalSeconds}s");
}
if (options.RetryOptions.RetryCount != RetryPolicyOptions.InfiniteRetry)
{
TimeSpan retrySum = options.RetryOptions.GetRetryPolicyDelaySum();
if (retrySum > options.TotalRequestTimeoutOptions.TimeoutInterval)
{
builder.AddError($"The cumulative delay of the retry policy cannot be larger than total request timeout policy interval. " +
$"Cumulative Delay: {retrySum.TotalSeconds}s," +
$"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s");
}
}
return builder.Build();
}
}
Alternative Designs
No response
Risks
No response