diff --git a/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs b/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs index 4465722a41e..363d0d58c9f 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs @@ -15,6 +15,7 @@ namespace System using System.ArrayExtensions; using System.Collections.Generic; using System.Reflection; + using System.Runtime.CompilerServices; [Diagnostics.CodeAnalysis.SuppressMessage("Code", "PS0025:Dictionary keys should implement IEquatable", Justification = "A DeepCopy algorithm requires reference counting necessitating dictionaries keyed on objects by reference")] @@ -123,7 +124,11 @@ public override int GetHashCode(object obj) return 0; } - return obj.GetHashCode(); + // RuntimeHelpers.GetHashCode returns the identity hash code, which avoids + // calling ValueType.GetHashCode() on structs marked with [InlineArray] + // (that throws NotSupportedException). This matches the BCL + // System.Collections.Generic.ReferenceEqualityComparer behavior. + return RuntimeHelpers.GetHashCode(obj); } } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 6a55b5f799b..b0ccc29d546 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -2451,13 +2451,26 @@ namespace NServiceBus.Transport Default = 1, Isolated = 2, } - public class DispatchProperties : System.Collections.Generic.Dictionary + public class DispatchProperties : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable { public DispatchProperties() { } public DispatchProperties(System.Collections.Generic.Dictionary properties) { } - public NServiceBus.DelayedDelivery.DelayDeliveryWith DelayDeliveryWith { get; set; } - public NServiceBus.Performance.TimeToBeReceived.DiscardIfNotReceivedBefore DiscardIfNotReceivedBefore { get; set; } - public NServiceBus.DelayedDelivery.DoNotDeliverBefore DoNotDeliverBefore { get; set; } + public DispatchProperties(System.Collections.Generic.IDictionary properties) { } + public int Count { get; } + public NServiceBus.DelayedDelivery.DelayDeliveryWith? DelayDeliveryWith { get; set; } + public NServiceBus.Performance.TimeToBeReceived.DiscardIfNotReceivedBefore? DiscardIfNotReceivedBefore { get; set; } + public NServiceBus.DelayedDelivery.DoNotDeliverBefore? DoNotDeliverBefore { get; set; } + public bool IsReadOnly { get; } + public string this[string key] { get; set; } + public System.Collections.Generic.ICollection Keys { get; } + public System.Collections.Generic.ICollection Values { get; } + public void Add(string key, string value) { } + public void Clear() { } + public bool ContainsKey(string key) { } + public System.Collections.Generic.IEnumerator> GetEnumerator() { } + public bool Remove(string key) { } + public bool TryAdd(string key, string value) { } + public bool TryGetValue(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out string value) { } } public class ErrorContext { diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 99cad102d70..3796f339459 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -66,7 +66,6 @@ NServiceBus.Settings.IReadOnlySettings NServiceBus.Settings.SettingsHolder NServiceBus.StaticHeadersConfigExtensions NServiceBus.SubscriptionMigrationModeSettings -NServiceBus.Transport.DispatchProperties NServiceBus.Transport.ErrorContext NServiceBus.Transport.IMessageDispatcher NServiceBus.Transport.IMessageReceiver diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt index 1d23aafe370..5d4598a9783 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt @@ -19,3 +19,21 @@ NServiceBus.Extensibility.ContextBag+Slot violates the following rules: - Field Value of type System.Object is a reference type. - The size cannot be determined because there are fields that are reference types. +NServiceBus.Transport.DispatchProperties+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.String is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.String is a reference type. + - The size cannot be determined because there are fields that are reference types. + +NServiceBus.Transport.ReceiveProperties+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.String is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.String is a reference type. + - The size cannot be determined because there are fields that are reference types. + diff --git a/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs b/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs index 67f86708167..c0c8fae72e5 100644 --- a/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs +++ b/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs @@ -34,7 +34,7 @@ public void Should_wrap_provided_dictionary() } [Test] - public void Should_be_same_reference_as_source() + public void Should_copy_from_provided_dictionary() { var source = new Dictionary { ["Key"] = "Value" }; var properties = new ReceiveProperties(source); diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index 0504c24f16e..1e126d3fa12 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -113,7 +113,7 @@ static TransportOperation[] ConvertToOutboxOperations(Transport.TransportOperati return transportOperations; } - static void SerializeRoutingStrategy(AddressTag addressTag, Dictionary options) + static void SerializeRoutingStrategy(AddressTag addressTag, DispatchProperties options) { switch (addressTag) { @@ -128,7 +128,7 @@ static void SerializeRoutingStrategy(AddressTag addressTag, Dictionary options) + static AddressTag DeserializeRoutingStrategy(DispatchProperties options) { if (options.Remove("Destination", out var destination)) { diff --git a/src/NServiceBus.Core/Transports/DispatchProperties.cs b/src/NServiceBus.Core/Transports/DispatchProperties.cs index 92683892374..6a54d86b465 100644 --- a/src/NServiceBus.Core/Transports/DispatchProperties.cs +++ b/src/NServiceBus.Core/Transports/DispatchProperties.cs @@ -1,19 +1,30 @@ -namespace NServiceBus.Transport; +#nullable enable + +namespace NServiceBus.Transport; using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using DelayedDelivery; using Performance.TimeToBeReceived; /// /// Describes additional properties for an outgoing message. /// -public class DispatchProperties : Dictionary +[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Name reflects domain semantics, not collection implementation.")] +public class DispatchProperties : IDictionary, IReadOnlyDictionary { //These can't be changed to be backwards compatible with previous versions of the core - static readonly string DoNotDeliverBeforeKeyName = "DeliverAt"; - static readonly string DelayDeliveryWithKeyName = "DelayDeliveryFor"; - static readonly string DiscardIfNotReceivedBeforeKeyName = "TimeToBeReceived"; + const string DoNotDeliverBeforeKeyName = "DeliverAt"; + const string DelayDeliveryWithKeyName = "DelayDeliveryFor"; + const string DiscardIfNotReceivedBeforeKeyName = "TimeToBeReceived"; + + // Dedicated fields for the three well-known properties, avoiding string-key lookups + string? deliverAt; + string? delayDeliveryFor; + string? timeToBeReceived; /// /// Creates a new instance of . @@ -23,45 +34,626 @@ public DispatchProperties() } /// - /// Creates a new instance of an copies the values from the provided dictionary. + /// Creates a new instance of and copies the values from the provided dictionary. + /// + public DispatchProperties(Dictionary properties) + : this((IDictionary)properties) + { + } + + /// + /// Creates a new instance of and copies the values from the provided dictionary. /// - public DispatchProperties(Dictionary properties) : base(properties ?? []) + public DispatchProperties(IDictionary properties) { + if (properties is null) + { + return; + } + + if (properties is DispatchProperties source) + { + // Fast path: direct field copy when source is a DispatchProperties + deliverAt = source.deliverAt; + delayDeliveryFor = source.delayDeliveryFor; + timeToBeReceived = source.timeToBeReceived; + + count = source.count; + for (int i = 0; i < count; i++) + { + slots[i] = source.slots[i]; + } + + if (source.stash is { Count: > 0 }) + { + stash = new Dictionary(source.stash); + } + + return; + } + + foreach (var kvp in properties) + { + AddInternal(kvp.Key, kvp.Value); + } } /// /// Delay message delivery to a specific . /// - public DoNotDeliverBefore DoNotDeliverBefore + public DoNotDeliverBefore? DoNotDeliverBefore { - get => ContainsKey(DoNotDeliverBeforeKeyName) - ? new DoNotDeliverBefore(DateTimeOffsetHelper.ToDateTimeOffset(this[DoNotDeliverBeforeKeyName])) + get => deliverAt is not null + ? new DoNotDeliverBefore(DateTimeOffsetHelper.ToDateTimeOffset(deliverAt)) : null; - set => this[DoNotDeliverBeforeKeyName] = DateTimeOffsetHelper.ToWireFormattedString(value.At); + set => deliverAt = value is not null ? DateTimeOffsetHelper.ToWireFormattedString(value.At) : null; } /// /// Delay message delivery by a certain . /// - public DelayDeliveryWith DelayDeliveryWith + public DelayDeliveryWith? DelayDeliveryWith { - get => ContainsKey(DelayDeliveryWithKeyName) - ? new DelayDeliveryWith(TimeSpan.Parse(this[DelayDeliveryWithKeyName])) + get => delayDeliveryFor is not null + ? new DelayDeliveryWith(TimeSpan.Parse(delayDeliveryFor)) : null; - set => this[DelayDeliveryWithKeyName] = value.Delay.ToString(); + set => delayDeliveryFor = value?.Delay.ToString(); } /// /// Discard the message after a certain period of time. /// - public DiscardIfNotReceivedBefore DiscardIfNotReceivedBefore + public DiscardIfNotReceivedBefore? DiscardIfNotReceivedBefore { - get => ContainsKey(DiscardIfNotReceivedBeforeKeyName) - ? new DiscardIfNotReceivedBefore(TimeSpan.Parse(this[DiscardIfNotReceivedBeforeKeyName])) + get => timeToBeReceived is not null + ? new DiscardIfNotReceivedBefore(TimeSpan.Parse(timeToBeReceived)) : null; - set => this[DiscardIfNotReceivedBeforeKeyName] = value.MaxTime.ToString(); + set => timeToBeReceived = value?.MaxTime.ToString(); + } + + /// + public string this[string key] + { + get + { + ArgumentNullException.ThrowIfNull(key); + if (TryGetValueFromFields(key, out var fieldValue)) + { + return fieldValue; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return slot.Value; + } + } + + if (stash is not null && stash.TryGetValue(key, out var value)) + { + return value; + } + + ThrowKeyNotFoundException(key); + return null; + } + set + { + ArgumentNullException.ThrowIfNull(key); + if (TrySetField(key, value)) + { + return; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + slot.Value = value; + return; + } + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + } + + /// + public ICollection Keys + { + get + { + var keys = new string[Count]; + int index = 0; + + if (deliverAt is not null) + { + keys[index++] = DoNotDeliverBeforeKeyName; + } + + if (delayDeliveryFor is not null) + { + keys[index++] = DelayDeliveryWithKeyName; + } + + if (timeToBeReceived is not null) + { + keys[index++] = DiscardIfNotReceivedBeforeKeyName; + } + + for (int i = 0; i < count; i++) + { + keys[index++] = slots[i].Key; + } + + if (stash is null) + { + return keys; + } + + foreach (var key in stash.Keys) + { + keys[index++] = key; + } + + return keys; + } + } + + /// + public ICollection Values + { + get + { + var values = new string[Count]; + int index = 0; + + if (deliverAt is not null) + { + values[index++] = deliverAt; + } + + if (delayDeliveryFor is not null) + { + values[index++] = delayDeliveryFor; + } + + if (timeToBeReceived is not null) + { + values[index++] = timeToBeReceived; + } + + for (int i = 0; i < count; i++) + { + values[index++] = slots[i].Value; + } + + if (stash is null) + { + return values; + } + + foreach (var value in stash.Values) + { + values[index++] = value; + } + + return values; + } + } + + /// + public int Count + { + get + { + int fieldCount = 0; + + if (deliverAt is not null) + { + fieldCount++; + } + + if (delayDeliveryFor is not null) + { + fieldCount++; + } + + if (timeToBeReceived is not null) + { + fieldCount++; + } + + return fieldCount + count + (stash?.Count ?? 0); + } + } + + /// + public bool IsReadOnly => false; + + /// + public void Add(string key, string value) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + } + + if (stash is not null && stash.ContainsKey(key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + + AddInternal(key, value); + } + + /// + public bool ContainsKey(string key) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return true; + } + } + + return stash is not null && stash.ContainsKey(key); + } + + /// + public bool Remove(string key) + { + ArgumentNullException.ThrowIfNull(key); + if (RemoveFromFields(key)) + { + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + count--; + if (i != count) + { + slots[i] = slots[count]; + } + + slots[count] = new Slot(); + return true; + } + + return stash?.Remove(key) ?? false; + } + + /// + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) + { + ArgumentNullException.ThrowIfNull(key); + if (TryGetValueFromFields(key, out var fieldValue)) + { + value = fieldValue; + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + value = slot.Value; + return true; + } + + if (stash is not null && stash.TryGetValue(key, out var stashedValue)) + { + value = stashedValue; + return true; + } + + value = null!; + return false; + } + + /// + /// Attempts to add the specified key and value to the dictionary. + /// + /// true if the key/value pair was added; false if the key already exists. + public bool TryAdd(string key, string value) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + return false; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return false; + } + } + + if (stash is not null && stash.ContainsKey(key)) + { + return false; + } + + AddInternal(key, value); + return true; + } + + /// + public void Clear() + { + deliverAt = null; + delayDeliveryFor = null; + timeToBeReceived = null; + + for (int i = 0; i < count; i++) + { + slots[i] = new Slot(); + } + + count = 0; + stash?.Clear(); + } + + /// + public IEnumerator> GetEnumerator() + { + if (deliverAt is not null) + { + yield return new KeyValuePair(DoNotDeliverBeforeKeyName, deliverAt); + } + + if (delayDeliveryFor is not null) + { + yield return new KeyValuePair(DelayDeliveryWithKeyName, delayDeliveryFor); + } + + if (timeToBeReceived is not null) + { + yield return new KeyValuePair(DiscardIfNotReceivedBeforeKeyName, timeToBeReceived); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + yield return new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + yield break; + } + + foreach (var kvp in stash) + { + yield return kvp; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + bool ContainsKeyInFields(string key) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + return true; + } + + return timeToBeReceived is not null && key == DiscardIfNotReceivedBeforeKeyName; + } + + bool TryGetValueFromFields(string key, out string value) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + value = deliverAt; + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + value = delayDeliveryFor; + return true; + } + + if (timeToBeReceived is not null && key == DiscardIfNotReceivedBeforeKeyName) + { + value = timeToBeReceived; + return true; + } + + value = null!; + return false; + } + + bool TrySetField(string key, string value) + { + switch (key) + { + case DoNotDeliverBeforeKeyName: + deliverAt = value; + return true; + case DelayDeliveryWithKeyName: + delayDeliveryFor = value; + return true; + case DiscardIfNotReceivedBeforeKeyName: + timeToBeReceived = value; + return true; + default: + return false; + } + } + + bool RemoveFromFields(string key) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + deliverAt = null; + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + delayDeliveryFor = null; + return true; + } + + if (timeToBeReceived is null || key != DiscardIfNotReceivedBeforeKeyName) + { + return false; + } + + timeToBeReceived = null; + return true; + + } + + void AddInternal(string key, string value) + { + if (TrySetField(key, value)) + { + return; + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + IEnumerable IReadOnlyDictionary.Values => Values; + + void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); + + bool ICollection>.Contains(KeyValuePair item) => TryGetValue(item.Key, out var value) && StringComparer.Ordinal.Equals(value, item.Value); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException("Destination array is not long enough to copy all items."); + } + + int index = arrayIndex; + + if (deliverAt is not null) + { + array[index++] = new KeyValuePair(DoNotDeliverBeforeKeyName, deliverAt); + } + + if (delayDeliveryFor is not null) + { + array[index++] = new KeyValuePair(DelayDeliveryWithKeyName, delayDeliveryFor); + } + + if (timeToBeReceived is not null) + { + array[index++] = new KeyValuePair(DiscardIfNotReceivedBeforeKeyName, timeToBeReceived); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + array[index++] = new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + return; + } + + foreach (var kvp in stash) + { + array[index++] = kvp; + } + } + + bool ICollection>.Remove(KeyValuePair item) => ((ICollection>)this).Contains(item) && Remove(item.Key); + + [DoesNotReturn] + static void ThrowKeyNotFoundException(string key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); + + SlotArray slots; + int count; + Dictionary? stash; + + // Only needed for custom properties added by transports; the three well-known + // properties are stored as dedicated fields to avoid string-key lookups. + const int InlineArrayLength = 2; + + struct Slot + { + public string Key; + public string Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Transports/ReceiveProperties.cs b/src/NServiceBus.Core/Transports/ReceiveProperties.cs index f4eecbab4c0..4b7466e7ac9 100644 --- a/src/NServiceBus.Core/Transports/ReceiveProperties.cs +++ b/src/NServiceBus.Core/Transports/ReceiveProperties.cs @@ -2,9 +2,11 @@ namespace NServiceBus.Transport; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; /// /// Properties received from the transport that can be propagated to outgoing dispatch operations. @@ -13,8 +15,6 @@ namespace NServiceBus.Transport; [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Name reflects domain semantics, not collection implementation.")] public sealed class ReceiveProperties : IReadOnlyDictionary { - readonly Dictionary properties; - /// /// An empty instance. /// @@ -23,34 +23,188 @@ public sealed class ReceiveProperties : IReadOnlyDictionary /// /// Creates an empty instance of . /// - public ReceiveProperties() => properties = []; + public ReceiveProperties() + { + } /// /// Creates a from the provided dictionary. - /// The dictionary is stored by reference — do not mutate it after passing to this constructor. /// - public ReceiveProperties(Dictionary dictionary) => properties = dictionary; + public ReceiveProperties(Dictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + foreach (var kvp in dictionary) + { + AddInternal(kvp.Key, kvp.Value); + } + } /// - public string this[string key] => properties[key]; + public string this[string key] + { + get + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return slot.Value; + } + } + + if (stash is not null && stash.TryGetValue(key, out var value)) + { + return value; + } + + ThrowKeyNotFoundException(key); + return null; + } + } /// - public IEnumerable Keys => properties.Keys; + public IEnumerable Keys + { + get + { + for (int i = 0; i < count; i++) + { + yield return slots[i].Key; + } + + if (stash is not null) + { + foreach (var key in stash.Keys) + { + yield return key; + } + } + } + } /// - public IEnumerable Values => properties.Values; + public IEnumerable Values + { + get + { + for (int i = 0; i < count; i++) + { + yield return slots[i].Value; + } + + if (stash is not null) + { + foreach (var value in stash.Values) + { + yield return value; + } + } + } + } /// - public int Count => properties.Count; + public int Count => count + (stash?.Count ?? 0); /// - public bool ContainsKey(string key) => properties.ContainsKey(key); + public bool ContainsKey(string key) + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return true; + } + } + + return stash is not null && stash.ContainsKey(key); + } /// - public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) => properties.TryGetValue(key, out value); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + value = slot.Value; + return true; + } + + if (stash is not null && stash.TryGetValue(key, out var stashedValue)) + { + value = stashedValue; + return true; + } + + value = null; + return false; + } /// - public IEnumerator> GetEnumerator() => properties.GetEnumerator(); + public IEnumerator> GetEnumerator() + { + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + yield return new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + yield break; + } + + foreach (var kvp in stash) + { + yield return kvp; + } + } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void AddInternal(string key, string value) + { + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + + [DoesNotReturn] + static void ThrowKeyNotFoundException(string key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); + + SlotArray slots; + int count; + Dictionary? stash; + + const int InlineArrayLength = 4; + + struct Slot + { + public string Key; + public string Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; + } } \ No newline at end of file