--- /dev/null
+// 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.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.Extensions.Options
+{
+ /// <summary>
+ /// Builds <see cref="ValidateOptionsResult"/> with support for multiple error messages.
+ /// </summary>
+ [DebuggerDisplay("{ErrorsCount} errors")]
+ public class ValidateOptionsResultBuilder
+ {
+ private const string MemberSeparatorString = ", ";
+
+ private List<string>? _errors;
+
+ /// <summary>
+ /// Creates new instance of the <see cref="ValidateOptionsResultBuilder"/> class.
+ /// </summary>
+ public ValidateOptionsResultBuilder() { }
+
+ /// <summary>
+ /// Adds a new validation error to the builder.
+ /// </summary>
+ /// <param name="error">Content of error message.</param>
+ /// <param name="propertyName">The property in the option object which contains an error.</param>
+ public void AddError(string error, string? propertyName = null)
+ {
+ ThrowHelper.ThrowIfNull(error);
+ Errors.Add(propertyName is null ? error : $"Property {propertyName}: {error}");
+ }
+
+ /// <summary>
+ /// Adds any validation error carried by the <see cref="ValidationResult"/> instance to this instance.
+ /// </summary>
+ /// <param name="result">The instance to append the error from.</param>
+ public void AddResult(ValidationResult? result)
+ {
+ if (result?.ErrorMessage is not null)
+ {
+ string joinedMembers = string.Join(MemberSeparatorString, result.MemberNames);
+ Errors.Add(joinedMembers.Length != 0
+ ? $"{joinedMembers}: {result.ErrorMessage}"
+ : result.ErrorMessage);
+ }
+ }
+
+ /// <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 AddResults(IEnumerable<ValidationResult?>? results)
+ {
+ if (results != null)
+ {
+ foreach (ValidationResult? result in results)
+ {
+ AddResult(result);
+ }
+ }
+ }
+
+ /// <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 AddResult(ValidateOptionsResult result)
+ {
+ ThrowHelper.ThrowIfNull(result);
+
+ if (result.Failed)
+ {
+ if (result.Failures is null)
+ {
+ Errors.Add(result.FailureMessage);
+ }
+ else
+ {
+ // We are adding each failure separately to have the right failures count in _errors list.
+ // Otherwise we could add result.FailureMessage as one failure containing all result failures.
+ foreach (var failure in result.Failures)
+ {
+ if (failure is not null)
+ {
+ Errors.Add(failure);
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Builds <see cref="ValidateOptionsResult"/> based on provided data.
+ /// </summary>
+ /// <returns>New instance of <see cref="ValidateOptionsResult"/>.</returns>
+ public ValidateOptionsResult Build()
+ {
+ if (_errors?.Count > 0)
+ {
+ return ValidateOptionsResult.Fail(_errors);
+ }
+
+ return ValidateOptionsResult.Success;
+ }
+
+ /// <summary>
+ /// Reset the builder to the empty state
+ /// </summary>
+ public void Clear() => _errors?.Clear();
+
+ private int ErrorsCount => _errors is null ? 0 : _errors.Count;
+
+ private List<string> Errors => _errors ??= new();
+ }
+}
--- /dev/null
+// 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.ComponentModel.DataAnnotations;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Extensions.Options.Tests
+{
+ public class OptionsValidationBuilderTests
+ {
+ [Fact]
+ public void ValidateEmptyBuilder()
+ {
+ ValidateOptionsResultBuilder builder = new();
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+
+ builder.AddResult((ValidationResult)null);
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+
+ builder.AddResult(ValidationResult.Success);
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+
+ builder.AddResult(new ValidationResult(null));
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+
+ builder.AddResult(new ValidationResult(null, null));
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+
+ builder.AddResult(ValidateOptionsResult.Skip);
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+ }
+
+ [Fact]
+ public void ValidateBuilderThrows()
+ {
+ ValidateOptionsResultBuilder builder = new();
+ Assert.Throws<ArgumentNullException>(() => builder.AddError(null));
+ Assert.Throws<ArgumentNullException>(() => builder.AddResult((ValidateOptionsResult)null));
+ }
+
+ [Fact]
+ public void ValidateAddErrors()
+ {
+ ValidateOptionsResultBuilder builder = new();
+
+ string errors = "Failure 1";
+ builder.AddError(errors);
+ ValidateOptionsResult r = builder.Build();
+ Assert.False(EqualResults(ValidateOptionsResult.Success, r), $"{r.FailureMessage}");
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Failure 2";
+ builder.AddError("Failure 2");
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Property Prop1: Failure 3";
+ builder.AddError("Failure 3", "Prop1");
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+ }
+
+ [Fact]
+ public void ValidateAddValidationResult()
+ {
+ ValidateOptionsResultBuilder builder = new();
+
+ string errors = "Failure 4";
+ builder.AddResult(new ValidationResult("Failure 4"));
+ ValidateOptionsResult r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; member1, member2: Failure 5";
+ builder.AddResult(new ValidationResult("Failure 5", new List<string>() { "member1", "member2" }));
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ builder.AddResults((IEnumerable<ValidationResult?>?) null);
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Failure 6; Failure 7";
+ builder.AddResults(
+ new List<ValidationResult?>()
+ {
+ new ValidationResult("Failure 6"),
+ null,
+ new ValidationResult("Failure 7"),
+ null
+ });
+
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+ }
+
+ [Fact]
+ public void ValidateAddValidateOptionResult()
+ {
+ ValidateOptionsResultBuilder builder = new();
+
+ string errors = "Failure 8";
+ builder.AddResult(ValidateOptionsResult.Fail("Failure 8"));
+ ValidateOptionsResult r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Failure 9; Failure 10";
+ builder.AddResult(ValidateOptionsResult.Fail(new List<string>() { "Failure 9", null, null, "Failure 10" }));
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+ }
+
+ [Fact]
+ public void ValidateClear()
+ {
+ ValidateOptionsResultBuilder builder = new();
+ string errors = "Failure 10";
+
+ builder.AddError(errors);
+ ValidateOptionsResult r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ builder.Clear();
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+
+ errors = "Failure 11";
+ builder.AddError(errors);
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+ }
+
+ [Fact]
+ public void ValidateAddingMixedErrors()
+ {
+ ValidateOptionsResultBuilder builder = new();
+ string errors = "Failure 12";
+ builder.AddError(errors);
+ ValidateOptionsResult r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Property Prop: Failure 13";
+ builder.AddError("Failure 13", "Prop");
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Failure 14";
+ builder.AddResult(new ValidationResult("Failure 14"));
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; member1, member2: Failure 15";
+ builder.AddResult(new ValidationResult("Failure 15", new List<string>() { "member1", "member2" }));
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Failure 16; Failure 17";
+ builder.AddResults(
+ new List<ValidationResult?>()
+ {
+ new ValidationResult("Failure 16"),
+ null,
+ new ValidationResult("Failure 17"),
+ null
+ });
+
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Failure 18";
+ builder.AddResult(ValidateOptionsResult.Fail("Failure 18"));
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ errors += "; Failure 19; Failure 20";
+ builder.AddResult(ValidateOptionsResult.Fail(new List<string>() { "Failure 19", null, null, "Failure 20" }));
+ r = builder.Build();
+ Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
+
+ builder.Clear();
+ Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
+ }
+
+ private static bool EqualResults(ValidateOptionsResult r1, ValidateOptionsResult r2) =>
+ r1.Succeeded == r2.Succeeded &&
+ r1.Skipped == r2.Skipped &&
+ r1.Failed == r2.Failed &&
+ r1.FailureMessage == r2.FailureMessage &&
+ (r1.Failures == r1.Failures || (r1.Failures != null && r1.Failures != null && Enumerable.SequenceEqual(r1.Failures, r2.Failures)));
+ }
+}