From c0956dde6bcc98e765376fc8b621123e5365a314 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 10 Sep 2026 20:46:49 +1000 Subject: [PATCH] Add net10 ImmutableDictionary.CreateRangeWithOverwrite Two overloads, with and without a key comparer. Scanning the whole System.Collections.Immutable namespace confirmed these are the only WithOverwrite members, so this is the complete addition rather than one of a family. Scoped to netcoreapp2.1 through net9.0. ImmutableDictionary is in the shared framework from netcoreapp2.1 and is a separate package below that, so extending it on net461, net47x, net48 or netstandard would mean taking a dependency Polyfill does not have and adding a feature constant for one member. Noted on both. Implemented over a builder with indexer assignment, which was verified against net11 to be indistinguishable: later entries overwrite earlier ones, an empty range returns the shared Empty instance, a supplied comparer is honoured and preserved by reference, and a null comparer means the default. The one difference was the paramName for a null key, where the builder indexer reports "key" and the BCL reports the KeyValuePair member as "Key", so the key is checked before the assignment to match. API count 1170 -> 1172. --- apiCount.include.md | 20 +-- api_list.include.md | 8 ++ assemblySize.include.md | 36 +++--- readme.md | 68 +++++----- src/Consume/Consume.cs | 12 ++ src/Polyfill/ImmutableDictionaryPolyfill.cs | 56 +++++++++ .../net5.0/ImmutableDictionaryPolyfill.cs | 39 ++++++ .../net6.0/ImmutableDictionaryPolyfill.cs | 39 ++++++ .../net7.0/ImmutableDictionaryPolyfill.cs | 39 ++++++ .../net8.0/ImmutableDictionaryPolyfill.cs | 39 ++++++ .../net9.0/ImmutableDictionaryPolyfill.cs | 39 ++++++ .../ImmutableDictionaryPolyfill.cs | 39 ++++++ .../ImmutableDictionaryPolyfill.cs | 39 ++++++ .../ImmutableDictionaryPolyfill.cs | 39 ++++++ .../ImmutableDictionaryPolyfill.cs | 39 ++++++ src/Tests/ImmutableDictionaryPolyfillTests.cs | 116 ++++++++++++++++++ 16 files changed, 609 insertions(+), 58 deletions(-) create mode 100644 src/Polyfill/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/net5.0/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/net6.0/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/net7.0/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/net8.0/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/net9.0/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/netcoreapp2.1/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/netcoreapp2.2/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/netcoreapp3.0/ImmutableDictionaryPolyfill.cs create mode 100644 src/Split/netcoreapp3.1/ImmutableDictionaryPolyfill.cs create mode 100644 src/Tests/ImmutableDictionaryPolyfillTests.cs diff --git a/apiCount.include.md b/apiCount.include.md index 42499ca8..b91c49b8 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,4 +1,4 @@ -**API count: 1170** +**API count: 1172** ### Per Target Framework @@ -14,15 +14,15 @@ | `netstandard2.0` | 1074 | | `netstandard2.1` | 907 | | `netcoreapp2.0` | 997 | -| `netcoreapp2.1` | 918 | -| `netcoreapp2.2` | 918 | -| `netcoreapp3.0` | 879 | -| `netcoreapp3.1` | 878 | -| `net5.0` | 756 | -| `net6.0` | 652 | -| `net7.0` | 511 | -| `net8.0` | 390 | -| `net9.0` | 243 | +| `netcoreapp2.1` | 920 | +| `netcoreapp2.2` | 920 | +| `netcoreapp3.0` | 881 | +| `netcoreapp3.1` | 880 | +| `net5.0` | 758 | +| `net6.0` | 654 | +| `net7.0` | 513 | +| `net8.0` | 392 | +| `net9.0` | 245 | | `net10.0` | 182 | | `net11.0` | 58 | | `uap10.0` | 1064 | diff --git a/api_list.include.md b/api_list.include.md index 049a4633..30421d64 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -641,6 +641,14 @@ * `ReadOnlyCollection AsReadOnly()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.asreadonly?view=net-11.0#system-collections-generic-collectionextensions-asreadonly-1(system-collections-generic-ilist((-0)))) +#### ImmutableDictionary + + * `ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer?, ReadOnlySpan>) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary.createrangewithoverwrite?view=net-11.0#system-collections-immutable-immutabledictionary-createrangewithoverwrite-2(system-collections-generic-iequalitycomparer((-0))-system-readonlyspan((system-collections-generic-keyvaluepair((-0-1)))))) + * Note: Only available on netcoreapp2.1 and later, since ImmutableDictionary is a separate package below that. + * `ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan>) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary.createrangewithoverwrite?view=net-11.0#system-collections-immutable-immutabledictionary-createrangewithoverwrite-2(system-readonlyspan((system-collections-generic-keyvaluepair((-0-1)))))) + * Note: Only available on netcoreapp2.1 and later, since ImmutableDictionary is a separate package below that. + + #### Int128 * `Int128 BigMul(Int128, Int128, Int128)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int128.bigmul?view=net-11.0) diff --git a/assemblySize.include.md b/assemblySize.include.md index c1d5524a..e84fea85 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -12,15 +12,15 @@ | net48 | 8.5KB | 387.0KB | +378.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | | net481 | 8.5KB | 387.0KB | +378.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | | netcoreapp2.0 | 9.0KB | 365.0KB | +356.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 346.0KB | +337.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.2 | 9.0KB | 346.0KB | +337.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.0 | 9.5KB | 344.0KB | +334.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| netcoreapp3.1 | 9.5KB | 342.5KB | +333.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net5.0 | 9.5KB | 308.0KB | +298.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net6.0 | 10.0KB | 249.0KB | +239.0KB | +9.5KB | +7.0KB | +512bytes | +3.5KB | -| net7.0 | 10.0KB | 217.5KB | +207.5KB | +9.5KB | +6.0KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 188.0KB | +178.5KB | +8.5KB | +512bytes | +512bytes | +3.5KB | -| net9.0 | 9.5KB | 117.0KB | +107.5KB | +8.0KB | | +512bytes | +3.0KB | +| netcoreapp2.1 | 9.0KB | 347.0KB | +338.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp2.2 | 9.0KB | 347.0KB | +338.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp3.0 | 9.5KB | 345.5KB | +336.0KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | +| netcoreapp3.1 | 9.5KB | 343.5KB | +334.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net5.0 | 9.5KB | 309.0KB | +299.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net6.0 | 10.0KB | 250.0KB | +240.0KB | +10.0KB | +7.0KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 218.5KB | +208.5KB | +9.5KB | +6.0KB | +1.0KB | +3.5KB | +| net8.0 | 9.5KB | 189.5KB | +180.0KB | +8.0KB | | +512bytes | +3.0KB | +| net9.0 | 9.5KB | 118.0KB | +108.5KB | +8.5KB | | +512bytes | +3.5KB | | net10.0 | 10.0KB | 93.5KB | +83.5KB | +8.5KB | | +512bytes | +3.0KB | | net11.0 | 10.0KB | 21.0KB | +11.0KB | +9.0KB | | +512bytes | +3.5KB | @@ -39,14 +39,14 @@ | net48 | 8.5KB | 562.6KB | +554.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | net481 | 8.5KB | 562.6KB | +554.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | netcoreapp2.0 | 9.0KB | 530.7KB | +521.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 499.0KB | +490.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.2 | 9.0KB | 499.0KB | +490.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.0 | 9.5KB | 490.3KB | +480.8KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| netcoreapp3.1 | 9.5KB | 488.8KB | +479.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net5.0 | 9.5KB | 436.9KB | +427.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net6.0 | 10.0KB | 357.7KB | +347.7KB | +17.2KB | +8.7KB | +1.1KB | +4.2KB | -| net7.0 | 10.0KB | 309.3KB | +299.3KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 266.1KB | +256.6KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | -| net9.0 | 9.5KB | 169.1KB | +159.6KB | +15.5KB | | +1.1KB | +3.7KB | +| netcoreapp2.1 | 9.0KB | 500.5KB | +491.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp2.2 | 9.0KB | 500.5KB | +491.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp3.0 | 9.5KB | 492.3KB | +482.8KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 490.3KB | +480.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net5.0 | 9.5KB | 438.4KB | +428.9KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net6.0 | 10.0KB | 359.2KB | +349.2KB | +17.7KB | +8.7KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 310.9KB | +300.9KB | +17.1KB | +7.4KB | +1.6KB | +4.2KB | +| net8.0 | 9.5KB | 268.1KB | +258.6KB | +15.5KB | +299bytes | +1.1KB | +3.7KB | +| net9.0 | 9.5KB | 170.6KB | +161.1KB | +16.0KB | | +1.1KB | +4.2KB | | net10.0 | 10.0KB | 136.1KB | +126.1KB | +16.0KB | | +1.1KB | +3.7KB | | net11.0 | 10.0KB | 30.9KB | +20.9KB | +16.5KB | | +1.1KB | +4.2KB | diff --git a/readme.md b/readme.md index 79730c84..5ed8bc4c 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ The package targets `netstandard2.0` and is designed to support the following ru * `uap10` -**API count: 1170** +**API count: 1172** ### Per Target Framework @@ -29,15 +29,15 @@ The package targets `netstandard2.0` and is designed to support the following ru | `netstandard2.0` | 1074 | | `netstandard2.1` | 907 | | `netcoreapp2.0` | 997 | -| `netcoreapp2.1` | 918 | -| `netcoreapp2.2` | 918 | -| `netcoreapp3.0` | 879 | -| `netcoreapp3.1` | 878 | -| `net5.0` | 756 | -| `net6.0` | 652 | -| `net7.0` | 511 | -| `net8.0` | 390 | -| `net9.0` | 243 | +| `netcoreapp2.1` | 920 | +| `netcoreapp2.2` | 920 | +| `netcoreapp3.0` | 881 | +| `netcoreapp3.1` | 880 | +| `net5.0` | 758 | +| `net6.0` | 654 | +| `net7.0` | 513 | +| `net8.0` | 392 | +| `net9.0` | 245 | | `net10.0` | 182 | | `net11.0` | 58 | | `uap10.0` | 1064 | @@ -106,15 +106,15 @@ This project uses features from the newest stable SDK and C# language. As such c | net48 | 8.5KB | 387.0KB | +378.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | | net481 | 8.5KB | 387.0KB | +378.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | | netcoreapp2.0 | 9.0KB | 365.0KB | +356.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 346.0KB | +337.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.2 | 9.0KB | 346.0KB | +337.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.0 | 9.5KB | 344.0KB | +334.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| netcoreapp3.1 | 9.5KB | 342.5KB | +333.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net5.0 | 9.5KB | 308.0KB | +298.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net6.0 | 10.0KB | 249.0KB | +239.0KB | +9.5KB | +7.0KB | +512bytes | +3.5KB | -| net7.0 | 10.0KB | 217.5KB | +207.5KB | +9.5KB | +6.0KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 188.0KB | +178.5KB | +8.5KB | +512bytes | +512bytes | +3.5KB | -| net9.0 | 9.5KB | 117.0KB | +107.5KB | +8.0KB | | +512bytes | +3.0KB | +| netcoreapp2.1 | 9.0KB | 347.0KB | +338.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp2.2 | 9.0KB | 347.0KB | +338.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp3.0 | 9.5KB | 345.5KB | +336.0KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | +| netcoreapp3.1 | 9.5KB | 343.5KB | +334.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net5.0 | 9.5KB | 309.0KB | +299.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net6.0 | 10.0KB | 250.0KB | +240.0KB | +10.0KB | +7.0KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 218.5KB | +208.5KB | +9.5KB | +6.0KB | +1.0KB | +3.5KB | +| net8.0 | 9.5KB | 189.5KB | +180.0KB | +8.0KB | | +512bytes | +3.0KB | +| net9.0 | 9.5KB | 118.0KB | +108.5KB | +8.5KB | | +512bytes | +3.5KB | | net10.0 | 10.0KB | 93.5KB | +83.5KB | +8.5KB | | +512bytes | +3.0KB | | net11.0 | 10.0KB | 21.0KB | +11.0KB | +9.0KB | | +512bytes | +3.5KB | @@ -133,15 +133,15 @@ This project uses features from the newest stable SDK and C# language. As such c | net48 | 8.5KB | 562.6KB | +554.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | net481 | 8.5KB | 562.6KB | +554.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | netcoreapp2.0 | 9.0KB | 530.7KB | +521.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 499.0KB | +490.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.2 | 9.0KB | 499.0KB | +490.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.0 | 9.5KB | 490.3KB | +480.8KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| netcoreapp3.1 | 9.5KB | 488.8KB | +479.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net5.0 | 9.5KB | 436.9KB | +427.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net6.0 | 10.0KB | 357.7KB | +347.7KB | +17.2KB | +8.7KB | +1.1KB | +4.2KB | -| net7.0 | 10.0KB | 309.3KB | +299.3KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 266.1KB | +256.6KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | -| net9.0 | 9.5KB | 169.1KB | +159.6KB | +15.5KB | | +1.1KB | +3.7KB | +| netcoreapp2.1 | 9.0KB | 500.5KB | +491.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp2.2 | 9.0KB | 500.5KB | +491.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp3.0 | 9.5KB | 492.3KB | +482.8KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 490.3KB | +480.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net5.0 | 9.5KB | 438.4KB | +428.9KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net6.0 | 10.0KB | 359.2KB | +349.2KB | +17.7KB | +8.7KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 310.9KB | +300.9KB | +17.1KB | +7.4KB | +1.6KB | +4.2KB | +| net8.0 | 9.5KB | 268.1KB | +258.6KB | +15.5KB | +299bytes | +1.1KB | +3.7KB | +| net9.0 | 9.5KB | 170.6KB | +161.1KB | +16.0KB | | +1.1KB | +4.2KB | | net10.0 | 10.0KB | 136.1KB | +126.1KB | +16.0KB | | +1.1KB | +3.7KB | | net11.0 | 10.0KB | 30.9KB | +20.9KB | +16.5KB | | +1.1KB | +4.2KB | @@ -1256,6 +1256,14 @@ The class `Polyfill` includes the following extension methods: * `ReadOnlyCollection AsReadOnly()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.asreadonly?view=net-11.0#system-collections-generic-collectionextensions-asreadonly-1(system-collections-generic-ilist((-0)))) +#### ImmutableDictionary + + * `ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer?, ReadOnlySpan>) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary.createrangewithoverwrite?view=net-11.0#system-collections-immutable-immutabledictionary-createrangewithoverwrite-2(system-collections-generic-iequalitycomparer((-0))-system-readonlyspan((system-collections-generic-keyvaluepair((-0-1)))))) + * Note: Only available on netcoreapp2.1 and later, since ImmutableDictionary is a separate package below that. + * `ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan>) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary.createrangewithoverwrite?view=net-11.0#system-collections-immutable-immutabledictionary-createrangewithoverwrite-2(system-readonlyspan((system-collections-generic-keyvaluepair((-0-1)))))) + * Note: Only available on netcoreapp2.1 and later, since ImmutableDictionary is a separate package below that. + + #### Int128 * `Int128 BigMul(Int128, Int128, Int128)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.int128.bigmul?view=net-11.0) @@ -2607,7 +2615,7 @@ void ObjectDisposedExceptionExample(bool isDisposed) ObjectDisposedException.ThrowIf(isDisposed, typeof(Consume)); } ``` -snippet source | anchor +snippet source | anchor @@ -2626,7 +2634,7 @@ void EnsureExample(Order order, Customer customer, string customerId, string ema this.quantity = Ensure.NotNegativeOrZero(quantity); } ``` -snippet source | anchor +snippet source | anchor diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index 364aab2d..af3a0759 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -18,6 +18,9 @@ namespace Consumes; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +#if NETCOREAPP2_1_OR_GREATER +using System.Collections.Immutable; +#endif using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; @@ -216,6 +219,15 @@ void StringBuilder_ParamsSpan_Methods() } #endif +#if NETCOREAPP2_1_OR_GREATER + void ImmutableDictionary_Methods() + { + ReadOnlySpan> items = new KeyValuePair[] { new("a", 1) }; + var withOverwrite = ImmutableDictionary.CreateRangeWithOverwrite(items); + withOverwrite = ImmutableDictionary.CreateRangeWithOverwrite(StringComparer.Ordinal, items); + } +#endif + void Interlocked_Narrow_Methods() { byte unsignedByte = 0; diff --git a/src/Polyfill/ImmutableDictionaryPolyfill.cs b/src/Polyfill/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..be5e91c0 --- /dev/null +++ b/src/Polyfill/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,56 @@ +#pragma warning disable + +// ImmutableDictionary is in the shared framework from netcoreapp2.1. Below that it is a separate +// package, so there is nothing to extend without taking a dependency Polyfill does not have. +#if NETCOREAPP2_1_OR_GREATER && !NET10_0_OR_GREATER + +namespace Polyfills; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary.createrangewithoverwrite?view=net-11.0#system-collections-immutable-immutabledictionary-createrangewithoverwrite-2(system-readonlyspan((system-collections-generic-keyvaluepair((-0-1))))) + //Note: Only available on netcoreapp2.1 and later, since ImmutableDictionary is a separate package below that. + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary.createrangewithoverwrite?view=net-11.0#system-collections-immutable-immutabledictionary-createrangewithoverwrite-2(system-collections-generic-iequalitycomparer((-0))-system-readonlyspan((system-collections-generic-keyvaluepair((-0-1))))) + //Note: Only available on netcoreapp2.1 and later, since ImmutableDictionary is a separate package below that. + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + // the builder indexer names this parameter "key", where the BCL overload reports the + // KeyValuePair member as "Key" + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + + builder[item.Key] = item.Value; + } + + return builder.ToImmutable(); + } +} + +#endif diff --git a/src/Split/net5.0/ImmutableDictionaryPolyfill.cs b/src/Split/net5.0/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/net5.0/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/net6.0/ImmutableDictionaryPolyfill.cs b/src/Split/net6.0/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/net6.0/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/net7.0/ImmutableDictionaryPolyfill.cs b/src/Split/net7.0/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/net7.0/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/net8.0/ImmutableDictionaryPolyfill.cs b/src/Split/net8.0/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/net8.0/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/net9.0/ImmutableDictionaryPolyfill.cs b/src/Split/net9.0/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/net9.0/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/netcoreapp2.1/ImmutableDictionaryPolyfill.cs b/src/Split/netcoreapp2.1/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/netcoreapp2.1/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/netcoreapp2.2/ImmutableDictionaryPolyfill.cs b/src/Split/netcoreapp2.2/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/netcoreapp2.2/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/netcoreapp3.0/ImmutableDictionaryPolyfill.cs b/src/Split/netcoreapp3.0/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/netcoreapp3.0/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Split/netcoreapp3.1/ImmutableDictionaryPolyfill.cs b/src/Split/netcoreapp3.1/ImmutableDictionaryPolyfill.cs new file mode 100644 index 00000000..ae3f5fc1 --- /dev/null +++ b/src/Split/netcoreapp3.1/ImmutableDictionaryPolyfill.cs @@ -0,0 +1,39 @@ +// +#pragma warning disable +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +static partial class Polyfill +{ + extension(ImmutableDictionary) + { + /// + /// Creates a new from the specified items, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(null, items); + /// + /// Creates a new from the specified items using the specified key comparer, where later entries overwrite earlier ones with the same key. + /// + public static ImmutableDictionary CreateRangeWithOverwrite(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull => + CreateRangeWithOverwriteCore(keyComparer, items); + } + static ImmutableDictionary CreateRangeWithOverwriteCore(IEqualityComparer? keyComparer, ReadOnlySpan> items) + where TKey : notnull + { + var builder = ImmutableDictionary.CreateBuilder(keyComparer); + foreach (var item in items) + { + if (item.Key is null) + { + throw new ArgumentNullException(nameof(item.Key)); + } + builder[item.Key] = item.Value; + } + return builder.ToImmutable(); + } +} diff --git a/src/Tests/ImmutableDictionaryPolyfillTests.cs b/src/Tests/ImmutableDictionaryPolyfillTests.cs new file mode 100644 index 00000000..2a18bc7e --- /dev/null +++ b/src/Tests/ImmutableDictionaryPolyfillTests.cs @@ -0,0 +1,116 @@ +#if NETCOREAPP2_1_OR_GREATER + +using System.Collections.Immutable; +using System.Linq; + +// Runs from netcoreapp2.1, which is where ImmutableDictionary is in the shared framework, so +// net10.0 and later validate the BCL and everything below the polyfill. +public class ImmutableDictionaryPolyfillTests +{ + static KeyValuePair[] withDuplicates = + [ + new("a", 1), + new("b", 2), + new("a", 3), + new("c", 4), + new("b", 5) + ]; + + [Test] + public async Task LaterEntriesOverwriteEarlierOnes() + { + var result = Create(withDuplicates); + + await Assert.That(result.Count).IsEqualTo(3); + await Assert.That(result["a"]).IsEqualTo(3); + await Assert.That(result["b"]).IsEqualTo(5); + await Assert.That(result["c"]).IsEqualTo(4); + } + + // the point of the API: CreateRange rejects the same input + [Test] + public async Task CreateRangeRejectsWhatThisAccepts() + { + await Assert.That(() => ImmutableDictionary.CreateRange(withDuplicates)).Throws(); + + var result = Create(withDuplicates); + await Assert.That(result.Count).IsEqualTo(3); + } + + [Test] + public async Task EmptyReturnsTheSharedEmptyInstance() + { + var result = Create([]); + + await Assert.That(result.Count).IsEqualTo(0); + await Assert.That(result).IsSameReferenceAs(ImmutableDictionary.Empty); + } + + [Test] + public async Task ComparerIsHonouredAndPreserved() + { + KeyValuePair[] items = [new("A", 1), new("a", 2)]; + + var ignoringCase = Create(StringComparer.OrdinalIgnoreCase, items); + await Assert.That(ignoringCase.Count).IsEqualTo(1); + await Assert.That(ignoringCase["A"]).IsEqualTo(2); + await Assert.That(ignoringCase.KeyComparer).IsSameReferenceAs(StringComparer.OrdinalIgnoreCase); + + var caseSensitive = Create(StringComparer.Ordinal, items); + await Assert.That(caseSensitive.Count).IsEqualTo(2); + + // a null comparer means the default, and an empty range still keeps a supplied one + await Assert.That(Create(null, withDuplicates).KeyComparer).IsSameReferenceAs(EqualityComparer.Default); + await Assert.That(Create(StringComparer.Ordinal, []).KeyComparer).IsSameReferenceAs(StringComparer.Ordinal); + } + + [Test] + public async Task NullKeyIsRejected() + { + KeyValuePair[] items = [new("a", 1), new(null!, 2)]; + + var captured = Capture(() => Create(items)); + + // the BCL names the KeyValuePair member rather than the builder's parameter + await Assert.That(captured).IsEqualTo("ArgumentNullException:Key"); + } + + [Test] + public async Task MatchesABuilderWithTheSamePolicy() + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var pair in withDuplicates) + { + builder[pair.Key] = pair.Value; + } + + var viaBuilder = builder.ToImmutable(); + var viaCreate = Create(withDuplicates); + + await Assert.That(viaCreate.Count).IsEqualTo(viaBuilder.Count); + await Assert.That(viaCreate.OrderBy(_ => _.Key).Select(_ => $"{_.Key}={_.Value}").ToList()) + .IsEquivalentTo(viaBuilder.OrderBy(_ => _.Key).Select(_ => $"{_.Key}={_.Value}").ToList()); + } + + // the spans stay inside these helpers, so nothing ref struct shaped lives across an await + static ImmutableDictionary Create(KeyValuePair[] items) => + ImmutableDictionary.CreateRangeWithOverwrite(items); + + static ImmutableDictionary Create(IEqualityComparer? comparer, KeyValuePair[] items) => + ImmutableDictionary.CreateRangeWithOverwrite(comparer, items); + + static string Capture(Func func) + { + try + { + func(); + return "none"; + } + catch (ArgumentException exception) + { + return $"{exception.GetType().Name}:{exception.ParamName}"; + } + } +} + +#endif