Skip to content

Commit 57e2c3a

Browse files
authored
Introducing ValidateOptionsResultBuilder (#82749)
1 parent 90e5402 commit 57e2c3a

File tree

5 files changed

+329
-3
lines changed

5 files changed

+329
-3
lines changed

src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,16 @@ public ValidateOptionsResult() { }
297297
public static Microsoft.Extensions.Options.ValidateOptionsResult Fail(System.Collections.Generic.IEnumerable<string> failures) { throw null; }
298298
public static Microsoft.Extensions.Options.ValidateOptionsResult Fail(string failureMessage) { throw null; }
299299
}
300+
public class ValidateOptionsResultBuilder
301+
{
302+
public ValidateOptionsResultBuilder() { }
303+
public void AddError(string error, string? propertyName = null) { throw null; }
304+
public void AddResult(System.ComponentModel.DataAnnotations.ValidationResult? result) { throw null; }
305+
public void AddResults(System.Collections.Generic.IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult?>? results) { throw null; }
306+
public void AddResult(ValidateOptionsResult result) { throw null; }
307+
public ValidateOptionsResult Build() { throw null; }
308+
public void Clear() { throw null; }
309+
}
300310
public partial class ValidateOptions<TOptions> : Microsoft.Extensions.Options.IValidateOptions<TOptions> where TOptions : class
301311
{
302312
public ValidateOptions(string? name, System.Func<TOptions, bool> validation, string failureMessage) { }

src/libraries/Microsoft.Extensions.Options/ref/Microsoft.Extensions.Options.csproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<ItemGroup>
77
<Compile Include="Microsoft.Extensions.Options.cs" />
88
</ItemGroup>
9-
9+
1010
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
1111
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMembersAttribute.cs" />
1212
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMemberTypes.cs" />
@@ -17,7 +17,11 @@
1717
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\ref\Microsoft.Extensions.Primitives.csproj" />
1818
</ItemGroup>
1919

20-
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
20+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'">
2121
<PackageReference Include="System.ComponentModel.Annotations" Version="$(SystemComponentModelAnnotationsVersion)" />
2222
</ItemGroup>
23+
24+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
25+
<Reference Include="System.ComponentModel.DataAnnotations" />
26+
</ItemGroup>
2327
</Project>

src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\src\Microsoft.Extensions.Primitives.csproj" />
2323
</ItemGroup>
2424

25-
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
25+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'">
2626
<PackageReference Include="System.ComponentModel.Annotations" Version="$(SystemComponentModelAnnotationsVersion)" />
2727
</ItemGroup>
2828

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Diagnostics;
8+
using System.Runtime.CompilerServices;
9+
10+
namespace Microsoft.Extensions.Options
11+
{
12+
/// <summary>
13+
/// Builds <see cref="ValidateOptionsResult"/> with support for multiple error messages.
14+
/// </summary>
15+
[DebuggerDisplay("{ErrorsCount} errors")]
16+
public class ValidateOptionsResultBuilder
17+
{
18+
private const string MemberSeparatorString = ", ";
19+
20+
private List<string>? _errors;
21+
22+
/// <summary>
23+
/// Creates new instance of the <see cref="ValidateOptionsResultBuilder"/> class.
24+
/// </summary>
25+
public ValidateOptionsResultBuilder() { }
26+
27+
/// <summary>
28+
/// Adds a new validation error to the builder.
29+
/// </summary>
30+
/// <param name="error">Content of error message.</param>
31+
/// <param name="propertyName">The property in the option object which contains an error.</param>
32+
public void AddError(string error, string? propertyName = null)
33+
{
34+
ThrowHelper.ThrowIfNull(error);
35+
Errors.Add(propertyName is null ? error : $"Property {propertyName}: {error}");
36+
}
37+
38+
/// <summary>
39+
/// Adds any validation error carried by the <see cref="ValidationResult"/> instance to this instance.
40+
/// </summary>
41+
/// <param name="result">The instance to append the error from.</param>
42+
public void AddResult(ValidationResult? result)
43+
{
44+
if (result?.ErrorMessage is not null)
45+
{
46+
string joinedMembers = string.Join(MemberSeparatorString, result.MemberNames);
47+
Errors.Add(joinedMembers.Length != 0
48+
? $"{joinedMembers}: {result.ErrorMessage}"
49+
: result.ErrorMessage);
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Adds any validation error carried by the enumeration of <see cref="ValidationResult"/> instances to this instance.
55+
/// </summary>
56+
/// <param name="results">The enumeration to consume the errors from.</param>
57+
public void AddResults(IEnumerable<ValidationResult?>? results)
58+
{
59+
if (results != null)
60+
{
61+
foreach (ValidationResult? result in results)
62+
{
63+
AddResult(result);
64+
}
65+
}
66+
}
67+
68+
/// <summary>
69+
/// Adds any validation errors carried by the <see cref="ValidateOptionsResult"/> instance to this instance.
70+
/// </summary>
71+
/// <param name="result">The instance to consume the errors from.</param>
72+
public void AddResult(ValidateOptionsResult result)
73+
{
74+
ThrowHelper.ThrowIfNull(result);
75+
76+
if (result.Failed)
77+
{
78+
if (result.Failures is null)
79+
{
80+
Errors.Add(result.FailureMessage);
81+
}
82+
else
83+
{
84+
// We are adding each failure separately to have the right failures count in _errors list.
85+
// Otherwise we could add result.FailureMessage as one failure containing all result failures.
86+
foreach (var failure in result.Failures)
87+
{
88+
if (failure is not null)
89+
{
90+
Errors.Add(failure);
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
/// <summary>
98+
/// Builds <see cref="ValidateOptionsResult"/> based on provided data.
99+
/// </summary>
100+
/// <returns>New instance of <see cref="ValidateOptionsResult"/>.</returns>
101+
public ValidateOptionsResult Build()
102+
{
103+
if (_errors?.Count > 0)
104+
{
105+
return ValidateOptionsResult.Fail(_errors);
106+
}
107+
108+
return ValidateOptionsResult.Success;
109+
}
110+
111+
/// <summary>
112+
/// Reset the builder to the empty state
113+
/// </summary>
114+
public void Clear() => _errors?.Clear();
115+
116+
private int ErrorsCount => _errors is null ? 0 : _errors.Count;
117+
118+
private List<string> Errors => _errors ??= new();
119+
}
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Linq;
8+
using Xunit;
9+
10+
namespace Microsoft.Extensions.Options.Tests
11+
{
12+
public class OptionsValidationBuilderTests
13+
{
14+
[Fact]
15+
public void ValidateEmptyBuilder()
16+
{
17+
ValidateOptionsResultBuilder builder = new();
18+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
19+
20+
builder.AddResult((ValidationResult)null);
21+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
22+
23+
builder.AddResult(ValidationResult.Success);
24+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
25+
26+
builder.AddResult(new ValidationResult(null));
27+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
28+
29+
builder.AddResult(new ValidationResult(null, null));
30+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
31+
32+
builder.AddResult(ValidateOptionsResult.Skip);
33+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
34+
}
35+
36+
[Fact]
37+
public void ValidateBuilderThrows()
38+
{
39+
ValidateOptionsResultBuilder builder = new();
40+
Assert.Throws<ArgumentNullException>(() => builder.AddError(null));
41+
Assert.Throws<ArgumentNullException>(() => builder.AddResult((ValidateOptionsResult)null));
42+
}
43+
44+
[Fact]
45+
public void ValidateAddErrors()
46+
{
47+
ValidateOptionsResultBuilder builder = new();
48+
49+
string errors = "Failure 1";
50+
builder.AddError(errors);
51+
ValidateOptionsResult r = builder.Build();
52+
Assert.False(EqualResults(ValidateOptionsResult.Success, r), $"{r.FailureMessage}");
53+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
54+
55+
errors += "; Failure 2";
56+
builder.AddError("Failure 2");
57+
r = builder.Build();
58+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
59+
60+
errors += "; Property Prop1: Failure 3";
61+
builder.AddError("Failure 3", "Prop1");
62+
r = builder.Build();
63+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
64+
}
65+
66+
[Fact]
67+
public void ValidateAddValidationResult()
68+
{
69+
ValidateOptionsResultBuilder builder = new();
70+
71+
string errors = "Failure 4";
72+
builder.AddResult(new ValidationResult("Failure 4"));
73+
ValidateOptionsResult r = builder.Build();
74+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
75+
76+
errors += "; member1, member2: Failure 5";
77+
builder.AddResult(new ValidationResult("Failure 5", new List<string>() { "member1", "member2" }));
78+
r = builder.Build();
79+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
80+
81+
builder.AddResults((IEnumerable<ValidationResult?>?) null);
82+
r = builder.Build();
83+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
84+
85+
errors += "; Failure 6; Failure 7";
86+
builder.AddResults(
87+
new List<ValidationResult?>()
88+
{
89+
new ValidationResult("Failure 6"),
90+
null,
91+
new ValidationResult("Failure 7"),
92+
null
93+
});
94+
95+
r = builder.Build();
96+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
97+
}
98+
99+
[Fact]
100+
public void ValidateAddValidateOptionResult()
101+
{
102+
ValidateOptionsResultBuilder builder = new();
103+
104+
string errors = "Failure 8";
105+
builder.AddResult(ValidateOptionsResult.Fail("Failure 8"));
106+
ValidateOptionsResult r = builder.Build();
107+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
108+
109+
errors += "; Failure 9; Failure 10";
110+
builder.AddResult(ValidateOptionsResult.Fail(new List<string>() { "Failure 9", null, null, "Failure 10" }));
111+
r = builder.Build();
112+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
113+
}
114+
115+
[Fact]
116+
public void ValidateClear()
117+
{
118+
ValidateOptionsResultBuilder builder = new();
119+
string errors = "Failure 10";
120+
121+
builder.AddError(errors);
122+
ValidateOptionsResult r = builder.Build();
123+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
124+
125+
builder.Clear();
126+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
127+
128+
errors = "Failure 11";
129+
builder.AddError(errors);
130+
r = builder.Build();
131+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
132+
}
133+
134+
[Fact]
135+
public void ValidateAddingMixedErrors()
136+
{
137+
ValidateOptionsResultBuilder builder = new();
138+
string errors = "Failure 12";
139+
builder.AddError(errors);
140+
ValidateOptionsResult r = builder.Build();
141+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
142+
143+
errors += "; Property Prop: Failure 13";
144+
builder.AddError("Failure 13", "Prop");
145+
r = builder.Build();
146+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
147+
148+
errors += "; Failure 14";
149+
builder.AddResult(new ValidationResult("Failure 14"));
150+
r = builder.Build();
151+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
152+
153+
errors += "; member1, member2: Failure 15";
154+
builder.AddResult(new ValidationResult("Failure 15", new List<string>() { "member1", "member2" }));
155+
r = builder.Build();
156+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
157+
158+
errors += "; Failure 16; Failure 17";
159+
builder.AddResults(
160+
new List<ValidationResult?>()
161+
{
162+
new ValidationResult("Failure 16"),
163+
null,
164+
new ValidationResult("Failure 17"),
165+
null
166+
});
167+
168+
r = builder.Build();
169+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
170+
171+
errors += "; Failure 18";
172+
builder.AddResult(ValidateOptionsResult.Fail("Failure 18"));
173+
r = builder.Build();
174+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
175+
176+
errors += "; Failure 19; Failure 20";
177+
builder.AddResult(ValidateOptionsResult.Fail(new List<string>() { "Failure 19", null, null, "Failure 20" }));
178+
r = builder.Build();
179+
Assert.True(EqualResults(ValidateOptionsResult.Fail(errors), r), $"{r.FailureMessage} != {ValidateOptionsResult.Fail(errors).FailureMessage}");
180+
181+
builder.Clear();
182+
Assert.True(EqualResults(ValidateOptionsResult.Success, builder.Build()));
183+
}
184+
185+
private static bool EqualResults(ValidateOptionsResult r1, ValidateOptionsResult r2) =>
186+
r1.Succeeded == r2.Succeeded &&
187+
r1.Skipped == r2.Skipped &&
188+
r1.Failed == r2.Failed &&
189+
r1.FailureMessage == r2.FailureMessage &&
190+
(r1.Failures == r1.Failures || (r1.Failures != null && r1.Failures != null && Enumerable.SequenceEqual(r1.Failures, r2.Failures)));
191+
}
192+
}

0 commit comments

Comments
 (0)