Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 '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