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

Introduce ResilienceStrategyBuilder.Validator #1412

Merged
merged 4 commits into from
Jul 18, 2023
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
6 changes: 6 additions & 0 deletions src/Polly.Core/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ Polly.ResilienceStrategyBuilderBase.OnCreatingStrategy.set -> void
Polly.ResilienceStrategyBuilderBase.Properties.get -> Polly.ResilienceProperties!
Polly.ResilienceStrategyBuilderBase.Randomizer.get -> System.Func<double>!
Polly.ResilienceStrategyBuilderBase.Randomizer.set -> void
Polly.ResilienceStrategyBuilderBase.Validator.get -> System.Action<Polly.ResilienceValidationContext!>!
Polly.ResilienceStrategyBuilderBase.Validator.set -> void
Polly.ResilienceStrategyBuilderContext
Polly.ResilienceStrategyBuilderContext.BuilderInstanceName.get -> string?
Polly.ResilienceStrategyBuilderContext.BuilderName.get -> string?
Expand All @@ -304,6 +306,10 @@ Polly.ResilienceStrategyOptions
Polly.ResilienceStrategyOptions.ResilienceStrategyOptions() -> void
Polly.ResilienceStrategyOptions.StrategyName.get -> string?
Polly.ResilienceStrategyOptions.StrategyName.set -> void
Polly.ResilienceValidationContext
Polly.ResilienceValidationContext.Instance.get -> object!
Polly.ResilienceValidationContext.PrimaryMessage.get -> string!
Polly.ResilienceValidationContext.ResilienceValidationContext(object! instance, string! primaryMessage) -> void
Polly.Retry.OnRetryArguments
Polly.Retry.OnRetryArguments.Attempt.get -> int
Polly.Retry.OnRetryArguments.Attempt.init -> void
Expand Down
6 changes: 4 additions & 2 deletions src/Polly.Core/Registry/ResilienceStrategyRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ public ResilienceStrategyRegistry()
public ResilienceStrategyRegistry(ResilienceStrategyRegistryOptions<TKey> options)
{
Guard.NotNull(options);

ValidationHelper.ValidateObject(options, "The resilience strategy registry options are invalid.");
Guard.NotNull(options.BuilderFactory);
Guard.NotNull(options.StrategyComparer);
Guard.NotNull(options.BuilderComparer);
Guard.NotNull(options.BuilderNameFormatter);

_activator = options.BuilderFactory;
_builders = new ConcurrentDictionary<TKey, Action<ResilienceStrategyBuilder, ConfigureBuilderContext<TKey>>>(options.BuilderComparer);
Expand Down
20 changes: 18 additions & 2 deletions src/Polly.Core/ResilienceStrategyBuilderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public abstract class ResilienceStrategyBuilderBase
{
private readonly List<Entry> _entries = new();
private bool _used;
private Action<ResilienceValidationContext> _validator = ValidationHelper.ValidateObject;

private protected ResilienceStrategyBuilderBase()
{
Expand Down Expand Up @@ -92,14 +93,29 @@ private protected ResilienceStrategyBuilderBase(ResilienceStrategyBuilderBase ot
[Required]
public Func<double> Randomizer { get; set; } = RandomUtil.Instance.NextDouble;

/// <summary>
/// Gets or sets the validator that is used for the validation.
/// </summary>
/// <value>The default value is a validation function that uses data annotations for validation.</value>
/// <remarks>
/// The validator should throw <see cref="ValidationException"/> when the validated instance is invalid.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when the attempting to assign <see langword="null"/> to this property.</exception>
[EditorBrowsable(EditorBrowsableState.Never)]
public Action<ResilienceValidationContext> Validator
{
get => _validator;
set => _validator = Guard.NotNull(value);
}

internal abstract bool IsGenericBuilder { get; }

internal void AddStrategyCore(Func<ResilienceStrategyBuilderContext, ResilienceStrategy> factory, ResilienceStrategyOptions options)
{
Guard.NotNull(factory);
Guard.NotNull(options);

ValidationHelper.ValidateObject(options, $"The '{TypeNameFormatter.Format(options.GetType())}' are invalid.");
Validator(new ResilienceValidationContext(options, $"The '{TypeNameFormatter.Format(options.GetType())}' are invalid."));

if (_used)
{
Expand All @@ -111,7 +127,7 @@ internal void AddStrategyCore(Func<ResilienceStrategyBuilderContext, ResilienceS

internal ResilienceStrategy BuildStrategy()
{
ValidationHelper.ValidateObject(this, $"The '{nameof(ResilienceStrategyBuilder)}' configuration is invalid.");
Validator(new(this, $"The '{nameof(ResilienceStrategyBuilder)}' configuration is invalid."));

_used = true;

Expand Down
32 changes: 32 additions & 0 deletions src/Polly.Core/ResilienceValidationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Polly;

/// <summary>
/// The validation context that encapsulates parameters for the validation.
/// </summary>
public sealed class ResilienceValidationContext
{
/// <summary>
/// Initializes a new instance of the <see cref="ResilienceValidationContext"/> class.
/// </summary>
/// <param name="instance">The instance being validated.</param>
/// <param name="primaryMessage">The primary validation message.</param>
public ResilienceValidationContext(object instance, string primaryMessage)
{
Instance = Guard.NotNull(instance);
PrimaryMessage = Guard.NotNull(primaryMessage);
}

/// <summary>
/// Gets the instance being validated.
/// </summary>
public object Instance { get; }

/// <summary>
/// Gets the primary validation message.
/// </summary>
/// <remarks>
/// The primary message is displayed first followed by the details about the validation errors.
/// </remarks>
public string PrimaryMessage { get; }
}

8 changes: 5 additions & 3 deletions src/Polly.Core/Utils/ValidationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ namespace Polly.Utils;
[ExcludeFromCodeCoverage]
internal static class ValidationHelper
{
public static void ValidateObject(object instance, string mainMessage)
public static void ValidateObject(ResilienceValidationContext context)
{
Guard.NotNull(context);

var errors = new List<ValidationResult>();

if (!Validator.TryValidateObject(instance, new ValidationContext(instance), errors, true))
if (!Validator.TryValidateObject(context.Instance, new ValidationContext(context.Instance), errors, true))
{
var stringBuilder = new StringBuilder(mainMessage);
var stringBuilder = new StringBuilder(context.PrimaryMessage);
stringBuilder.AppendLine();

stringBuilder.AppendLine("Validation Errors:");
Expand Down
1 change: 0 additions & 1 deletion src/Polly.Extensions/Polly.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<Compile Include="..\Polly.Core\Utils\Guard.cs" Link="Utils\Guard.cs" />
<Compile Include="..\Polly.Core\Utils\ObjectPool.cs" Link="Utils\ObjectPool.cs" />
<Compile Include="..\Polly.Core\ToBeRemoved\TimeProvider.cs" Link="ToBeRemoved\TimeProvider.cs" />
<Compile Include="..\Polly.Core\Utils\ValidationHelper.cs" Link="Utils\ValidationHelper.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@ public static TBuilder ConfigureTelemetry<TBuilder>(this TBuilder builder, Telem
Guard.NotNull(builder);
Guard.NotNull(options);

ValidationHelper.ValidateObject(options, "The resilience telemetry options are invalid.");

builder.Validator(new(options, $"The '{nameof(TelemetryOptions)}' are invalid."));
builder.DiagnosticSource = new ResilienceTelemetryDiagnosticSource(options);

builder.OnCreatingStrategy = strategies =>
{
var telemetryStrategy = new TelemetryResilienceStrategy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void Ctor_Defaults()
options.MinimumThroughput = 2;
options.SamplingDuration = TimeSpan.FromMilliseconds(500);

ValidationHelper.ValidateObject(options, "Dummy.");
ValidationHelper.ValidateObject(new(options, "Dummy."));
}

[Fact]
Expand Down Expand Up @@ -64,7 +64,7 @@ public void Ctor_Generic_Defaults()
options.MinimumThroughput = 2;
options.SamplingDuration = TimeSpan.FromMilliseconds(500);

ValidationHelper.ValidateObject(options, "Dummy.");
ValidationHelper.ValidateObject(new(options, "Dummy."));
}

[Fact]
Expand All @@ -83,7 +83,7 @@ public void InvalidOptions_Validate()
};

options
.Invoking(o => ValidationHelper.ValidateObject(o, "Dummy."))
.Invoking(o => ValidationHelper.ValidateObject(new(o, "Dummy.")))
.Should()
.Throw<ValidationException>()
.WithMessage("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void Ctor_Defaults()
options.FailureThreshold = 1;
options.BreakDuration = TimeSpan.FromMilliseconds(500);

ValidationHelper.ValidateObject(options, "Dummy.");
ValidationHelper.ValidateObject(new(options, "Dummy."));
}

[Fact]
Expand Down Expand Up @@ -58,7 +58,7 @@ public void Ctor_Generic_Defaults()
options.BreakDuration = TimeSpan.FromMilliseconds(500);

options.ShouldHandle = _ => PredicateResult.True;
ValidationHelper.ValidateObject(options, "Dummy.");
ValidationHelper.ValidateObject(new(options, "Dummy."));
}

[Fact]
Expand All @@ -75,7 +75,7 @@ public void InvalidOptions_Validate()
};

options
.Invoking(o => ValidationHelper.ValidateObject(o, "Dummy."))
.Invoking(o => ValidationHelper.ValidateObject(new(o, "Dummy.")))
.Should()
.Throw<ValidationException>()
.WithMessage("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void Validation()
};

options
.Invoking(o => ValidationHelper.ValidateObject(o, "Invalid."))
.Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid.")))
.Should()
.Throw<ValidationException>()
.WithMessage("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void Validation()
};

options
.Invoking(o => ValidationHelper.ValidateObject(o, "Invalid."))
.Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid.")))
.Should()
.Throw<ValidationException>()
.WithMessage("""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Polly.Registry;
using Polly.Retry;
Expand Down Expand Up @@ -35,7 +34,7 @@ public void Ctor_InvalidOptions_Throws()
{
this.Invoking(_ => new ResilienceStrategyRegistry<string>(new ResilienceStrategyRegistryOptions<string> { BuilderFactory = null! }))
.Should()
.Throw<ValidationException>().WithMessage("The resilience strategy registry options are invalid.*");
.Throw<ArgumentNullException>();
}

[Fact]
Expand Down
30 changes: 30 additions & 0 deletions test/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Time.Testing;
using Moq;
using Polly.Retry;
using Polly.Utils;

namespace Polly.Core.Tests;
Expand Down Expand Up @@ -122,6 +123,35 @@ public void AddStrategy_Duplicate_Throws()
.WithMessage("The resilience pipeline must contain unique resilience strategies.");
}

[Fact]
public void Validator_Ok()
{
var builder = new ResilienceStrategyBuilder();

builder.Validator.Should().NotBeNull();

builder.Validator(new ResilienceValidationContext("ABC", "ABC"));

builder
.Invoking(b => b.Validator(new ResilienceValidationContext(new RetryStrategyOptions { RetryCount = -4 }, "The primary message.")))
.Should()
.Throw<ValidationException>()
.WithMessage("""
The primary message.
Validation Errors:
The field RetryCount must be between -1 and 100.
""");
}

[Fact]
public void Validator_Null_Throws()
{
new ResilienceStrategyBuilder()
.Invoking(b => b.Validator = null!)
.Should()
.Throw<ArgumentNullException>();
}

[Fact]
public void AddStrategy_MultipleNonDelegating_Ok()
{
Expand Down
2 changes: 1 addition & 1 deletion test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void InvalidOptions()
BaseDelay = TimeSpan.MinValue
};

options.Invoking(o => ValidationHelper.ValidateObject(o, "Invalid Options"))
options.Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid Options")))
.Should()
.Throw<ValidationException>()
.WithMessage("""
Expand Down
4 changes: 2 additions & 2 deletions test/Polly.Core.Tests/Timeout/TimeoutStrategyOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public void Timeout_Invalid_EnsureValidationError(TimeSpan value)
};

options
.Invoking(o => ValidationHelper.ValidateObject(o, "Dummy message"))
.Invoking(o => ValidationHelper.ValidateObject(new(o, "Dummy message")))
.Should()
.Throw<ValidationException>();
}
Expand All @@ -41,7 +41,7 @@ public void Timeout_Valid(TimeSpan value)
};

options
.Invoking(o => ValidationHelper.ValidateObject(o, "Dummy message"))
.Invoking(o => ValidationHelper.ValidateObject(new(o, "Dummy message")))
.Should()
.NotThrow();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,22 @@ namespace Polly.Extensions.Tests.Telemetry;
public class TelemetryResilienceStrategyBuilderExtensionsTests
{
private readonly ResilienceStrategyBuilder _builder = new();
private readonly ResilienceStrategyBuilder<string> _genericBuilder = new();

[InlineData(true)]
[InlineData(false)]
[Theory]
public void ConfigureTelemetry_EnsureDiagnosticSourceUpdated(bool generic)
[Fact]
public void ConfigureTelemetry_EnsureDiagnosticSourceUpdated()
{
if (generic)
{
_genericBuilder.ConfigureTelemetry(NullLoggerFactory.Instance);
_genericBuilder.DiagnosticSource.Should().BeOfType<ResilienceTelemetryDiagnosticSource>();
}
else
{
_builder.ConfigureTelemetry(NullLoggerFactory.Instance);
_builder.DiagnosticSource.Should().BeOfType<ResilienceTelemetryDiagnosticSource>();
_builder.AddStrategy(new TestResilienceStrategy()).Build().Should().NotBeOfType<TestResilienceStrategy>();
}
_builder.ConfigureTelemetry(NullLoggerFactory.Instance);
_builder.DiagnosticSource.Should().BeOfType<ResilienceTelemetryDiagnosticSource>();
_builder.AddStrategy(new TestResilienceStrategy()).Build().Should().NotBeOfType<TestResilienceStrategy>();
}

[InlineData(true)]
[InlineData(false)]
[Theory]
public void ConfigureTelemetry_EnsureLogging(bool generic)
[Fact]
public void ConfigureTelemetry_EnsureLogging()
{
using var factory = TestUtilities.CreateLoggerFactory(out var fakeLogger);

if (generic)
{
_genericBuilder.ConfigureTelemetry(factory);
_genericBuilder.AddStrategy(new TestResilienceStrategy()).Build().Execute(_ => string.Empty);
}
else
{
_builder.ConfigureTelemetry(factory);
_builder.AddStrategy(new TestResilienceStrategy()).Build().Execute(_ => { });
}
_builder.ConfigureTelemetry(factory);
_builder.AddStrategy(new TestResilienceStrategy()).Build().Execute(_ => { });

fakeLogger.GetRecords().Should().NotBeEmpty();
fakeLogger.GetRecords().Should().HaveCount(2);
Expand All @@ -59,20 +38,7 @@ public void ConfigureTelemetry_InvalidOptions_Throws()
})).Should()
.Throw<ValidationException>()
.WithMessage("""
The resilience telemetry options are invalid.

Validation Errors:
The LoggerFactory field is required.
""");

_genericBuilder
.Invoking(b => b.ConfigureTelemetry(new TelemetryOptions
{
LoggerFactory = null!,
})).Should()
.Throw<ValidationException>()
.WithMessage("""
The resilience telemetry options are invalid.
The 'TelemetryOptions' are invalid.

Validation Errors:
The LoggerFactory field is required.
Expand Down
Loading