diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index dd6080891ee..cd760667e30 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -292,6 +292,8 @@ Polly.ResilienceStrategyBuilderBase.OnCreatingStrategy.set -> void Polly.ResilienceStrategyBuilderBase.Properties.get -> Polly.ResilienceProperties! Polly.ResilienceStrategyBuilderBase.Randomizer.get -> System.Func! Polly.ResilienceStrategyBuilderBase.Randomizer.set -> void +Polly.ResilienceStrategyBuilderBase.Validator.get -> System.Action! +Polly.ResilienceStrategyBuilderBase.Validator.set -> void Polly.ResilienceStrategyBuilderContext Polly.ResilienceStrategyBuilderContext.BuilderInstanceName.get -> string? Polly.ResilienceStrategyBuilderContext.BuilderName.get -> string? @@ -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 diff --git a/src/Polly.Core/Registry/ResilienceStrategyRegistry.cs b/src/Polly.Core/Registry/ResilienceStrategyRegistry.cs index 298f9d89438..f844d2ab436 100644 --- a/src/Polly.Core/Registry/ResilienceStrategyRegistry.cs +++ b/src/Polly.Core/Registry/ResilienceStrategyRegistry.cs @@ -46,8 +46,10 @@ public ResilienceStrategyRegistry() public ResilienceStrategyRegistry(ResilienceStrategyRegistryOptions 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>>(options.BuilderComparer); diff --git a/src/Polly.Core/ResilienceStrategyBuilderBase.cs b/src/Polly.Core/ResilienceStrategyBuilderBase.cs index 4aeb59ed874..978c4ee2409 100644 --- a/src/Polly.Core/ResilienceStrategyBuilderBase.cs +++ b/src/Polly.Core/ResilienceStrategyBuilderBase.cs @@ -16,6 +16,7 @@ public abstract class ResilienceStrategyBuilderBase { private readonly List _entries = new(); private bool _used; + private Action _validator = ValidationHelper.ValidateObject; private protected ResilienceStrategyBuilderBase() { @@ -92,6 +93,21 @@ private protected ResilienceStrategyBuilderBase(ResilienceStrategyBuilderBase ot [Required] public Func Randomizer { get; set; } = RandomUtil.Instance.NextDouble; + /// + /// Gets or sets the validator that is used for the validation. + /// + /// The default value is a validation function that uses data annotations for validation. + /// + /// The validator should throw when the validated instance is invalid. + /// + /// Thrown when the attempting to assign to this property. + [EditorBrowsable(EditorBrowsableState.Never)] + public Action Validator + { + get => _validator; + set => _validator = Guard.NotNull(value); + } + internal abstract bool IsGenericBuilder { get; } internal void AddStrategyCore(Func factory, ResilienceStrategyOptions options) @@ -99,7 +115,7 @@ internal void AddStrategyCore(Func +/// The validation context that encapsulates parameters for the validation. +/// +public sealed class ResilienceValidationContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The instance being validated. + /// The primary validation message. + public ResilienceValidationContext(object instance, string primaryMessage) + { + Instance = Guard.NotNull(instance); + PrimaryMessage = Guard.NotNull(primaryMessage); + } + + /// + /// Gets the instance being validated. + /// + public object Instance { get; } + + /// + /// Gets the primary validation message. + /// + /// + /// The primary message is displayed first followed by the details about the validation errors. + /// + public string PrimaryMessage { get; } +} + diff --git a/src/Polly.Core/Utils/ValidationHelper.cs b/src/Polly.Core/Utils/ValidationHelper.cs index 464611b6987..5fb599c7f19 100644 --- a/src/Polly.Core/Utils/ValidationHelper.cs +++ b/src/Polly.Core/Utils/ValidationHelper.cs @@ -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(); - 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:"); diff --git a/src/Polly.Extensions/Polly.Extensions.csproj b/src/Polly.Extensions/Polly.Extensions.csproj index 0c9fbbe7862..f81a4b6dfa4 100644 --- a/src/Polly.Extensions/Polly.Extensions.csproj +++ b/src/Polly.Extensions/Polly.Extensions.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategyBuilderExtensions.cs b/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategyBuilderExtensions.cs index 66f6b64614d..c3e2383eaaf 100644 --- a/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategyBuilderExtensions.cs @@ -48,10 +48,8 @@ public static TBuilder ConfigureTelemetry(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( diff --git a/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs b/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs index 8a8a15fb847..baf58a9006e 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs @@ -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] @@ -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] @@ -83,7 +83,7 @@ public void InvalidOptions_Validate() }; options - .Invoking(o => ValidationHelper.ValidateObject(o, "Dummy.")) + .Invoking(o => ValidationHelper.ValidateObject(new(o, "Dummy."))) .Should() .Throw() .WithMessage(""" diff --git a/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs b/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs index 3b22e052ecc..e5aeec6ae96 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs @@ -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] @@ -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] @@ -75,7 +75,7 @@ public void InvalidOptions_Validate() }; options - .Invoking(o => ValidationHelper.ValidateObject(o, "Dummy.")) + .Invoking(o => ValidationHelper.ValidateObject(new(o, "Dummy."))) .Should() .Throw() .WithMessage(""" diff --git a/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs b/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs index fb43c955561..e1f9241e7ef 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs @@ -38,7 +38,7 @@ public void Validation() }; options - .Invoking(o => ValidationHelper.ValidateObject(o, "Invalid.")) + .Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid."))) .Should() .Throw() .WithMessage(""" diff --git a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs index 0965e710f63..6465b4a8dde 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs @@ -71,7 +71,7 @@ public void Validation() }; options - .Invoking(o => ValidationHelper.ValidateObject(o, "Invalid.")) + .Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid."))) .Should() .Throw() .WithMessage(""" diff --git a/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs b/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs index 05aa6eaa0b3..40f703af48b 100644 --- a/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs +++ b/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using System.Globalization; using Polly.Registry; using Polly.Retry; @@ -35,7 +34,7 @@ public void Ctor_InvalidOptions_Throws() { this.Invoking(_ => new ResilienceStrategyRegistry(new ResilienceStrategyRegistryOptions { BuilderFactory = null! })) .Should() - .Throw().WithMessage("The resilience strategy registry options are invalid.*"); + .Throw(); } [Fact] diff --git a/test/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs b/test/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs index 892d6f16a03..72bb3cf9cef 100644 --- a/test/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs @@ -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; @@ -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() + .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(); + } + [Fact] public void AddStrategy_MultipleNonDelegating_Ok() { diff --git a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs index 1b482313187..d7a9740f804 100644 --- a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs @@ -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() .WithMessage(""" diff --git a/test/Polly.Core.Tests/Timeout/TimeoutStrategyOptionsTests.cs b/test/Polly.Core.Tests/Timeout/TimeoutStrategyOptionsTests.cs index a313530b0c2..ea17556bc0c 100644 --- a/test/Polly.Core.Tests/Timeout/TimeoutStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Timeout/TimeoutStrategyOptionsTests.cs @@ -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(); } @@ -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(); } diff --git a/test/Polly.Extensions.Tests/Telemetry/TelemetryResilienceStrategyBuilderExtensionsTests.cs b/test/Polly.Extensions.Tests/Telemetry/TelemetryResilienceStrategyBuilderExtensionsTests.cs index 5cec61ad7bd..a72df5750ed 100644 --- a/test/Polly.Extensions.Tests/Telemetry/TelemetryResilienceStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Extensions.Tests/Telemetry/TelemetryResilienceStrategyBuilderExtensionsTests.cs @@ -7,43 +7,22 @@ namespace Polly.Extensions.Tests.Telemetry; public class TelemetryResilienceStrategyBuilderExtensionsTests { private readonly ResilienceStrategyBuilder _builder = new(); - private readonly ResilienceStrategyBuilder _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(); - } - else - { - _builder.ConfigureTelemetry(NullLoggerFactory.Instance); - _builder.DiagnosticSource.Should().BeOfType(); - _builder.AddStrategy(new TestResilienceStrategy()).Build().Should().NotBeOfType(); - } + _builder.ConfigureTelemetry(NullLoggerFactory.Instance); + _builder.DiagnosticSource.Should().BeOfType(); + _builder.AddStrategy(new TestResilienceStrategy()).Build().Should().NotBeOfType(); } - [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); @@ -59,20 +38,7 @@ public void ConfigureTelemetry_InvalidOptions_Throws() })).Should() .Throw() .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() - .WithMessage(""" - The resilience telemetry options are invalid. + The 'TelemetryOptions' are invalid. Validation Errors: The LoggerFactory field is required.