From db54ec749f2eba03acf5b58c0645ddcea559d238 Mon Sep 17 00:00:00 2001 From: martintmk Date: Sat, 18 Mar 2023 13:06:48 +0100 Subject: [PATCH 1/4] Introduce ResilienceProperties --- .../Builder/ResilienceStrategyBuilderTests.cs | 2 + .../ResilienceContextTests.cs | 3 +- .../ResiliencePropertiesTests.cs | 92 ++++++++++++++ .../ResiliencePropertyKeyTests.cs | 22 ++++ .../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 | 30 +++++ 12 files changed, 297 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..bd501eff359 --- /dev/null +++ b/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs @@ -0,0 +1,22 @@ +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!)); + } +} 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..820e01d952a 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 ResilienceProperties(); } 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..8a5a546f907 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 ResilienceProperties(); + /// /// 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..203183a4275 --- /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 Dictionary(); + + /// + /// Gets the value of a given property. + /// + /// Strongly typed key to get the value of property. + /// Returns the value of the property. + /// The type of property value as defined by parameter. + /// True, if an property is 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 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 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..9ad6ac9cdc0 --- /dev/null +++ b/src/Polly.Core/ResiliencePropertyKey.cs @@ -0,0 +1,30 @@ +namespace Polly; + +#pragma warning disable CA1815 // Override equals and operator equals on value types, this type should never be used as key to dictionary + +/// +/// Represents a key used by . +/// +/// The type of the value of the property. +public readonly struct ResiliencePropertyKey +{ + /// + /// 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; +} + From 83c8e058091352426974772d5878009975fbf6f3 Mon Sep 17 00:00:00 2001 From: martintmk Date: Mon, 20 Mar 2023 08:49:36 +0100 Subject: [PATCH 2/4] PR comments --- src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs | 1 + .../Builder/ResilienceStrategyBuilderOptions.cs | 2 +- src/Polly.Core/ResilienceContext.cs | 2 +- src/Polly.Core/ResilienceProperties.cs | 10 +++++----- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs b/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs index bd501eff359..c738e7be560 100644 --- a/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs +++ b/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs @@ -3,6 +3,7 @@ using Xunit; namespace Polly.Core.Tests; + public class ResiliencePropertyKeyTests { [Fact] diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs index 820e01d952a..b3e745a36d9 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs @@ -17,5 +17,5 @@ public class ResilienceStrategyBuilderOptions /// /// Gets the custom properties attached to builder options. /// - public ResilienceProperties Properties { get; } = new ResilienceProperties(); + public ResilienceProperties Properties { get; } = new(); } diff --git a/src/Polly.Core/ResilienceContext.cs b/src/Polly.Core/ResilienceContext.cs index 8a5a546f907..bc53c78de6f 100644 --- a/src/Polly.Core/ResilienceContext.cs +++ b/src/Polly.Core/ResilienceContext.cs @@ -51,7 +51,7 @@ private ResilienceContext() /// /// Gets the custom properties attached to the context. /// - public ResilienceProperties Properties { get; } = new ResilienceProperties(); + public ResilienceProperties Properties { get; } = new(); /// /// Gets a instance from the pool. diff --git a/src/Polly.Core/ResilienceProperties.cs b/src/Polly.Core/ResilienceProperties.cs index 203183a4275..908b25d3cb5 100644 --- a/src/Polly.Core/ResilienceProperties.cs +++ b/src/Polly.Core/ResilienceProperties.cs @@ -9,15 +9,15 @@ namespace Polly; /// public sealed class ResilienceProperties : IDictionary { - private Dictionary Options { get; } = new Dictionary(); + private Dictionary Options { get; } = new(); /// /// Gets the value of a given property. /// - /// Strongly typed key to get the value of 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 an property is retrieved. + /// 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) @@ -33,7 +33,7 @@ public bool TryGetValue(ResiliencePropertyKey key, [MaybeNullWhe /// /// Gets the value of a given property with a fallback default value. /// - /// Strongly typed key to get the value of property. + /// 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. @@ -50,7 +50,7 @@ public TValue GetValue(ResiliencePropertyKey key, TValue default /// /// Sets the value of a given property. /// - /// Strongly typed key to get the value of 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) From 2162debb5bd2cef9df6878ad7178fd7b6c0c7cdc Mon Sep 17 00:00:00 2001 From: martintmk Date: Mon, 20 Mar 2023 09:00:28 +0100 Subject: [PATCH 3/4] Add equality members --- .../ResiliencePropertyKeyTests.cs | 28 ++++++++++++++++++ src/Polly.Core/ResiliencePropertyKey.cs | 29 +++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs b/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs index c738e7be560..545b07dc464 100644 --- a/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs +++ b/src/Polly.Core.Tests/ResiliencePropertyKeyTests.cs @@ -20,4 +20,32 @@ 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/ResiliencePropertyKey.cs b/src/Polly.Core/ResiliencePropertyKey.cs index 9ad6ac9cdc0..79c34fea8cc 100644 --- a/src/Polly.Core/ResiliencePropertyKey.cs +++ b/src/Polly.Core/ResiliencePropertyKey.cs @@ -1,12 +1,10 @@ namespace Polly; -#pragma warning disable CA1815 // Override equals and operator equals on value types, this type should never be used as key to dictionary - /// /// Represents a key used by . /// /// The type of the value of the property. -public readonly struct ResiliencePropertyKey +public readonly struct ResiliencePropertyKey : IEquatable> { /// /// Initializes a new instance of the struct. @@ -26,5 +24,30 @@ public ResiliencePropertyKey(string key) /// 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); } From 1c77935ff45ee9a872c12b8396f96597e60c8d3a Mon Sep 17 00:00:00 2001 From: martintmk Date: Mon, 20 Mar 2023 09:34:16 +0100 Subject: [PATCH 4/4] Build failure --- src/Polly.Core/ResiliencePropertyKey.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Polly.Core/ResiliencePropertyKey.cs b/src/Polly.Core/ResiliencePropertyKey.cs index 79c34fea8cc..b01f2f05822 100644 --- a/src/Polly.Core/ResiliencePropertyKey.cs +++ b/src/Polly.Core/ResiliencePropertyKey.cs @@ -26,7 +26,7 @@ public ResiliencePropertyKey(string key) public override string ToString() => Key; /// /// - public override bool Equals(object obj) => obj is ResiliencePropertyKey other && Equals(other); + public override bool Equals(object? obj) => obj is ResiliencePropertyKey other && Equals(other); /// public bool Equals(ResiliencePropertyKey other) => StringComparer.Ordinal.Equals(Key, other.Key);