From 0b3e741963355dbc741acce394496273ae332ab1 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Mon, 20 Mar 2023 10:06:22 +0100 Subject: [PATCH] Introduce ResilienceProperties (#1062) --- .../Builder/ResilienceStrategyBuilderTests.cs | 2 + .../ResilienceContextTests.cs | 3 +- .../ResiliencePropertiesTests.cs | 92 ++++++++++++++ .../ResiliencePropertyKeyTests.cs | 51 ++++++++ .../Builder/ResilienceStrategyBuilder.cs | 1 + .../ResilienceStrategyBuilderContext.cs | 18 +-- .../ResilienceStrategyBuilderOptions.cs | 5 + .../CallerArgumentExpressionAttribute.cs | 0 .../LegacySupport/MaybeNullWhenAttribute.cs | 12 ++ src/Polly.Core/ResilienceContext.cs | 6 + src/Polly.Core/ResilienceProperties.cs | 114 ++++++++++++++++++ src/Polly.Core/ResiliencePropertyKey.cs | 53 ++++++++ 12 files changed, 349 insertions(+), 8 deletions(-) create mode 100644 src/Polly.Core.Tests/ResiliencePropertiesTests.cs create mode 100644 src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs rename src/Polly.Core/{Utils => LegacySupport}/CallerArgumentExpressionAttribute.cs (100%) create mode 100644 src/Polly.Core/LegacySupport/MaybeNullWhenAttribute.cs create mode 100644 src/Polly.Core/ResilienceProperties.cs create mode 100644 src/Polly.Core/ResiliencePropertyKey.cs diff --git a/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs index 1ddaeb2df9d..68cd3cd4df9 100644 --- a/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs +++ b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs @@ -260,6 +260,7 @@ public void BuildStrategy_EnsureCorrectContext() context.BuilderName.Should().Be("builder-name"); context.StrategyName.Should().Be("strategy-name"); context.StrategyType.Should().Be("strategy-type"); + context.BuilderProperties.Should().BeSameAs(builder.Options.Properties); verified1 = true; return new TestResilienceStrategy(); @@ -272,6 +273,7 @@ public void BuildStrategy_EnsureCorrectContext() context.BuilderName.Should().Be("builder-name"); context.StrategyName.Should().Be("strategy-name-2"); context.StrategyType.Should().Be("strategy-type-2"); + context.BuilderProperties.Should().BeSameAs(builder.Options.Properties); verified2 = true; return new TestResilienceStrategy(); diff --git a/src/Polly.Core.Tests/ResilienceContextTests.cs b/src/Polly.Core.Tests/ResilienceContextTests.cs index 91493596a3c..2c83a294836 100644 --- a/src/Polly.Core.Tests/ResilienceContextTests.cs +++ b/src/Polly.Core.Tests/ResilienceContextTests.cs @@ -33,7 +33,7 @@ public void Return_EnsureDefaults() context.CancellationToken = cts.Token; context.Initialize(true); context.CancellationToken.Should().Be(cts.Token); - + context.Properties.Set(new ResiliencePropertyKey("abc"), 10); ResilienceContext.Return(context); AssertDefaults(context); @@ -77,5 +77,6 @@ private static void AssertDefaults(ResilienceContext context) context.ResultType.Name.Should().Be("UnknownResult"); context.IsSynchronous.Should().BeFalse(); context.CancellationToken.Should().Be(CancellationToken.None); + context.Properties.Should().BeEmpty(); } } diff --git a/src/Polly.Core.Tests/ResiliencePropertiesTests.cs b/src/Polly.Core.Tests/ResiliencePropertiesTests.cs new file mode 100644 index 00000000000..40bc5136227 --- /dev/null +++ b/src/Polly.Core.Tests/ResiliencePropertiesTests.cs @@ -0,0 +1,92 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Polly.Core.Tests; + +public class ResiliencePropertiesTests +{ + [Fact] + public void TryGetValue_Ok() + { + var key = new ResiliencePropertyKey("dummy"); + var props = new ResilienceProperties(); + + props.Set(key, 12345); + + props.TryGetValue(key, out var val).Should().Be(true); + val.Should().Be(12345); + } + + [Fact] + public void TryGetValue_NotFound_Ok() + { + var key = new ResiliencePropertyKey("dummy"); + var props = new ResilienceProperties(); + + props.TryGetValue(key, out var val).Should().Be(false); + } + + [Fact] + public void GetValue_Ok() + { + var key = new ResiliencePropertyKey("dummy"); + var props = new ResilienceProperties(); + + props.Set(key, 12345); + + props.GetValue(key, default).Should().Be(12345); + } + + [Fact] + public void GetValue_NotFound_EnsureDefault() + { + var key = new ResiliencePropertyKey("dummy"); + var props = new ResilienceProperties(); + + props.GetValue(key, -1).Should().Be(-1); + } + + [Fact] + public void TryGetValue_IncorrectType_NotFound() + { + var key1 = new ResiliencePropertyKey("dummy"); + var key2 = new ResiliencePropertyKey("dummy"); + + var props = new ResilienceProperties(); + + props.Set(key1, 12345); + + props.TryGetValue(key2, out var val).Should().Be(false); + } + + [Fact] + public void DictionaryOperations_Ok() + { + IDictionary dict = new ResilienceProperties(); + + dict.TryGetValue("xyz", out var _).Should().BeFalse(); + dict.GetEnumerator().Should().NotBeNull(); + ((IEnumerable)dict).GetEnumerator().Should().NotBeNull(); + dict.IsReadOnly.Should().BeFalse(); + dict.Count.Should().Be(0); + dict.Add("dummy", 12345L); + dict.Values.Should().Contain(12345L); + dict.Keys.Should().Contain("dummy"); + dict.ContainsKey("dummy").Should().BeTrue(); + dict.Contains(new KeyValuePair("dummy", 12345L)).Should().BeTrue(); + dict.Add("dummy2", "xyz"); + dict["dummy2"].Should().Be("xyz"); + dict["dummy3"] = "abc"; + dict["dummy3"].Should().Be("abc"); + dict.Remove("dummy2").Should().BeTrue(); + dict.Remove(new KeyValuePair("not-exists", "abc")).Should().BeFalse(); + dict.Clear(); + dict.Count.Should().Be(0); + dict.Add(new KeyValuePair("dummy", "abc")); + var array = new KeyValuePair[1]; + dict.CopyTo(array, 0); + array[0].Key.Should().Be("dummy"); + array[0].Value.Should().Be("abc"); + } +} diff --git a/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs b/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs new file mode 100644 index 00000000000..545b07dc464 --- /dev/null +++ b/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs @@ -0,0 +1,51 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Polly.Core.Tests; + +public class ResiliencePropertyKeyTests +{ + [Fact] + public void Ctor_Ok() + { + var instance = new ResiliencePropertyKey("dummy"); + + instance.Key.Should().Be("dummy"); + instance.ToString().Should().Be("dummy"); + } + + [Fact] + public void Ctor_Null_Throws() + { + Assert.Throws(() => new ResiliencePropertyKey(null!)); + } + + [Fact] + public void Equality_Ok() + { + var key1 = new ResiliencePropertyKey("dummy"); + var key2 = new ResiliencePropertyKey("dummy"); + + key1.Equals(key2).Should().BeTrue(); + key1.Equals(new ResiliencePropertyKey("dummy2")).Should().BeFalse(); + key1.Equals(new ResiliencePropertyKey("dummy")).Should().BeFalse(); + + key1.Equals((object)key2).Should().BeTrue(); + key1.Equals((object)new ResiliencePropertyKey("dummy2")).Should().BeFalse(); + + (key1 == key2).Should().BeTrue(); + (key1 != key2).Should().BeFalse(); + } + + [Fact] + public void GetHashCode_Ok() + { + var key1 = new ResiliencePropertyKey("dummy"); + var key2 = new ResiliencePropertyKey("dummy"); + + key1.GetHashCode().Should().Be(key2.GetHashCode()); + key1.GetHashCode().Should().NotBe(new ResiliencePropertyKey("dummy2").GetHashCode()); + key1.GetHashCode().Should().NotBe(new ResiliencePropertyKey("dummy").GetHashCode()); + } +} diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs index 5b695b921da..55ad51bb297 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs @@ -90,6 +90,7 @@ private ResilienceStrategy CreateResilienceStrategy(Entry entry) { var context = new ResilienceStrategyBuilderContext( builderName: Options.BuilderName, + builderProperties: Options.Properties, strategyName: entry.Properties.StrategyName, strategyType: entry.Properties.StrategyType); diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs index ed9bcc18c6f..b4afb691377 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs @@ -5,15 +5,14 @@ namespace Polly.Builder; /// public class ResilienceStrategyBuilderContext { - /// - /// Initializes a new instance of the class. - /// - /// The name of the builder. - /// The strategy name. - /// The strategy type. - public ResilienceStrategyBuilderContext(string builderName, string strategyName, string strategyType) + internal ResilienceStrategyBuilderContext( + string builderName, + ResilienceProperties builderProperties, + string strategyName, + string strategyType) { BuilderName = Guard.NotNull(builderName); + BuilderProperties = Guard.NotNull(builderProperties); StrategyName = Guard.NotNull(strategyName); StrategyType = Guard.NotNull(strategyType); } @@ -23,6 +22,11 @@ public ResilienceStrategyBuilderContext(string builderName, string strategyName, /// public string BuilderName { get; } + /// + /// Gets the custom properties attached to the builder. + /// + public ResilienceProperties BuilderProperties { get; } + /// /// Gets the name of the strategy. /// diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs index 654e4d5ba37..b3e745a36d9 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs @@ -13,4 +13,9 @@ public class ResilienceStrategyBuilderOptions /// This property is also included in the telemetry that is produced by the individual resilience strategies. [Required(AllowEmptyStrings = true)] public string BuilderName { get; set; } = string.Empty; + + /// + /// Gets the custom properties attached to builder options. + /// + public ResilienceProperties Properties { get; } = new(); } diff --git a/src/Polly.Core/Utils/CallerArgumentExpressionAttribute.cs b/src/Polly.Core/LegacySupport/CallerArgumentExpressionAttribute.cs similarity index 100% rename from src/Polly.Core/Utils/CallerArgumentExpressionAttribute.cs rename to src/Polly.Core/LegacySupport/CallerArgumentExpressionAttribute.cs diff --git a/src/Polly.Core/LegacySupport/MaybeNullWhenAttribute.cs b/src/Polly.Core/LegacySupport/MaybeNullWhenAttribute.cs new file mode 100644 index 00000000000..430d10a6884 --- /dev/null +++ b/src/Polly.Core/LegacySupport/MaybeNullWhenAttribute.cs @@ -0,0 +1,12 @@ +#if NETFRAMEWORK || NETSTANDARD + +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class MaybeNullWhenAttribute : Attribute +{ + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + public bool ReturnValue { get; } +} +#endif \ No newline at end of file diff --git a/src/Polly.Core/ResilienceContext.cs b/src/Polly.Core/ResilienceContext.cs index 41786752921..bc53c78de6f 100644 --- a/src/Polly.Core/ResilienceContext.cs +++ b/src/Polly.Core/ResilienceContext.cs @@ -48,6 +48,11 @@ private ResilienceContext() /// internal bool IsInitialized => ResultType != typeof(UnknownResult); + /// + /// Gets the custom properties attached to the context. + /// + public ResilienceProperties Properties { get; } = new(); + /// /// Gets a instance from the pool. /// @@ -91,6 +96,7 @@ private void Reset() ResultType = typeof(UnknownResult); ContinueOnCapturedContext = false; CancellationToken = default; + ((IDictionary)Properties).Clear(); } /// diff --git a/src/Polly.Core/ResilienceProperties.cs b/src/Polly.Core/ResilienceProperties.cs new file mode 100644 index 00000000000..908b25d3cb5 --- /dev/null +++ b/src/Polly.Core/ResilienceProperties.cs @@ -0,0 +1,114 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Polly; + +#pragma warning disable CA1710 // Identifiers should have correct suffix + +/// +/// Represents a collection of custom resilience properties. +/// +public sealed class ResilienceProperties : IDictionary +{ + private Dictionary Options { get; } = new(); + + /// + /// Gets the value of a given property. + /// + /// Strongly typed key to get the value of the property. + /// Returns the value of the property. + /// The type of property value as defined by parameter. + /// True, if a property was retrieved. + public bool TryGetValue(ResiliencePropertyKey key, [MaybeNullWhen(false)] out TValue value) + { + if (Options.TryGetValue(key.Key, out object? val) && val is TValue typedValue) + { + value = typedValue; + return true; + } + + value = default; + return false; + } + + /// + /// Gets the value of a given property with a fallback default value. + /// + /// Strongly typed key to get the value of the property. + /// The default value to use if property is not found. + /// The type of property value as defined by parameter. + /// The property value or the default value. + public TValue GetValue(ResiliencePropertyKey key, TValue defaultValue) + { + if (TryGetValue(key, out var value)) + { + return value; + } + + return defaultValue; + } + + /// + /// Sets the value of a given property. + /// + /// Strongly typed key to get the value of the property. + /// Returns the value of the property. + /// The type of property value as defined by parameter. + public void Set(ResiliencePropertyKey key, TValue value) + { + Options[key.Key] = value; + } + + /// + object? IDictionary.this[string key] + { + get => Options[key]; + set => Options[key] = value; + } + + /// + ICollection IDictionary.Keys => Options.Keys; + + /// + ICollection IDictionary.Values => Options.Values; + + /// + int ICollection>.Count => Options.Count; + + /// + bool ICollection>.IsReadOnly => ((IDictionary)Options).IsReadOnly; + + /// + void IDictionary.Add(string key, object? value) => Options.Add(key, value); + + /// + void ICollection>.Add(KeyValuePair item) => ((IDictionary)Options).Add(item); + + /// + void ICollection>.Clear() => Options.Clear(); + + /// + bool ICollection>.Contains(KeyValuePair item) => ((IDictionary)Options).Contains(item); + + /// + bool IDictionary.ContainsKey(string key) => Options.ContainsKey(key); + + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => + ((IDictionary)Options).CopyTo(array, arrayIndex); + + /// + IEnumerator> IEnumerable>.GetEnumerator() => Options.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Options).GetEnumerator(); + + /// + bool IDictionary.Remove(string key) => Options.Remove(key); + + /// + bool ICollection>.Remove(KeyValuePair item) => ((IDictionary)Options).Remove(item); + + /// + bool IDictionary.TryGetValue(string key, out object? value) => Options.TryGetValue(key, out value); +} + diff --git a/src/Polly.Core/ResiliencePropertyKey.cs b/src/Polly.Core/ResiliencePropertyKey.cs new file mode 100644 index 00000000000..b01f2f05822 --- /dev/null +++ b/src/Polly.Core/ResiliencePropertyKey.cs @@ -0,0 +1,53 @@ +namespace Polly; + +/// +/// Represents a key used by . +/// +/// The type of the value of the property. +public readonly struct ResiliencePropertyKey : IEquatable> +{ + /// + /// Initializes a new instance of the struct. + /// + /// The key name. + public ResiliencePropertyKey(string key) + { + Guard.NotNull(key); + + Key = key; + } + + /// + /// Gets the name of the key. + /// + public string Key { get; } + + /// + public override string ToString() => Key; + + /// /// + public override bool Equals(object? obj) => obj is ResiliencePropertyKey other && Equals(other); + + /// + public bool Equals(ResiliencePropertyKey other) => StringComparer.Ordinal.Equals(Key, other.Key); + + /// + public override int GetHashCode() => (StringComparer.Ordinal.GetHashCode(Key), typeof(TValue)).GetHashCode(); + + /// + /// The operator to compare two instances of for equality. + /// + /// The left instance. + /// The right instance. + /// True if the instances are equal, false otherwise. + public static bool operator ==(ResiliencePropertyKey left, ResiliencePropertyKey right) => left.Equals(right); + + /// + /// The operator to compare two instances of for inequality. + /// + /// The left instance. + /// The right instance. + /// True if the instances are not equal, false otherwise. + public static bool operator !=(ResiliencePropertyKey left, ResiliencePropertyKey right) => !(left == right); +} +