diff --git a/tracer/src/Datadog.Trace/ExtensionMethods/DictionaryExtensions.cs b/tracer/src/Datadog.Trace/ExtensionMethods/DictionaryExtensions.cs index 8800f863ca20..2f9250dd6772 100644 --- a/tracer/src/Datadog.Trace/ExtensionMethods/DictionaryExtensions.cs +++ b/tracer/src/Datadog.Trace/ExtensionMethods/DictionaryExtensions.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using Datadog.Trace.Util; namespace Datadog.Trace.ExtensionMethods @@ -128,5 +129,42 @@ public static TV Get(this Dictionary map, TK key, Func c map[key] = newVal; return newVal; } + + /// + /// Checks if two dictionaries contain the same keys and values. + /// Note that this method assumes the two dictionaries use the same + /// ; using different comparers in each + /// dictionary is not supported. + /// + public static bool SequenceEqual( + this ReadOnlyDictionary dict1, + ReadOnlyDictionary dict2, + StringComparison valueComparison = StringComparison.Ordinal) + { + if (dict1 is null & dict2 is null) + { + return true; + } + + if (dict1 is null || dict2 is null || dict1.Count != dict2.Count) + { + return false; + } + + if (dict1.Count == 0 && dict2.Count == 0) + { + return true; + } + + foreach (var kvp1 in dict1) + { + if (!dict2.TryGetValue(kvp1.Key, out var val2) || !string.Equals(kvp1.Value, val2, valueComparison)) + { + return false; + } + } + + return true; + } } } diff --git a/tracer/test/Datadog.Trace.Tests/ExtensionMethods/DictionaryExtensionsTests.cs b/tracer/test/Datadog.Trace.Tests/ExtensionMethods/DictionaryExtensionsTests.cs new file mode 100644 index 000000000000..aa16d30340c8 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/ExtensionMethods/DictionaryExtensionsTests.cs @@ -0,0 +1,109 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Datadog.Trace.ExtensionMethods; +using Datadog.Trace.Util; +using FluentAssertions; +using Xunit; + +namespace Datadog.Trace.Tests.ExtensionMethods; + +public class DictionaryExtensionsTests +{ + [Fact] + public void SequenceEqual_HandlesBothNull() + { + ReadOnlyDictionary dict1 = null; + dict1!.SequenceEqual(null).Should().BeTrue(); + } + + [Fact] + public void SequenceEqual_HandlesOneNull() + { + ReadOnlyDictionary dict1 = null; + ReadOnlyDictionary dict2 = ReadOnlyDictionary.Empty; + dict1!.SequenceEqual(dict2).Should().BeFalse(); + dict2!.SequenceEqual(dict1).Should().BeFalse(); + } + + [Fact] + public void SequenceEqual_HandlesEmpty() + { + ReadOnlyDictionary dict1 = ReadOnlyDictionary.Empty; + ReadOnlyDictionary dict2 = ReadOnlyDictionary.Empty; + dict1.SequenceEqual(dict2).Should().BeTrue(); + dict2.SequenceEqual(dict1).Should().BeTrue(); + } + + [Fact] + public void SequenceEqual_HandlesDifferentSizes() + { + ReadOnlyDictionary dict1 = new(new Dictionary { { "key", "value" } }); + ReadOnlyDictionary dict2 = ReadOnlyDictionary.Empty; + dict1.SequenceEqual(dict2).Should().BeFalse(); + dict2.SequenceEqual(dict1).Should().BeFalse(); + } + + [Fact] + public void SequenceEqual_HandlesDifferentKeys() + { + ReadOnlyDictionary dict1 = new(new Dictionary { { "key1", "value" } }); + ReadOnlyDictionary dict2 = new(new Dictionary { { "key2", "value" } }); + dict1.SequenceEqual(dict2).Should().BeFalse(); + dict2.SequenceEqual(dict1).Should().BeFalse(); + } + + [Theory] + [InlineData("value2")] + [InlineData("VALUE1")] + [InlineData("value1 ")] + [InlineData(" value1 ")] + [InlineData("")] + [InlineData(null)] + public void SequenceEqual_HandlesDifferentValues(string value) + { + ReadOnlyDictionary dict1 = new(new Dictionary { { "key", "value1" } }); + ReadOnlyDictionary dict2 = new(new Dictionary { { "key", value } }); + dict1.SequenceEqual(dict2).Should().BeFalse(); + dict2.SequenceEqual(dict1).Should().BeFalse(); + } + + [Fact] + public void SequenceEqual_UsesDictionaryKeyComparer() + { + ReadOnlyDictionary dict1 = new(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "key", "value" } }); + ReadOnlyDictionary dict2 = new(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "KEY", "value" } }); + dict1.SequenceEqual(dict2).Should().BeTrue(); + dict2.SequenceEqual(dict1).Should().BeTrue(); + } + + [Fact] + public void SequenceEqual_UsesDictionaryWithDifferentComparersIsNotSupported() + { + ReadOnlyDictionary dict1 = new(new Dictionary(StringComparer.Ordinal) { { "key", "value" } }); + ReadOnlyDictionary dict2 = new(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "KEY", "value" } }); + + // reversing the order changes the result + dict1.SequenceEqual(dict2).Should().BeTrue(); + dict2.SequenceEqual(dict1).Should().BeFalse(); + } + + [Theory] + [InlineData(StringComparison.Ordinal, "value", true)] + [InlineData(StringComparison.Ordinal, "VALUE", false)] + [InlineData(StringComparison.OrdinalIgnoreCase, "VALUE", true)] + public void SequenceEqual_UsesComparer(StringComparison comparison, string value, bool expected) + { + ReadOnlyDictionary dict1 = new(new Dictionary { { "key", "value" } }); + ReadOnlyDictionary dict2 = new(new Dictionary { { "key", value } }); + + // reversing the order changes the result + dict1.SequenceEqual(dict2, comparison).Should().Be(expected); + dict2.SequenceEqual(dict1, comparison).Should().Be(expected); + } +}