diff --git a/apiCount.include.md b/apiCount.include.md
index 99bea8de..60540fc5 100644
--- a/apiCount.include.md
+++ b/apiCount.include.md
@@ -1,28 +1,28 @@
-**API count: 937**
+**API count: 941**
### Per Target Framework
| Target | APIs |
| -- | -- |
-| `net461` | 907 |
-| `net462` | 907 |
-| `net47` | 906 |
-| `net471` | 905 |
-| `net472` | 901 |
-| `net48` | 901 |
-| `net481` | 901 |
-| `netstandard2.0` | 903 |
-| `netstandard2.1` | 734 |
-| `netcoreapp2.0` | 827 |
-| `netcoreapp2.1` | 746 |
-| `netcoreapp2.2` | 746 |
-| `netcoreapp3.0` | 692 |
-| `netcoreapp3.1` | 691 |
-| `net5.0` | 563 |
-| `net6.0` | 468 |
-| `net7.0` | 315 |
-| `net8.0` | 197 |
+| `net461` | 914 |
+| `net462` | 914 |
+| `net47` | 913 |
+| `net471` | 912 |
+| `net472` | 908 |
+| `net48` | 908 |
+| `net481` | 908 |
+| `netstandard2.0` | 910 |
+| `netstandard2.1` | 741 |
+| `netcoreapp2.0` | 834 |
+| `netcoreapp2.1` | 753 |
+| `netcoreapp2.2` | 753 |
+| `netcoreapp3.0` | 699 |
+| `netcoreapp3.1` | 698 |
+| `net5.0` | 570 |
+| `net6.0` | 475 |
+| `net7.0` | 322 |
+| `net8.0` | 204 |
| `net9.0` | 128 |
| `net10.0` | 76 |
| `net11.0` | 57 |
-| `uap10.0` | 893 |
+| `uap10.0` | 900 |
diff --git a/api_list.include.md b/api_list.include.md
index cac42e88..9aa72d2e 100644
--- a/api_list.include.md
+++ b/api_list.include.md
@@ -2,18 +2,24 @@
#### ArgumentException
+> Requires [`true`](#argumentexception-1) in the consuming project.
+
* `void ThrowIfNullOrEmpty(string?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorempty?view=net-11.0#system-argumentexception-throwifnullorempty(system-string-system-string))
* `void ThrowIfNullOrWhiteSpace(string?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorwhitespace?view=net-11.0#system-argumentexception-throwifnullorwhitespace(system-string-system-string))
#### ArgumentNullException
+> Requires [`true`](#argumentexception-1) in the consuming project.
+
* `void ThrowIfNull(object?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-11.0#system-argumentnullexception-throwifnull(system-object-system-string))
* `void ThrowIfNull(void*)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-11.0#system-argumentnullexception-throwifnull(system-void*-system-string))
#### ArgumentOutOfRangeException
+> Requires [`true`](#argumentexception-1) in the consuming project.
+
* `void ThrowIfEqual(T, T) where T : IEquatable?` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifequal?view=net-11.0#system-argumentoutofrangeexception-throwifequal-1(-0-0-system-string))
* `void ThrowIfGreaterThan(nint, nint)`
* `void ThrowIfGreaterThan(T, T) where T : IComparable` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthan?view=net-11.0#system-argumentoutofrangeexception-throwifgreaterthan-1(-0-0-system-string))
@@ -222,8 +228,16 @@
#### Dictionary
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0)
+ * Note: No-op on older targets; the BCL grows the backing storage.
+ * `DictionaryAlternateLookup GetAlternateLookup() where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
+ * Note: Returns the free-standing `DictionaryAlternateLookup` rather than the BCL's nested `Dictionary.AlternateLookup`. Use `var` for cross-target code.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
* `void TrimExcess()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess)
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
+ * `bool TryGetAlternateLookup(DictionaryAlternateLookup) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
#### DictionaryEntry
@@ -398,7 +412,14 @@
#### HashSet
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ensurecapacity?view=net-11.0#system-collections-generic-hashset-1-ensurecapacity(system-int32))
+ * Note: No-op on older targets; the BCL grows the backing storage.
+ * `HashSetAlternateLookup GetAlternateLookup()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
+ * Note: Returns the free-standing `HashSetAlternateLookup` rather than the BCL's nested `HashSet.AlternateLookup`. Use `var` for cross-target code.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trimexcess?view=net-11.0#system-collections-generic-hashset-1-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
+ * `bool TryGetAlternateLookup(HashSetAlternateLookup)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
* `bool TryGetValue(T, T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetvalue?view=net-11.0)
@@ -598,8 +619,11 @@
* `void AddRange(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.addrange?view=net-11.0)
* `void CopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.copyto?view=net-11.0)
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.ensurecapacity?view=net-11.0#system-collections-generic-list-1-ensurecapacity(system-int32))
+ * Note: No-op on older targets; the BCL grows the backing storage.
+ * Note: Returns void on older targets; the BCL returns int (the new capacity).
* `void InsertRange(int, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.insertrange?view=net-11.0)
* `void TrimExcess()`
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
#### Math
@@ -647,6 +671,8 @@
#### ObjectDisposedException
+> Requires [`true`](#argumentexception-1) in the consuming project.
+
* `void ThrowIf(bool, object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.objectdisposedexception.throwif?view=net-11.0##system-objectdisposedexception-throwif(system-boolean-system-object))
* `void ThrowIf(bool, Type)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.objectdisposedexception.throwif?view=net-11.0##system-objectdisposedexception-throwif(system-boolean-system-type))
@@ -738,7 +764,9 @@
#### Queue
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.ensurecapacity?view=net-11.0#system-collections-generic-queue-1-ensurecapacity(system-int32))
+ * Note: No-op on older targets; the BCL grows the backing storage.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.trimexcess?view=net-11.0#system-collections-generic-queue-1-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
#### Random
@@ -988,7 +1016,9 @@
#### Stack
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.ensurecapacity?view=net-11.0)
+ * Note: No-op on older targets; the BCL grows the backing storage.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trimexcess?view=net-11.0#system-collections-generic-stack-1-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
* `bool TryPeek(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypeek?view=net-11.0)
* `bool TryPop(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypop?view=net-11.0)
@@ -1308,6 +1338,8 @@
#### Ensure
+> Requires [`true`](#ensure-1) in the consuming project.
+
* `void DirectoryExists(string)`
* `T Equal(T, T)`
* `void FileExists(string)`
diff --git a/assemblySize.include.md b/assemblySize.include.md
index 46c2f81e..186d7549 100644
--- a/assemblySize.include.md
+++ b/assemblySize.include.md
@@ -2,24 +2,24 @@
| | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability |
|----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------|
-| netstandard2.0 | 8.0KB | 306.5KB | +298.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| netstandard2.1 | 8.5KB | 259.5KB | +251.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net461 | 8.5KB | 305.0KB | +296.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net462 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net47 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net471 | 8.5KB | 307.5KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net472 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB |
-| net48 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB |
-| net481 | 8.5KB | 306.5KB | +298.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB |
-| netcoreapp2.0 | 9.0KB | 282.5KB | +273.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| netcoreapp2.1 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
-| netcoreapp2.2 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +7.0KB | +9.0KB | +13.5KB |
-| netcoreapp3.0 | 9.5KB | 253.5KB | +244.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
-| netcoreapp3.1 | 9.5KB | 251.5KB | +242.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net5.0 | 9.5KB | 214.5KB | +205.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB |
-| net6.0 | 10.0KB | 153.0KB | +143.0KB | +10.0KB | +7.0KB | +512bytes | +4.0KB |
-| net7.0 | 10.0KB | 118.0KB | +108.0KB | +9.0KB | +5.5KB | +512bytes | +3.5KB |
-| net8.0 | 9.5KB | 89.5KB | +80.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB |
+| netstandard2.0 | 8.0KB | 310.5KB | +302.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| netstandard2.1 | 8.5KB | 263.5KB | +255.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net461 | 8.5KB | 309.0KB | +300.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net462 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net47 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net471 | 8.5KB | 311.5KB | +303.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net472 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB |
+| net48 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB |
+| net481 | 8.5KB | 310.5KB | +302.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB |
+| netcoreapp2.0 | 9.0KB | 286.5KB | +277.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| netcoreapp2.1 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB |
+| netcoreapp2.2 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB |
+| netcoreapp3.0 | 9.5KB | 257.5KB | +248.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
+| netcoreapp3.1 | 9.5KB | 255.5KB | +246.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB |
+| net5.0 | 9.5KB | 218.5KB | +209.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB |
+| net6.0 | 10.0KB | 157.0KB | +147.0KB | +12.5KB | +10.0KB | +1.0KB | +4.0KB |
+| net7.0 | 10.0KB | 122.0KB | +112.0KB | +9.0KB | +5.5KB | +512bytes | +4.0KB |
+| net8.0 | 9.5KB | 94.0KB | +84.5KB | +8.5KB | | +512bytes | +3.5KB |
| net9.0 | 9.5KB | 47.5KB | +38.0KB | +9.0KB | | +512bytes | +3.5KB |
| net10.0 | 10.0KB | 24.0KB | +14.0KB | +9.0KB | | +512bytes | +3.5KB |
| net11.0 | 10.0KB | 18.5KB | +8.5KB | +9.0KB | | +512bytes | +3.5KB |
@@ -29,24 +29,24 @@
| | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability |
|----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------|
-| netstandard2.0 | 8.0KB | 446.3KB | +438.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| netstandard2.1 | 8.5KB | 374.1KB | +365.6KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net461 | 8.5KB | 445.8KB | +437.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net462 | 7.0KB | 449.3KB | +442.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net47 | 7.0KB | 449.1KB | +442.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net471 | 8.5KB | 447.7KB | +439.2KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net472 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB |
-| net48 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB |
-| net481 | 8.5KB | 445.6KB | +437.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB |
-| netcoreapp2.0 | 9.0KB | 412.4KB | +403.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| netcoreapp2.1 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
-| netcoreapp2.2 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.7KB | +13.9KB | +18.9KB |
-| netcoreapp3.0 | 9.5KB | 363.0KB | +353.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
-| netcoreapp3.1 | 9.5KB | 360.9KB | +351.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net5.0 | 9.5KB | 305.8KB | +296.3KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB |
-| net6.0 | 10.0KB | 224.7KB | +214.7KB | +17.7KB | +8.7KB | +1.1KB | +4.7KB |
-| net7.0 | 10.0KB | 170.9KB | +160.9KB | +16.6KB | +6.9KB | +1.1KB | +4.2KB |
-| net8.0 | 9.5KB | 127.6KB | +118.1KB | +16.0KB | +811bytes | +1.1KB | +4.2KB |
+| netstandard2.0 | 8.0KB | 453.3KB | +445.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| netstandard2.1 | 8.5KB | 381.2KB | +372.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net461 | 8.5KB | 452.9KB | +444.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net462 | 7.0KB | 456.4KB | +449.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net47 | 7.0KB | 456.1KB | +449.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net471 | 8.5KB | 454.8KB | +446.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net472 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB |
+| net48 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB |
+| net481 | 8.5KB | 452.7KB | +444.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB |
+| netcoreapp2.0 | 9.0KB | 419.5KB | +410.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| netcoreapp2.1 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB |
+| netcoreapp2.2 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB |
+| netcoreapp3.0 | 9.5KB | 370.1KB | +360.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
+| netcoreapp3.1 | 9.5KB | 368.1KB | +358.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB |
+| net5.0 | 9.5KB | 313.0KB | +303.5KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB |
+| net6.0 | 10.0KB | 231.9KB | +221.9KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB |
+| net7.0 | 10.0KB | 178.3KB | +168.3KB | +16.6KB | +6.9KB | +1.1KB | +4.7KB |
+| net8.0 | 9.5KB | 135.4KB | +125.9KB | +16.0KB | +299bytes | +1.1KB | +4.2KB |
| net9.0 | 9.5KB | 68.6KB | +59.1KB | +16.5KB | | +1.1KB | +4.2KB |
| net10.0 | 10.0KB | 36.5KB | +26.5KB | +16.5KB | | +1.1KB | +4.2KB |
| net11.0 | 10.0KB | 27.4KB | +17.4KB | +16.5KB | | +1.1KB | +4.2KB |
diff --git a/claude.md b/claude.md
index aede5701..8831aacc 100644
--- a/claude.md
+++ b/claude.md
@@ -69,6 +69,8 @@ Polyfill uses extensive `#if` directives. Key constants:
- Each `*Polyfill.cs` file must contain exactly **one top-level type** (the `Polyfill` partial class). Helper classes must be **nested** inside `Polyfill`, otherwise `ReadMethodsForFiles` in `BuildApiTest` will throw.
- The filename of static polyfill files directly determines the `api_list.include.md` section header: `{TypeName}Polyfill.cs` → `#### {TypeName}`. For example, `FilePolyfill.cs` → `#### File`. Choose filenames to match the type being extended.
- `//Link:` comments on public methods must use `?view=net-11.0` for learn.microsoft.com URLs (enforced by `LinkReader`). For overloaded methods, include the `#fragment` anchor pointing to the specific overload (e.g., `#system-type-method(system-string-system-int32)`).
+- `//Note: ` comments (also leading trivia, parsed by `LinkReader.GetNotes`) attach a caveat to a polyfilled API. Each line emits a sub-bullet `* Note: ` under the signature in `api_list.include.md` (rendered by `BuildApiTest.WriteNotes`). Use sparingly — only when behavior diverges from the BCL in a way callers must know about (e.g., O(n) vs O(1), free-standing struct vs nested type, different exception type). Don't restate the doc summary.
+- Section-level opt-in gates (e.g., `Ensure`, `ArgumentNullException`) are emitted as a `> Requires true` blockquote under the section header. The mapping lives in `BuildApiTest.sectionGates` — add an entry there when you introduce a new section whose members all require an MSBuild flag.
### Test Projects
diff --git a/readme.md b/readme.md
index a107e5f6..2f901026 100644
--- a/readme.md
+++ b/readme.md
@@ -13,34 +13,34 @@ The package targets `netstandard2.0` and is designed to support the following ru
* `uap10`
-**API count: 937**
+**API count: 941**
### Per Target Framework
| Target | APIs |
| -- | -- |
-| `net461` | 907 |
-| `net462` | 907 |
-| `net47` | 906 |
-| `net471` | 905 |
-| `net472` | 901 |
-| `net48` | 901 |
-| `net481` | 901 |
-| `netstandard2.0` | 903 |
-| `netstandard2.1` | 734 |
-| `netcoreapp2.0` | 827 |
-| `netcoreapp2.1` | 746 |
-| `netcoreapp2.2` | 746 |
-| `netcoreapp3.0` | 692 |
-| `netcoreapp3.1` | 691 |
-| `net5.0` | 563 |
-| `net6.0` | 468 |
-| `net7.0` | 315 |
-| `net8.0` | 197 |
+| `net461` | 914 |
+| `net462` | 914 |
+| `net47` | 913 |
+| `net471` | 912 |
+| `net472` | 908 |
+| `net48` | 908 |
+| `net481` | 908 |
+| `netstandard2.0` | 910 |
+| `netstandard2.1` | 741 |
+| `netcoreapp2.0` | 834 |
+| `netcoreapp2.1` | 753 |
+| `netcoreapp2.2` | 753 |
+| `netcoreapp3.0` | 699 |
+| `netcoreapp3.1` | 698 |
+| `net5.0` | 570 |
+| `net6.0` | 475 |
+| `net7.0` | 322 |
+| `net8.0` | 204 |
| `net9.0` | 128 |
| `net10.0` | 76 |
| `net11.0` | 57 |
-| `uap10.0` | 893 |
+| `uap10.0` | 900 |
@@ -96,24 +96,24 @@ This project uses features from the current stable SDK and C# language. As such
| | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability |
|----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------|
-| netstandard2.0 | 8.0KB | 306.5KB | +298.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| netstandard2.1 | 8.5KB | 259.5KB | +251.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net461 | 8.5KB | 305.0KB | +296.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net462 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net47 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net471 | 8.5KB | 307.5KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net472 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB |
-| net48 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB |
-| net481 | 8.5KB | 306.5KB | +298.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB |
-| netcoreapp2.0 | 9.0KB | 282.5KB | +273.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| netcoreapp2.1 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
-| netcoreapp2.2 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +7.0KB | +9.0KB | +13.5KB |
-| netcoreapp3.0 | 9.5KB | 253.5KB | +244.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
-| netcoreapp3.1 | 9.5KB | 251.5KB | +242.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
-| net5.0 | 9.5KB | 214.5KB | +205.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB |
-| net6.0 | 10.0KB | 153.0KB | +143.0KB | +10.0KB | +7.0KB | +512bytes | +4.0KB |
-| net7.0 | 10.0KB | 118.0KB | +108.0KB | +9.0KB | +5.5KB | +512bytes | +3.5KB |
-| net8.0 | 9.5KB | 89.5KB | +80.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB |
+| netstandard2.0 | 8.0KB | 310.5KB | +302.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| netstandard2.1 | 8.5KB | 263.5KB | +255.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net461 | 8.5KB | 309.0KB | +300.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net462 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net47 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net471 | 8.5KB | 311.5KB | +303.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| net472 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB |
+| net48 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB |
+| net481 | 8.5KB | 310.5KB | +302.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB |
+| netcoreapp2.0 | 9.0KB | 286.5KB | +277.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
+| netcoreapp2.1 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB |
+| netcoreapp2.2 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB |
+| netcoreapp3.0 | 9.5KB | 257.5KB | +248.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
+| netcoreapp3.1 | 9.5KB | 255.5KB | +246.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB |
+| net5.0 | 9.5KB | 218.5KB | +209.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB |
+| net6.0 | 10.0KB | 157.0KB | +147.0KB | +12.5KB | +10.0KB | +1.0KB | +4.0KB |
+| net7.0 | 10.0KB | 122.0KB | +112.0KB | +9.0KB | +5.5KB | +512bytes | +4.0KB |
+| net8.0 | 9.5KB | 94.0KB | +84.5KB | +8.5KB | | +512bytes | +3.5KB |
| net9.0 | 9.5KB | 47.5KB | +38.0KB | +9.0KB | | +512bytes | +3.5KB |
| net10.0 | 10.0KB | 24.0KB | +14.0KB | +9.0KB | | +512bytes | +3.5KB |
| net11.0 | 10.0KB | 18.5KB | +8.5KB | +9.0KB | | +512bytes | +3.5KB |
@@ -123,24 +123,24 @@ This project uses features from the current stable SDK and C# language. As such
| | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability |
|----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------|
-| netstandard2.0 | 8.0KB | 446.3KB | +438.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| netstandard2.1 | 8.5KB | 374.1KB | +365.6KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net461 | 8.5KB | 445.8KB | +437.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net462 | 7.0KB | 449.3KB | +442.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net47 | 7.0KB | 449.1KB | +442.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net471 | 8.5KB | 447.7KB | +439.2KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net472 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB |
-| net48 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB |
-| net481 | 8.5KB | 445.6KB | +437.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB |
-| netcoreapp2.0 | 9.0KB | 412.4KB | +403.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| netcoreapp2.1 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
-| netcoreapp2.2 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.7KB | +13.9KB | +18.9KB |
-| netcoreapp3.0 | 9.5KB | 363.0KB | +353.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
-| netcoreapp3.1 | 9.5KB | 360.9KB | +351.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
-| net5.0 | 9.5KB | 305.8KB | +296.3KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB |
-| net6.0 | 10.0KB | 224.7KB | +214.7KB | +17.7KB | +8.7KB | +1.1KB | +4.7KB |
-| net7.0 | 10.0KB | 170.9KB | +160.9KB | +16.6KB | +6.9KB | +1.1KB | +4.2KB |
-| net8.0 | 9.5KB | 127.6KB | +118.1KB | +16.0KB | +811bytes | +1.1KB | +4.2KB |
+| netstandard2.0 | 8.0KB | 453.3KB | +445.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| netstandard2.1 | 8.5KB | 381.2KB | +372.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net461 | 8.5KB | 452.9KB | +444.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net462 | 7.0KB | 456.4KB | +449.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net47 | 7.0KB | 456.1KB | +449.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net471 | 8.5KB | 454.8KB | +446.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| net472 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB |
+| net48 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB |
+| net481 | 8.5KB | 452.7KB | +444.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB |
+| netcoreapp2.0 | 9.0KB | 419.5KB | +410.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
+| netcoreapp2.1 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB |
+| netcoreapp2.2 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB |
+| netcoreapp3.0 | 9.5KB | 370.1KB | +360.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
+| netcoreapp3.1 | 9.5KB | 368.1KB | +358.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB |
+| net5.0 | 9.5KB | 313.0KB | +303.5KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB |
+| net6.0 | 10.0KB | 231.9KB | +221.9KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB |
+| net7.0 | 10.0KB | 178.3KB | +168.3KB | +16.6KB | +6.9KB | +1.1KB | +4.7KB |
+| net8.0 | 9.5KB | 135.4KB | +125.9KB | +16.0KB | +299bytes | +1.1KB | +4.2KB |
| net9.0 | 9.5KB | 68.6KB | +59.1KB | +16.5KB | | +1.1KB | +4.2KB |
| net10.0 | 10.0KB | 36.5KB | +26.5KB | +16.5KB | | +1.1KB | +4.2KB |
| net11.0 | 10.0KB | 27.4KB | +17.4KB | +16.5KB | | +1.1KB | +4.2KB |
@@ -753,8 +753,16 @@ The class `Polyfill` includes the following extension methods:
#### Dictionary
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0)
+ * Note: No-op on older targets; the BCL grows the backing storage.
+ * `DictionaryAlternateLookup GetAlternateLookup() where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
+ * Note: Returns the free-standing `DictionaryAlternateLookup` rather than the BCL's nested `Dictionary.AlternateLookup`. Use `var` for cross-target code.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
* `void TrimExcess()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess)
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
+ * `bool TryGetAlternateLookup(DictionaryAlternateLookup) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
#### DictionaryEntry
@@ -929,7 +937,14 @@ The class `Polyfill` includes the following extension methods:
#### HashSet
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ensurecapacity?view=net-11.0#system-collections-generic-hashset-1-ensurecapacity(system-int32))
+ * Note: No-op on older targets; the BCL grows the backing storage.
+ * `HashSetAlternateLookup GetAlternateLookup()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
+ * Note: Returns the free-standing `HashSetAlternateLookup` rather than the BCL's nested `HashSet.AlternateLookup`. Use `var` for cross-target code.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trimexcess?view=net-11.0#system-collections-generic-hashset-1-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
+ * `bool TryGetAlternateLookup(HashSetAlternateLookup)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0)
+ * Note: Lookups are O(n) on older targets; the BCL is O(1).
* `bool TryGetValue(T, T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetvalue?view=net-11.0)
@@ -1129,8 +1144,11 @@ The class `Polyfill` includes the following extension methods:
* `void AddRange(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.addrange?view=net-11.0)
* `void CopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.copyto?view=net-11.0)
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.ensurecapacity?view=net-11.0#system-collections-generic-list-1-ensurecapacity(system-int32))
+ * Note: No-op on older targets; the BCL grows the backing storage.
+ * Note: Returns void on older targets; the BCL returns int (the new capacity).
* `void InsertRange(int, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.insertrange?view=net-11.0)
* `void TrimExcess()`
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
#### Math
@@ -1269,7 +1287,9 @@ The class `Polyfill` includes the following extension methods:
#### Queue
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.ensurecapacity?view=net-11.0#system-collections-generic-queue-1-ensurecapacity(system-int32))
+ * Note: No-op on older targets; the BCL grows the backing storage.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.trimexcess?view=net-11.0#system-collections-generic-queue-1-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
#### Random
@@ -1519,7 +1539,9 @@ The class `Polyfill` includes the following extension methods:
#### Stack
* `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.ensurecapacity?view=net-11.0)
+ * Note: No-op on older targets; the BCL grows the backing storage.
* `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trimexcess?view=net-11.0#system-collections-generic-stack-1-trimexcess(system-int32))
+ * Note: No-op on older targets; the BCL shrinks the backing storage.
* `bool TryPeek(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypeek?view=net-11.0)
* `bool TryPop(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypop?view=net-11.0)
@@ -2098,7 +2120,7 @@ void ObjectDisposedExceptionExample(bool isDisposed)
ObjectDisposedException.ThrowIf(isDisposed, typeof(Consume));
}
```
-snippet source | anchor
+snippet source | anchor
@@ -2117,7 +2139,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/ApiBuilderTests/BuildApiTest.cs b/src/ApiBuilderTests/BuildApiTest.cs
index b05272a1..c5510dc2 100644
--- a/src/ApiBuilderTests/BuildApiTest.cs
+++ b/src/ApiBuilderTests/BuildApiTest.cs
@@ -158,6 +158,7 @@ static int WriteExtensions(StreamWriter writer, int count)
.ToList();
writer.WriteLine($"#### {name}");
writer.WriteLine();
+ WriteSectionGate(name, writer);
if (instanceMethodsForType.Count != 0)
{
foreach (var method in instanceMethodsForType.OrderBy(Key))
@@ -268,11 +269,9 @@ static List ReadPropertiesForFiles(IEnumerable files)
static void WriteType(string name, StreamWriter writer, ref int count)
{
- writer.WriteLine(
- $"""
- #### {name}
-
- """);
+ writer.WriteLine($"#### {name}");
+ writer.WriteLine();
+ WriteSectionGate(name, writer);
count++;
}
@@ -283,6 +282,7 @@ static void WriteTypeMethods(string name, StreamWriter writer, ref int count, IE
{
writer.WriteLine($"#### {name}");
writer.WriteLine();
+ WriteSectionGate(name, writer);
foreach (var method in methods.OrderBy(Key))
{
count++;
@@ -293,6 +293,30 @@ static void WriteTypeMethods(string name, StreamWriter writer, ref int count, IE
writer.WriteLine();
}
+ // Sections whose every member requires an MSBuild opt-in flag. Listed here (not
+ // derived from path) because some directories with opt-ins also contribute methods
+ // to mixed sections like StringBuilder; only fully-gated section names belong here.
+ // The anchor points to the readme heading that explains the flag.
+ static readonly Dictionary sectionGates = new()
+ {
+ ["ArgumentException"] = ("PolyArgumentExceptions", "argumentexception-1"),
+ ["ArgumentNullException"] = ("PolyArgumentExceptions", "argumentexception-1"),
+ ["ArgumentOutOfRangeException"] = ("PolyArgumentExceptions", "argumentexception-1"),
+ ["ObjectDisposedException"] = ("PolyArgumentExceptions", "argumentexception-1"),
+ ["Ensure"] = ("PolyEnsure", "ensure-1"),
+ };
+
+ static void WriteSectionGate(string sectionName, StreamWriter writer)
+ {
+ if (!sectionGates.TryGetValue(sectionName, out var gate))
+ {
+ return;
+ }
+
+ writer.WriteLine($"> Requires [`<{gate.Flag}>true{gate.Flag}>`](#{gate.Anchor}) in the consuming project.");
+ writer.WriteLine();
+ }
+
static void WriteSignature(Method method, StreamWriter writer)
{
var signature = new StringBuilder($"{method.ReturnType} {method.Identifier.Text}{BuildTypeArgs(method)}({BuildParameters(method, true)})");
@@ -314,6 +338,8 @@ static void WriteSignature(Method method, StreamWriter writer)
{
writer.WriteLine($" * `{signature}`");
}
+
+ WriteNotes(method, writer);
}
static void WriteSignature(Property method, StreamWriter writer)
@@ -328,6 +354,16 @@ static void WriteSignature(Property method, StreamWriter writer)
{
writer.WriteLine($" * `{signature}`");
}
+
+ WriteNotes(method, writer);
+ }
+
+ static void WriteNotes(Member member, StreamWriter writer)
+ {
+ foreach (var note in member.GetNotes())
+ {
+ writer.WriteLine($" * Note: {note}");
+ }
}
static string Key(Method method) =>
diff --git a/src/ApiBuilderTests/LinkReader.cs b/src/ApiBuilderTests/LinkReader.cs
index cd6b138f..682a6719 100644
--- a/src/ApiBuilderTests/LinkReader.cs
+++ b/src/ApiBuilderTests/LinkReader.cs
@@ -27,4 +27,27 @@ public static bool TryGetReference(this Member member, [NotNullWhen(true)] out s
reference = null;
return false;
}
+
+ public static IReadOnlyList GetNotes(this Member member)
+ {
+ List? notes = null;
+ foreach (var trivia in member.GetLeadingTrivia())
+ {
+ if (!trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
+ {
+ continue;
+ }
+
+ var comment = trivia.ToString();
+ if (!comment.StartsWith("//Note: "))
+ {
+ continue;
+ }
+
+ notes ??= [];
+ notes.Add(comment.Substring("//Note: ".Length));
+ }
+
+ return (IReadOnlyList?)notes ?? [];
+ }
}
diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs
index 353fb8c2..d52c1d9a 100644
--- a/src/Consume/Consume.cs
+++ b/src/Consume/Consume.cs
@@ -583,6 +583,44 @@ void Dictionary_Methods()
pairs.ToDictionary(StringComparer.Ordinal);
}
+#if !NET9_0_OR_GREATER
+ void Dictionary_AlternateLookup()
+ {
+ var dictionary = new Dictionary(new KeyHolderAlternateComparer());
+ var lookup = dictionary.GetAlternateLookup();
+ _ = lookup.Dictionary;
+ lookup.ContainsKey("a");
+ lookup.TryAdd("a", 1);
+ lookup.TryGetValue("a", out var value);
+ lookup.TryGetValue("a", out var actualKey, out value);
+ lookup["a"] = 2;
+ _ = lookup["a"];
+ lookup.Remove("a");
+ lookup.Remove("a", out actualKey, out value);
+
+ if (dictionary.TryGetAlternateLookup(out var maybeLookup))
+ {
+ _ = maybeLookup.Dictionary;
+ }
+ }
+
+ class KeyHolder
+ {
+ public string Name { get; set; } = "";
+ }
+
+ class KeyHolderAlternateComparer :
+ IEqualityComparer,
+ IAlternateEqualityComparer
+ {
+ public bool Equals(KeyHolder? x, KeyHolder? y) => x?.Name == y?.Name;
+ public int GetHashCode(KeyHolder target) => target.Name.GetHashCode();
+ public bool Equals(string alternate, KeyHolder other) => alternate == other.Name;
+ public int GetHashCode(string alternate) => alternate.GetHashCode();
+ public KeyHolder Create(string alternate) => new() { Name = alternate };
+ }
+#endif
+
void Lock_Methods()
{
var locker = new Lock();
@@ -837,6 +875,24 @@ void HashSet_Methods()
set.TrimExcess();
}
+#if !NET9_0_OR_GREATER
+ void HashSet_AlternateLookup()
+ {
+ var set = new HashSet(new KeyHolderAlternateComparer());
+ var lookup = set.GetAlternateLookup();
+ _ = lookup.Set;
+ lookup.Add("a");
+ lookup.Contains("a");
+ lookup.TryGetValue("a", out var actual);
+ lookup.Remove("a");
+
+ if (set.TryGetAlternateLookup(out var maybeLookup))
+ {
+ _ = maybeLookup.Set;
+ }
+ }
+#endif
+
#if FeatureHttp
void HttpClient_Methods(HttpClient target)
{
diff --git a/src/Polyfill/DictionaryAlternateLookup.cs b/src/Polyfill/DictionaryAlternateLookup.cs
new file mode 100644
index 00000000..8c1f78a8
--- /dev/null
+++ b/src/Polyfill/DictionaryAlternateLookup.cs
@@ -0,0 +1,158 @@
+#if !NET9_0_OR_GREATER
+
+namespace Polyfills;
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+///
+/// Provides a type that may be used to perform operations on a
+/// using a as a key instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking
+/// on each. The native .NET 9+ counterpart
+/// is O(1) by using the dictionary's internal hash buckets.
+///
+//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.alternatelookup-1?view=net-11.0
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct DictionaryAlternateLookup
+ where TKey : notnull
+{
+ readonly Dictionary dictionary;
+ readonly IAlternateEqualityComparer comparer;
+
+ internal DictionaryAlternateLookup(
+ Dictionary dictionary,
+ IAlternateEqualityComparer comparer)
+ {
+ this.dictionary = dictionary;
+ this.comparer = comparer;
+ }
+
+ /// Gets the against which this instance performs operations.
+ public Dictionary Dictionary => dictionary;
+
+ /// Gets or sets the value associated with the specified alternate key.
+ public TValue this[TAlternateKey key]
+ {
+ get
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ return dictionary[actual];
+ }
+
+ throw new KeyNotFoundException();
+ }
+ set
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary[actual] = value;
+ }
+ else
+ {
+ dictionary.Add(comparer.Create(key), value);
+ }
+ }
+ }
+
+ /// Determines whether the dictionary contains the specified alternate key.
+ public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _);
+
+ /// Attempts to add the specified alternate key and value to the dictionary.
+ public bool TryAdd(TAlternateKey key, TValue value)
+ {
+ if (TryFindKey(key, out _))
+ {
+ return false;
+ }
+
+ dictionary.Add(comparer.Create(key), value);
+ return true;
+ }
+
+ /// Gets the value associated with the specified alternate key.
+ public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ value = dictionary[actual];
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary.
+ public bool TryGetValue(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ /// Removes the value with the specified alternate key from the dictionary.
+ public bool Remove(TAlternateKey key)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary.Remove(actual);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters.
+ public bool Remove(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ dictionary.Remove(actualKey);
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey)
+ {
+ foreach (var existing in dictionary.Keys)
+ {
+ if (comparer.Equals(alternate, existing))
+ {
+ actualKey = existing;
+ return true;
+ }
+ }
+
+ actualKey = default;
+ return false;
+ }
+}
+
+#endif
diff --git a/src/Polyfill/HashSetAlternateLookup.cs b/src/Polyfill/HashSetAlternateLookup.cs
new file mode 100644
index 00000000..56b48812
--- /dev/null
+++ b/src/Polyfill/HashSetAlternateLookup.cs
@@ -0,0 +1,90 @@
+#if !NET9_0_OR_GREATER
+
+namespace Polyfills;
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+///
+/// Provides a type that may be used to perform operations on a
+/// using a instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the set and invoking
+/// on each element. The native .NET 9+
+/// counterpart is O(1) by using the set's internal hash buckets.
+///
+//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.alternatelookup-1?view=net-11.0
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct HashSetAlternateLookup
+{
+ readonly HashSet set;
+ readonly IAlternateEqualityComparer comparer;
+
+ internal HashSetAlternateLookup(
+ HashSet set,
+ IAlternateEqualityComparer comparer)
+ {
+ this.set = set;
+ this.comparer = comparer;
+ }
+
+ /// Gets the against which this instance performs operations.
+ public HashSet Set => set;
+
+ /// Adds an item that is created from the specified alternate value, if no equal value already exists.
+ public bool Add(TAlternate item)
+ {
+ if (TryFindValue(item, out _))
+ {
+ return false;
+ }
+
+ set.Add(comparer.Create(item));
+ return true;
+ }
+
+ /// Determines whether the set contains a value equal to the specified alternate value.
+ public bool Contains(TAlternate item) => TryFindValue(item, out _);
+
+ /// Removes the value equal to the specified alternate value from the set.
+ public bool Remove(TAlternate item)
+ {
+ if (TryFindValue(item, out var actual))
+ {
+ set.Remove(actual);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// Searches the set for the value equal to the specified alternate value and returns it.
+ public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) =>
+ TryFindValue(equalValue, out actualValue);
+
+ bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue)
+ {
+ foreach (var existing in set)
+ {
+ if (comparer.Equals(alternate, existing))
+ {
+ actualValue = existing;
+ return true;
+ }
+ }
+
+ actualValue = default;
+ return false;
+ }
+}
+
+#endif
diff --git a/src/Polyfill/IAlternateEqualityComparer.cs b/src/Polyfill/IAlternateEqualityComparer.cs
new file mode 100644
index 00000000..e82f7ca7
--- /dev/null
+++ b/src/Polyfill/IAlternateEqualityComparer.cs
@@ -0,0 +1,38 @@
+#if !NET9_0_OR_GREATER
+
+namespace System.Collections.Generic;
+
+using Diagnostics.CodeAnalysis;
+
+///
+/// Implemented by an to support comparing
+/// a instance with a instance.
+///
+//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ialternateequalitycomparer-2?view=net-11.0
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+interface IAlternateEqualityComparer
+{
+ ///
+ /// Determines whether the specified equals the specified .
+ ///
+ bool Equals(TAlternate alternate, T other);
+
+ ///
+ /// Returns a hash code for the specified alternate instance.
+ ///
+ int GetHashCode(TAlternate alternate);
+
+ ///
+ /// Creates a that is considered equal to the specified .
+ ///
+ T Create(TAlternate alternate);
+}
+
+#else
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Collections.Generic.IAlternateEqualityComparer<,>))]
+#endif
diff --git a/src/Polyfill/Polyfill_Dictionary.cs b/src/Polyfill/Polyfill_Dictionary.cs
index c5c33977..26d3b569 100644
--- a/src/Polyfill/Polyfill_Dictionary.cs
+++ b/src/Polyfill/Polyfill_Dictionary.cs
@@ -1,5 +1,6 @@
namespace Polyfills;
+using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
@@ -57,6 +58,7 @@ public static bool Remove(
/// Ensures that the capacity of this dictionary is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0
+ //Note: No-op on older targets; the BCL grows the backing storage.
public static void EnsureCapacity(this Dictionary target, int capacity)
{
}
@@ -65,6 +67,7 @@ public static void EnsureCapacity(this Dictionary ta
/// Sets the capacity of this dictionary to hold up a specified number of entries without any further expansion of its backing storage.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess(system-int32)
+ //Note: No-op on older targets; the BCL shrinks the backing storage.
public static void TrimExcess(this Dictionary target, int capacity)
{
}
@@ -73,9 +76,55 @@ public static void TrimExcess(this Dictionary target
/// Sets the capacity of this dictionary to what it would be if it had been originally initialized with all its entries.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess
+ //Note: No-op on older targets; the BCL shrinks the backing storage.
public static void TrimExcess(this Dictionary target)
{
}
+#endif
+
+#if !NET9_0_OR_GREATER
+
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a as a key instead of a .
+ ///
+ //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0
+ //Note: Lookups are O(n) on older targets; the BCL is O(1).
+ //Note: Returns the free-standing `DictionaryAlternateLookup` rather than the BCL's nested `Dictionary.AlternateLookup`. Use `var` for cross-target code.
+ public static DictionaryAlternateLookup GetAlternateLookup(
+ this Dictionary target)
+ where TKey : notnull
+ {
+ if (target.Comparer is not IAlternateEqualityComparer comparer)
+ {
+ throw new InvalidOperationException(
+ $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>.");
+ }
+
+ return new(target, comparer);
+ }
+
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a as a key instead of a .
+ ///
+ //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetalternatelookup?view=net-11.0
+ //Note: Lookups are O(n) on older targets; the BCL is O(1).
+ public static bool TryGetAlternateLookup(
+ this Dictionary target,
+ out DictionaryAlternateLookup lookup)
+ where TKey : notnull
+ {
+ if (target.Comparer is IAlternateEqualityComparer comparer)
+ {
+ lookup = new(target, comparer);
+ return true;
+ }
+
+ lookup = default;
+ return false;
+ }
+
#endif
}
diff --git a/src/Polyfill/Polyfill_HashSet.cs b/src/Polyfill/Polyfill_HashSet.cs
index dd5dbcde..25ea53d1 100644
--- a/src/Polyfill/Polyfill_HashSet.cs
+++ b/src/Polyfill/Polyfill_HashSet.cs
@@ -1,5 +1,6 @@
namespace Polyfills;
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -38,6 +39,7 @@ public static bool TryGetValue(
/// Ensures that the capacity of this HashSet is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ensurecapacity?view=net-11.0#system-collections-generic-hashset-1-ensurecapacity(system-int32)
+ //Note: No-op on older targets; the BCL grows the backing storage.
public static void EnsureCapacity(this HashSet target, int capacity)
{
}
@@ -50,9 +52,49 @@ public static void EnsureCapacity(this HashSet target, int capacity)
/// Sets the capacity of a HashSet object to the specified number of entries, rounded up to a nearby, implementation-specific value.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trimexcess?view=net-11.0#system-collections-generic-hashset-1-trimexcess(system-int32)
+ //Note: No-op on older targets; the BCL shrinks the backing storage.
public static void TrimExcess(this HashSet target, int capacity)
{
}
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a instead of a .
+ ///
+ //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0
+ //Note: Lookups are O(n) on older targets; the BCL is O(1).
+ //Note: Returns the free-standing `HashSetAlternateLookup` rather than the BCL's nested `HashSet.AlternateLookup`. Use `var` for cross-target code.
+ public static HashSetAlternateLookup GetAlternateLookup(
+ this HashSet target)
+ {
+ if (target.Comparer is not IAlternateEqualityComparer comparer)
+ {
+ throw new InvalidOperationException(
+ $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>.");
+ }
+
+ return new(target, comparer);
+ }
+
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a instead of a .
+ ///
+ //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0
+ //Note: Lookups are O(n) on older targets; the BCL is O(1).
+ public static bool TryGetAlternateLookup(
+ this HashSet target,
+ out HashSetAlternateLookup lookup)
+ {
+ if (target.Comparer is IAlternateEqualityComparer comparer)
+ {
+ lookup = new(target, comparer);
+ return true;
+ }
+
+ lookup = default;
+ return false;
+ }
+
#endif
}
diff --git a/src/Polyfill/Polyfill_List.cs b/src/Polyfill/Polyfill_List.cs
index fe9a65b8..32cbd23b 100644
--- a/src/Polyfill/Polyfill_List.cs
+++ b/src/Polyfill/Polyfill_List.cs
@@ -54,6 +54,8 @@ public static void CopyTo(this List target, Span destination)
/// Ensures that the capacity of this list is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.ensurecapacity?view=net-11.0#system-collections-generic-list-1-ensurecapacity(system-int32)
+ //Note: No-op on older targets; the BCL grows the backing storage.
+ //Note: Returns void on older targets; the BCL returns int (the new capacity).
public static void EnsureCapacity(this List target, int capacity)
{
}
@@ -62,6 +64,7 @@ public static void EnsureCapacity(this List target, int capacity)
/// Sets the capacity to the actual number of elements in the , if that number is less than a threshold value.
///
//Link:https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.trimexcess?view=net-11.0
+ //Note: No-op on older targets; the BCL shrinks the backing storage.
public static void TrimExcess(this List target)
{
}
diff --git a/src/Polyfill/Polyfill_Queue.cs b/src/Polyfill/Polyfill_Queue.cs
index 0bb23e74..c288a1df 100644
--- a/src/Polyfill/Polyfill_Queue.cs
+++ b/src/Polyfill/Polyfill_Queue.cs
@@ -10,6 +10,7 @@ static partial class Polyfill
/// Ensures that the capacity of this queue is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.ensurecapacity?view=net-11.0#system-collections-generic-queue-1-ensurecapacity(system-int32)
+ //Note: No-op on older targets; the BCL grows the backing storage.
public static void EnsureCapacity(this Queue target, int capacity)
{
}
@@ -22,6 +23,7 @@ public static void EnsureCapacity(this Queue target, int capacity)
/// Sets the capacity of a Queue object to the specified number of entries.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.trimexcess?view=net-11.0#system-collections-generic-queue-1-trimexcess(system-int32)
+ //Note: No-op on older targets; the BCL shrinks the backing storage.
public static void TrimExcess(this Queue target, int capacity)
{
}
diff --git a/src/Polyfill/Polyfill_Stack.cs b/src/Polyfill/Polyfill_Stack.cs
index dc5aa602..3f63a955 100644
--- a/src/Polyfill/Polyfill_Stack.cs
+++ b/src/Polyfill/Polyfill_Stack.cs
@@ -11,6 +11,7 @@ static partial class Polyfill
/// Ensures that the capacity of this Stack is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.ensurecapacity?view=net-11.0
+ //Note: No-op on older targets; the BCL grows the backing storage.
public static void EnsureCapacity(this Stack target, int capacity)
{
}
@@ -23,6 +24,7 @@ public static void EnsureCapacity(this Stack target, int capacity)
/// Sets the capacity of a Stack object to a specified number of entries.
///
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trimexcess?view=net-11.0#system-collections-generic-stack-1-trimexcess(system-int32)
+ //Note: No-op on older targets; the BCL shrinks the backing storage.
public static void TrimExcess(this Stack target, int capacity)
{
}
diff --git a/src/Split/net10.0/TypeForwardeds.cs b/src/Split/net10.0/TypeForwardeds.cs
index 1244a4bb..b17d055b 100644
--- a/src/Split/net10.0/TypeForwardeds.cs
+++ b/src/Split/net10.0/TypeForwardeds.cs
@@ -13,6 +13,7 @@
[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute))]
[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureGuardAttribute))]
[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute))]
+[assembly: TypeForwardedTo(typeof(System.Collections.Generic.IAlternateEqualityComparer<,>))]
[assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
[assembly: TypeForwardedTo(typeof(System.Collections.Generic.KeyValuePair))]
[assembly: TypeForwardedTo(typeof(System.Threading.Lock))]
diff --git a/src/Split/net11.0/TypeForwardeds.cs b/src/Split/net11.0/TypeForwardeds.cs
index 1244a4bb..b17d055b 100644
--- a/src/Split/net11.0/TypeForwardeds.cs
+++ b/src/Split/net11.0/TypeForwardeds.cs
@@ -13,6 +13,7 @@
[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute))]
[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureGuardAttribute))]
[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute))]
+[assembly: TypeForwardedTo(typeof(System.Collections.Generic.IAlternateEqualityComparer<,>))]
[assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
[assembly: TypeForwardedTo(typeof(System.Collections.Generic.KeyValuePair))]
[assembly: TypeForwardedTo(typeof(System.Threading.Lock))]
diff --git a/src/Split/net461/DictionaryAlternateLookup.cs b/src/Split/net461/DictionaryAlternateLookup.cs
new file mode 100644
index 00000000..7405eb10
--- /dev/null
+++ b/src/Split/net461/DictionaryAlternateLookup.cs
@@ -0,0 +1,136 @@
+//
+#pragma warning disable
+namespace Polyfills;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+///
+/// Provides a type that may be used to perform operations on a
+/// using a as a key instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking
+/// on each. The native .NET 9+ counterpart
+/// is O(1) by using the dictionary's internal hash buckets.
+///
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct DictionaryAlternateLookup
+ where TKey : notnull
+{
+ readonly Dictionary dictionary;
+ readonly IAlternateEqualityComparer comparer;
+ internal DictionaryAlternateLookup(
+ Dictionary dictionary,
+ IAlternateEqualityComparer comparer)
+ {
+ this.dictionary = dictionary;
+ this.comparer = comparer;
+ }
+ /// Gets the against which this instance performs operations.
+ public Dictionary Dictionary => dictionary;
+ /// Gets or sets the value associated with the specified alternate key.
+ public TValue this[TAlternateKey key]
+ {
+ get
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ return dictionary[actual];
+ }
+ throw new KeyNotFoundException();
+ }
+ set
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary[actual] = value;
+ }
+ else
+ {
+ dictionary.Add(comparer.Create(key), value);
+ }
+ }
+ }
+ /// Determines whether the dictionary contains the specified alternate key.
+ public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _);
+ /// Attempts to add the specified alternate key and value to the dictionary.
+ public bool TryAdd(TAlternateKey key, TValue value)
+ {
+ if (TryFindKey(key, out _))
+ {
+ return false;
+ }
+ dictionary.Add(comparer.Create(key), value);
+ return true;
+ }
+ /// Gets the value associated with the specified alternate key.
+ public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ value = dictionary[actual];
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary.
+ public bool TryGetValue(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ /// Removes the value with the specified alternate key from the dictionary.
+ public bool Remove(TAlternateKey key)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary.Remove(actual);
+ return true;
+ }
+ return false;
+ }
+ /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters.
+ public bool Remove(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ dictionary.Remove(actualKey);
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey)
+ {
+ foreach (var existing in dictionary.Keys)
+ {
+ if (comparer.Equals(alternate, existing))
+ {
+ actualKey = existing;
+ return true;
+ }
+ }
+ actualKey = default;
+ return false;
+ }
+}
diff --git a/src/Split/net461/HashSetAlternateLookup.cs b/src/Split/net461/HashSetAlternateLookup.cs
new file mode 100644
index 00000000..f63523f7
--- /dev/null
+++ b/src/Split/net461/HashSetAlternateLookup.cs
@@ -0,0 +1,75 @@
+//
+#pragma warning disable
+namespace Polyfills;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+///
+/// Provides a type that may be used to perform operations on a
+/// using a instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the set and invoking
+/// on each element. The native .NET 9+
+/// counterpart is O(1) by using the set's internal hash buckets.
+///
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct HashSetAlternateLookup
+{
+ readonly HashSet set;
+ readonly IAlternateEqualityComparer comparer;
+ internal HashSetAlternateLookup(
+ HashSet set,
+ IAlternateEqualityComparer comparer)
+ {
+ this.set = set;
+ this.comparer = comparer;
+ }
+ /// Gets the against which this instance performs operations.
+ public HashSet Set => set;
+ /// Adds an item that is created from the specified alternate value, if no equal value already exists.
+ public bool Add(TAlternate item)
+ {
+ if (TryFindValue(item, out _))
+ {
+ return false;
+ }
+ set.Add(comparer.Create(item));
+ return true;
+ }
+ /// Determines whether the set contains a value equal to the specified alternate value.
+ public bool Contains(TAlternate item) => TryFindValue(item, out _);
+ /// Removes the value equal to the specified alternate value from the set.
+ public bool Remove(TAlternate item)
+ {
+ if (TryFindValue(item, out var actual))
+ {
+ set.Remove(actual);
+ return true;
+ }
+ return false;
+ }
+ /// Searches the set for the value equal to the specified alternate value and returns it.
+ public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) =>
+ TryFindValue(equalValue, out actualValue);
+ bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue)
+ {
+ foreach (var existing in set)
+ {
+ if (comparer.Equals(alternate, existing))
+ {
+ actualValue = existing;
+ return true;
+ }
+ }
+ actualValue = default;
+ return false;
+ }
+}
diff --git a/src/Split/net461/IAlternateEqualityComparer.cs b/src/Split/net461/IAlternateEqualityComparer.cs
new file mode 100644
index 00000000..9b3d7a09
--- /dev/null
+++ b/src/Split/net461/IAlternateEqualityComparer.cs
@@ -0,0 +1,29 @@
+//
+#pragma warning disable
+namespace System.Collections.Generic;
+using Diagnostics.CodeAnalysis;
+///
+/// Implemented by an to support comparing
+/// a instance with a instance.
+///
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+interface IAlternateEqualityComparer
+{
+ ///
+ /// Determines whether the specified equals the specified .
+ ///
+ bool Equals(TAlternate alternate, T other);
+ ///
+ /// Returns a hash code for the specified alternate instance.
+ ///
+ int GetHashCode(TAlternate alternate);
+ ///
+ /// Creates a that is considered equal to the specified .
+ ///
+ T Create(TAlternate alternate);
+}
diff --git a/src/Split/net461/Polyfill_Dictionary.cs b/src/Split/net461/Polyfill_Dictionary.cs
index 0c560b6a..364b96cd 100644
--- a/src/Split/net461/Polyfill_Dictionary.cs
+++ b/src/Split/net461/Polyfill_Dictionary.cs
@@ -1,6 +1,7 @@
//
#pragma warning disable
namespace Polyfills;
+using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
@@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target
public static void TrimExcess(this Dictionary target)
{
}
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a as a key instead of a .
+ ///
+ public static DictionaryAlternateLookup GetAlternateLookup(
+ this Dictionary target)
+ where TKey : notnull
+ {
+ if (target.Comparer is not IAlternateEqualityComparer comparer)
+ {
+ throw new InvalidOperationException(
+ $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>.");
+ }
+ return new(target, comparer);
+ }
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a as a key instead of a .
+ ///
+ public static bool TryGetAlternateLookup(
+ this Dictionary target,
+ out DictionaryAlternateLookup lookup)
+ where TKey : notnull
+ {
+ if (target.Comparer is IAlternateEqualityComparer comparer)
+ {
+ lookup = new(target, comparer);
+ return true;
+ }
+ lookup = default;
+ return false;
+ }
}
diff --git a/src/Split/net461/Polyfill_HashSet.cs b/src/Split/net461/Polyfill_HashSet.cs
index 4c37b22d..95f54884 100644
--- a/src/Split/net461/Polyfill_HashSet.cs
+++ b/src/Split/net461/Polyfill_HashSet.cs
@@ -1,6 +1,7 @@
//
#pragma warning disable
namespace Polyfills;
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
static partial class Polyfill
@@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity)
public static void TrimExcess(this HashSet target, int capacity)
{
}
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a instead of a .
+ ///
+ public static HashSetAlternateLookup GetAlternateLookup(
+ this HashSet target)
+ {
+ if (target.Comparer is not IAlternateEqualityComparer comparer)
+ {
+ throw new InvalidOperationException(
+ $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>.");
+ }
+ return new(target, comparer);
+ }
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a instead of a .
+ ///
+ public static bool TryGetAlternateLookup(
+ this HashSet target,
+ out HashSetAlternateLookup lookup)
+ {
+ if (target.Comparer is IAlternateEqualityComparer comparer)
+ {
+ lookup = new(target, comparer);
+ return true;
+ }
+ lookup = default;
+ return false;
+ }
}
diff --git a/src/Split/net462/DictionaryAlternateLookup.cs b/src/Split/net462/DictionaryAlternateLookup.cs
new file mode 100644
index 00000000..7405eb10
--- /dev/null
+++ b/src/Split/net462/DictionaryAlternateLookup.cs
@@ -0,0 +1,136 @@
+//
+#pragma warning disable
+namespace Polyfills;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+///
+/// Provides a type that may be used to perform operations on a
+/// using a as a key instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking
+/// on each. The native .NET 9+ counterpart
+/// is O(1) by using the dictionary's internal hash buckets.
+///
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct DictionaryAlternateLookup
+ where TKey : notnull
+{
+ readonly Dictionary dictionary;
+ readonly IAlternateEqualityComparer comparer;
+ internal DictionaryAlternateLookup(
+ Dictionary dictionary,
+ IAlternateEqualityComparer comparer)
+ {
+ this.dictionary = dictionary;
+ this.comparer = comparer;
+ }
+ /// Gets the against which this instance performs operations.
+ public Dictionary Dictionary => dictionary;
+ /// Gets or sets the value associated with the specified alternate key.
+ public TValue this[TAlternateKey key]
+ {
+ get
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ return dictionary[actual];
+ }
+ throw new KeyNotFoundException();
+ }
+ set
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary[actual] = value;
+ }
+ else
+ {
+ dictionary.Add(comparer.Create(key), value);
+ }
+ }
+ }
+ /// Determines whether the dictionary contains the specified alternate key.
+ public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _);
+ /// Attempts to add the specified alternate key and value to the dictionary.
+ public bool TryAdd(TAlternateKey key, TValue value)
+ {
+ if (TryFindKey(key, out _))
+ {
+ return false;
+ }
+ dictionary.Add(comparer.Create(key), value);
+ return true;
+ }
+ /// Gets the value associated with the specified alternate key.
+ public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ value = dictionary[actual];
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary.
+ public bool TryGetValue(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ /// Removes the value with the specified alternate key from the dictionary.
+ public bool Remove(TAlternateKey key)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary.Remove(actual);
+ return true;
+ }
+ return false;
+ }
+ /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters.
+ public bool Remove(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ dictionary.Remove(actualKey);
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey)
+ {
+ foreach (var existing in dictionary.Keys)
+ {
+ if (comparer.Equals(alternate, existing))
+ {
+ actualKey = existing;
+ return true;
+ }
+ }
+ actualKey = default;
+ return false;
+ }
+}
diff --git a/src/Split/net462/HashSetAlternateLookup.cs b/src/Split/net462/HashSetAlternateLookup.cs
new file mode 100644
index 00000000..f63523f7
--- /dev/null
+++ b/src/Split/net462/HashSetAlternateLookup.cs
@@ -0,0 +1,75 @@
+//
+#pragma warning disable
+namespace Polyfills;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+///
+/// Provides a type that may be used to perform operations on a
+/// using a instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the set and invoking
+/// on each element. The native .NET 9+
+/// counterpart is O(1) by using the set's internal hash buckets.
+///
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct HashSetAlternateLookup
+{
+ readonly HashSet set;
+ readonly IAlternateEqualityComparer comparer;
+ internal HashSetAlternateLookup(
+ HashSet set,
+ IAlternateEqualityComparer comparer)
+ {
+ this.set = set;
+ this.comparer = comparer;
+ }
+ /// Gets the against which this instance performs operations.
+ public HashSet Set => set;
+ /// Adds an item that is created from the specified alternate value, if no equal value already exists.
+ public bool Add(TAlternate item)
+ {
+ if (TryFindValue(item, out _))
+ {
+ return false;
+ }
+ set.Add(comparer.Create(item));
+ return true;
+ }
+ /// Determines whether the set contains a value equal to the specified alternate value.
+ public bool Contains(TAlternate item) => TryFindValue(item, out _);
+ /// Removes the value equal to the specified alternate value from the set.
+ public bool Remove(TAlternate item)
+ {
+ if (TryFindValue(item, out var actual))
+ {
+ set.Remove(actual);
+ return true;
+ }
+ return false;
+ }
+ /// Searches the set for the value equal to the specified alternate value and returns it.
+ public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) =>
+ TryFindValue(equalValue, out actualValue);
+ bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue)
+ {
+ foreach (var existing in set)
+ {
+ if (comparer.Equals(alternate, existing))
+ {
+ actualValue = existing;
+ return true;
+ }
+ }
+ actualValue = default;
+ return false;
+ }
+}
diff --git a/src/Split/net462/IAlternateEqualityComparer.cs b/src/Split/net462/IAlternateEqualityComparer.cs
new file mode 100644
index 00000000..9b3d7a09
--- /dev/null
+++ b/src/Split/net462/IAlternateEqualityComparer.cs
@@ -0,0 +1,29 @@
+//
+#pragma warning disable
+namespace System.Collections.Generic;
+using Diagnostics.CodeAnalysis;
+///
+/// Implemented by an to support comparing
+/// a instance with a instance.
+///
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+interface IAlternateEqualityComparer
+{
+ ///
+ /// Determines whether the specified equals the specified .
+ ///
+ bool Equals(TAlternate alternate, T other);
+ ///
+ /// Returns a hash code for the specified alternate instance.
+ ///
+ int GetHashCode(TAlternate alternate);
+ ///
+ /// Creates a that is considered equal to the specified .
+ ///
+ T Create(TAlternate alternate);
+}
diff --git a/src/Split/net462/Polyfill_Dictionary.cs b/src/Split/net462/Polyfill_Dictionary.cs
index 0c560b6a..364b96cd 100644
--- a/src/Split/net462/Polyfill_Dictionary.cs
+++ b/src/Split/net462/Polyfill_Dictionary.cs
@@ -1,6 +1,7 @@
//
#pragma warning disable
namespace Polyfills;
+using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
@@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target
public static void TrimExcess(this Dictionary target)
{
}
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a as a key instead of a .
+ ///
+ public static DictionaryAlternateLookup GetAlternateLookup(
+ this Dictionary target)
+ where TKey : notnull
+ {
+ if (target.Comparer is not IAlternateEqualityComparer comparer)
+ {
+ throw new InvalidOperationException(
+ $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>.");
+ }
+ return new(target, comparer);
+ }
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a as a key instead of a .
+ ///
+ public static bool TryGetAlternateLookup(
+ this Dictionary target,
+ out DictionaryAlternateLookup lookup)
+ where TKey : notnull
+ {
+ if (target.Comparer is IAlternateEqualityComparer comparer)
+ {
+ lookup = new(target, comparer);
+ return true;
+ }
+ lookup = default;
+ return false;
+ }
}
diff --git a/src/Split/net462/Polyfill_HashSet.cs b/src/Split/net462/Polyfill_HashSet.cs
index 4c37b22d..95f54884 100644
--- a/src/Split/net462/Polyfill_HashSet.cs
+++ b/src/Split/net462/Polyfill_HashSet.cs
@@ -1,6 +1,7 @@
//
#pragma warning disable
namespace Polyfills;
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
static partial class Polyfill
@@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity)
public static void TrimExcess(this HashSet target, int capacity)
{
}
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a instead of a .
+ ///
+ public static HashSetAlternateLookup GetAlternateLookup(
+ this HashSet target)
+ {
+ if (target.Comparer is not IAlternateEqualityComparer comparer)
+ {
+ throw new InvalidOperationException(
+ $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>.");
+ }
+ return new(target, comparer);
+ }
+ ///
+ /// Gets an instance of a type that may be used to perform operations on the current
+ /// using a instead of a .
+ ///
+ public static bool TryGetAlternateLookup(
+ this HashSet target,
+ out HashSetAlternateLookup lookup)
+ {
+ if (target.Comparer is IAlternateEqualityComparer comparer)
+ {
+ lookup = new(target, comparer);
+ return true;
+ }
+ lookup = default;
+ return false;
+ }
}
diff --git a/src/Split/net47/DictionaryAlternateLookup.cs b/src/Split/net47/DictionaryAlternateLookup.cs
new file mode 100644
index 00000000..7405eb10
--- /dev/null
+++ b/src/Split/net47/DictionaryAlternateLookup.cs
@@ -0,0 +1,136 @@
+//
+#pragma warning disable
+namespace Polyfills;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+///
+/// Provides a type that may be used to perform operations on a
+/// using a as a key instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking
+/// on each. The native .NET 9+ counterpart
+/// is O(1) by using the dictionary's internal hash buckets.
+///
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct DictionaryAlternateLookup
+ where TKey : notnull
+{
+ readonly Dictionary dictionary;
+ readonly IAlternateEqualityComparer comparer;
+ internal DictionaryAlternateLookup(
+ Dictionary dictionary,
+ IAlternateEqualityComparer comparer)
+ {
+ this.dictionary = dictionary;
+ this.comparer = comparer;
+ }
+ /// Gets the against which this instance performs operations.
+ public Dictionary Dictionary => dictionary;
+ /// Gets or sets the value associated with the specified alternate key.
+ public TValue this[TAlternateKey key]
+ {
+ get
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ return dictionary[actual];
+ }
+ throw new KeyNotFoundException();
+ }
+ set
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary[actual] = value;
+ }
+ else
+ {
+ dictionary.Add(comparer.Create(key), value);
+ }
+ }
+ }
+ /// Determines whether the dictionary contains the specified alternate key.
+ public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _);
+ /// Attempts to add the specified alternate key and value to the dictionary.
+ public bool TryAdd(TAlternateKey key, TValue value)
+ {
+ if (TryFindKey(key, out _))
+ {
+ return false;
+ }
+ dictionary.Add(comparer.Create(key), value);
+ return true;
+ }
+ /// Gets the value associated with the specified alternate key.
+ public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ value = dictionary[actual];
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary.
+ public bool TryGetValue(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ /// Removes the value with the specified alternate key from the dictionary.
+ public bool Remove(TAlternateKey key)
+ {
+ if (TryFindKey(key, out var actual))
+ {
+ dictionary.Remove(actual);
+ return true;
+ }
+ return false;
+ }
+ /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters.
+ public bool Remove(
+ TAlternateKey key,
+ [MaybeNullWhen(false)] out TKey actualKey,
+ [MaybeNullWhen(false)] out TValue value)
+ {
+ if (TryFindKey(key, out actualKey))
+ {
+ value = dictionary[actualKey];
+ dictionary.Remove(actualKey);
+ return true;
+ }
+ value = default;
+ return false;
+ }
+ bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey)
+ {
+ foreach (var existing in dictionary.Keys)
+ {
+ if (comparer.Equals(alternate, existing))
+ {
+ actualKey = existing;
+ return true;
+ }
+ }
+ actualKey = default;
+ return false;
+ }
+}
diff --git a/src/Split/net47/HashSetAlternateLookup.cs b/src/Split/net47/HashSetAlternateLookup.cs
new file mode 100644
index 00000000..f63523f7
--- /dev/null
+++ b/src/Split/net47/HashSetAlternateLookup.cs
@@ -0,0 +1,75 @@
+//
+#pragma warning disable
+namespace Polyfills;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+///
+/// Provides a type that may be used to perform operations on a
+/// using a instead of a .
+///
+///
+/// This polyfill performs O(n) lookups by linearly scanning the set and invoking
+/// on each element. The native .NET 9+
+/// counterpart is O(1) by using the set's internal hash buckets.
+///
+[ExcludeFromCodeCoverage]
+[DebuggerNonUserCode]
+#if PolyUseEmbeddedAttribute
+[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
+#endif
+#if PolyPublic
+public
+#endif
+readonly struct HashSetAlternateLookup
+{
+ readonly HashSet set;
+ readonly IAlternateEqualityComparer comparer;
+ internal HashSetAlternateLookup(
+ HashSet