Introducing ValidateOptionsResultBuilder (#82749)
authorTarek Mahmoud Sayed <tarekms@microsoft.com>
Thu, 2 Mar 2023 21:15:32 +0000 (13:15 -0800)
committerGitHub <noreply@github.com>
Thu, 2 Mar 2023 21:15:32 +0000 (13:15 -0800)
src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs
src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.csproj
src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj
src/libraries/Microsoft.Extensions.Options/src/ValidateOptionsResultBuilder.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsValidationBuilderTests.cs [new file with mode: 0644]

index 3530738..6ae6095 100644 (file)
@@ -297,6 +297,16 @@ namespace Microsoft.Extensions.Options
         public static Microsoft.Extensions.Options.ValidateOptionsResult Fail(System.Collections.Generic.IEnumerable<string> failures) { throw null; }
         public static Microsoft.Extensions.Options.ValidateOptionsResult Fail(string failureMessage) { throw null; }
     }
+    public class ValidateOptionsResultBuilder
+    {
+        public ValidateOptionsResultBuilder() { }
+        public void AddError(string error, string? propertyName = null) { throw null; }
+        public void AddResult(System.ComponentModel.DataAnnotations.ValidationResult? result) { throw null; }
+        public void AddResults(System.Collections.Generic.IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult?>? results) { throw null; }
+        public void AddResult(ValidateOptionsResult result) { throw null; }
+        public ValidateOptionsResult Build() { throw null; }
+        public void Clear() { throw null; }
+    }
     public partial class ValidateOptions<TOptions> : Microsoft.Extensions.Options.IValidateOptions<TOptions> where TOptions : class
     {
         public ValidateOptions(string? name, System.Func<TOptions, bool> validation, string failureMessage) { }
index adaa555..7ee6d82 100644 (file)
@@ -6,7 +6,7 @@
   <ItemGroup>
     <Compile Include="Microsoft.Extensions.Options.cs" />
   </ItemGroup>
-  
+
   <ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
     <Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMembersAttribute.cs" />
     <Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMemberTypes.cs" />
     <ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\ref\Microsoft.Extensions.Primitives.csproj" />
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
+  <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'">
     <PackageReference Include="System.ComponentModel.Annotations" Version="$(SystemComponentModelAnnotationsVersion)" />
   </ItemGroup>
+
+  <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
+    <Reference Include="System.ComponentModel.DataAnnotations" />
+  </ItemGroup>
 </Project>
index c9713f5..202e943 100644 (file)
@@ -22,7 +22,7 @@
     <ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\src\Microsoft.Extensions.Primitives.csproj" />
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
+  <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'">
     <PackageReference Include="System.ComponentModel.Annotations" Version="$(SystemComponentModelAnnotationsVersion)" />
   </ItemGroup>
 
diff --git a/src/libraries/Microsoft.Extensions.Options/src/ValidateOptionsResultBuilder.cs b/src/libraries/Microsoft.Extensions.Options/src/ValidateOptionsResultBuilder.cs
new file mode 100644 (file)
index 0000000..d5c17e0
--- /dev/null
@@ -0,0 +1,120 @@
+// 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();
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsValidationBuilderTests.cs b/src/libraries/Microsoft.Extensions.Options/tests/Microsoft.Extensions.Options.Tests/OptionsValidationBuilderTests.cs
new file mode 100644 (file)
index 0000000..83ddc98
--- /dev/null
@@ -0,0 +1,192 @@
+// 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)));
+    }
+}