From 888aace33bd1ec004d4fbbef76951898f3323bf5 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 10 Sep 2026 20:31:10 +1000 Subject: [PATCH] Add net9 File.WriteAllBytes over a span One member. Diffing the whole byte writing family showed the rest was already covered: AppendAllBytes in both forms, both AppendAllBytesAsync overloads, both WriteAllBytesAsync overloads and ReadAllBytesAsync are all polyfilled already. WriteAllBytes(string, ReadOnlySpan) was the only gap. Verified against net11 that the span overload is indistinguishable from the array overload: identical content for empty, small and 100KB payloads, the same truncation of a longer existing file, the same creation of a missing one, and the same exception for a null, empty or whitespace path, a missing directory and a path that is a directory. So the polyfill delegates to the array overload rather than opening its own FileStream, which would have meant guessing at the file mode and sharing. Also fixes the //Link: on the existing WriteAllBytesAsync(string, ReadOnlyMemory, CancellationToken), which pointed at AppendAllBytesAsync. API count 1161 -> 1162. --- apiCount.include.md | 40 ++++---- api_list.include.md | 5 +- assemblySize.include.md | 60 +++++------ readme.md | 105 ++++++++++--------- src/Consume/Consume.cs | 5 + src/Polyfill/FilePolyfill.cs | 12 ++- src/Split/net461/FilePolyfill.cs | 6 ++ src/Split/net462/FilePolyfill.cs | 6 ++ src/Split/net47/FilePolyfill.cs | 6 ++ src/Split/net471/FilePolyfill.cs | 6 ++ src/Split/net472/FilePolyfill.cs | 6 ++ src/Split/net48/FilePolyfill.cs | 6 ++ src/Split/net481/FilePolyfill.cs | 6 ++ src/Split/net5.0/FilePolyfill.cs | 6 ++ src/Split/net6.0/FilePolyfill.cs | 6 ++ src/Split/net7.0/FilePolyfill.cs | 6 ++ src/Split/net8.0/FilePolyfill.cs | 6 ++ src/Split/netcoreapp2.0/FilePolyfill.cs | 6 ++ src/Split/netcoreapp2.1/FilePolyfill.cs | 6 ++ src/Split/netcoreapp2.2/FilePolyfill.cs | 6 ++ src/Split/netcoreapp3.0/FilePolyfill.cs | 6 ++ src/Split/netcoreapp3.1/FilePolyfill.cs | 6 ++ src/Split/netstandard2.0/FilePolyfill.cs | 6 ++ src/Split/netstandard2.1/FilePolyfill.cs | 6 ++ src/Split/uap10.0/FilePolyfill.cs | 6 ++ src/Tests/FileWriteAllBytesSpanTests.cs | 125 +++++++++++++++++++++++ 26 files changed, 363 insertions(+), 103 deletions(-) create mode 100644 src/Tests/FileWriteAllBytesSpanTests.cs diff --git a/apiCount.include.md b/apiCount.include.md index ce3a4e2c..e502c84a 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,28 +1,28 @@ -**API count: 1161** +**API count: 1162** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 1069 | -| `net462` | 1069 | -| `net47` | 1068 | -| `net471` | 1067 | -| `net472` | 1063 | -| `net48` | 1063 | -| `net481` | 1063 | -| `netstandard2.0` | 1065 | -| `netstandard2.1` | 898 | -| `netcoreapp2.0` | 988 | -| `netcoreapp2.1` | 909 | -| `netcoreapp2.2` | 909 | -| `netcoreapp3.0` | 870 | -| `netcoreapp3.1` | 869 | -| `net5.0` | 747 | -| `net6.0` | 643 | -| `net7.0` | 502 | -| `net8.0` | 381 | +| `net461` | 1070 | +| `net462` | 1070 | +| `net47` | 1069 | +| `net471` | 1068 | +| `net472` | 1064 | +| `net48` | 1064 | +| `net481` | 1064 | +| `netstandard2.0` | 1066 | +| `netstandard2.1` | 899 | +| `netcoreapp2.0` | 989 | +| `netcoreapp2.1` | 910 | +| `netcoreapp2.2` | 910 | +| `netcoreapp3.0` | 871 | +| `netcoreapp3.1` | 870 | +| `net5.0` | 748 | +| `net6.0` | 644 | +| `net7.0` | 503 | +| `net8.0` | 382 | | `net9.0` | 243 | | `net10.0` | 182 | | `net11.0` | 58 | -| `uap10.0` | 1055 | +| `uap10.0` | 1056 | diff --git a/api_list.include.md b/api_list.include.md index 870b7479..052722f6 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -464,8 +464,11 @@ * `Task ReadAllTextAsync(string, Encoding, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readalltextasync?view=net-11.0#system-io-file-readalltextasync(system-string-system-text-encoding-system-threading-cancellationtoken)) * `IAsyncEnumerable ReadLinesAsync(string, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readlinesasync?view=net-11.0#system-io-file-readalllinesasync(system-string-system-threading-cancellationtoken)) * `IAsyncEnumerable ReadLinesAsync(string, Encoding, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readlinesasync?view=net-11.0#system-io-file-readalllinesasync(system-string-system-text-encoding-system-threading-cancellationtoken)) + * `void WriteAllBytes(string, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytes?view=net-11.0#system-io-file-writeallbytes(system-string-system-readonlyspan((system-byte)))) + * Note: Copies the span to an array and uses the array overload, so this allocates where the BCL writes straight from the span. + * Note: Only reached when the argument is already a ReadOnlySpan. A byte array binds to the BCL array overload, exactly as it does without Polyfill. * `Task WriteAllBytesAsync(string, byte[], CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytesasync?view=net-11.0#system-io-file-writeallbytesasync(system-string-system-byte()-system-threading-cancellationtoken)) - * `Task WriteAllBytesAsync(string, ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.appendallbytesasync?view=net-11.0#system-io-file-appendallbytesasync(system-string-system-readonlymemory((system-byte))-system-threading-cancellationtoken)) + * `Task WriteAllBytesAsync(string, ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytesasync?view=net-11.0#system-io-file-writeallbytesasync(system-string-system-readonlymemory((system-byte))-system-threading-cancellationtoken)) * `Task WriteAllLinesAsync(string, IEnumerable, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writealllinesasync?view=net-11.0#system-io-file-writealllinesasync(system-string-system-collections-generic-ienumerable((system-string))-system-threading-cancellationtoken)) * `Task WriteAllLinesAsync(string, IEnumerable, Encoding, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writealllinesasync?view=net-11.0#system-io-file-writealllinesasync(system-string-system-collections-generic-ienumerable((system-string))-system-text-encoding-system-threading-cancellationtoken)) * `void WriteAllText(string, ReadOnlySpan, Encoding)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writealltext?view=net-11.0#system-io-file-writealltext(system-string-system-readonlyspan((system-char))-system-text-encoding)) diff --git a/assemblySize.include.md b/assemblySize.include.md index fb715bc2..f0111a0b 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -3,21 +3,21 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 385.5KB | +377.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netstandard2.1 | 8.5KB | 341.5KB | +333.0KB | +9.0KB | +6.5KB | +8.5KB | +13.5KB | -| net461 | 8.5KB | 384.0KB | +375.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 388.0KB | +381.0KB | +8.5KB | +6.0KB | +8.5KB | +13.0KB | +| netstandard2.1 | 8.5KB | 341.5KB | +333.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 384.5KB | +376.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net462 | 7.0KB | 388.0KB | +381.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | | net47 | 7.0KB | 387.5KB | +380.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net471 | 8.5KB | 386.5KB | +378.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 385.5KB | +377.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| net48 | 8.5KB | 385.5KB | +377.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| net481 | 8.5KB | 385.5KB | +377.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.0 | 9.0KB | 363.5KB | +354.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 344.0KB | +335.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.2 | 9.0KB | 344.0KB | +335.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.0 | 9.5KB | 342.5KB | +333.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.1 | 9.5KB | 341.0KB | +331.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| net5.0 | 9.5KB | 306.5KB | +297.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net6.0 | 10.0KB | 247.5KB | +237.5KB | +9.5KB | +6.5KB | +512bytes | +3.0KB | +| net471 | 8.5KB | 386.5KB | +378.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net472 | 8.5KB | 385.5KB | +377.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 385.5KB | +377.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net481 | 8.5KB | 385.5KB | +377.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 363.5KB | +354.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 344.5KB | +335.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 344.5KB | +335.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 342.5KB | +333.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| netcoreapp3.1 | 9.5KB | 341.0KB | +331.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net5.0 | 9.5KB | 306.5KB | +297.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net6.0 | 10.0KB | 247.5KB | +237.5KB | +9.5KB | +7.0KB | +512bytes | +3.5KB | | net7.0 | 10.0KB | 216.0KB | +206.0KB | +9.5KB | +6.0KB | +512bytes | +3.5KB | | net8.0 | 9.5KB | 186.5KB | +177.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB | | net9.0 | 9.5KB | 117.0KB | +107.5KB | +8.0KB | | +512bytes | +3.0KB | @@ -30,22 +30,22 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 561.3KB | +553.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netstandard2.1 | 8.5KB | 490.3KB | +481.8KB | +16.7KB | +8.2KB | +13.4KB | +18.9KB | -| net461 | 8.5KB | 560.8KB | +552.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 564.8KB | +557.8KB | +16.2KB | +7.7KB | +13.4KB | +18.4KB | -| net47 | 7.0KB | 564.0KB | +557.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net471 | 8.5KB | 562.7KB | +554.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 560.6KB | +552.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| net48 | 8.5KB | 560.6KB | +552.1KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| net481 | 8.5KB | 560.6KB | +552.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.0 | 9.0KB | 528.7KB | +519.7KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 496.5KB | +487.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.2 | 9.0KB | 496.5KB | +487.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.0 | 9.5KB | 488.3KB | +478.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.1 | 9.5KB | 486.8KB | +477.3KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| net5.0 | 9.5KB | 434.8KB | +425.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net6.0 | 10.0KB | 355.7KB | +345.7KB | +17.2KB | +8.2KB | +1.1KB | +3.7KB | -| net7.0 | 10.0KB | 307.3KB | +297.3KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | +| netstandard2.1 | 8.5KB | 490.3KB | +481.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 561.3KB | +552.8KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net462 | 7.0KB | 564.8KB | +557.8KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net47 | 7.0KB | 564.1KB | +557.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net471 | 8.5KB | 562.7KB | +554.2KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net472 | 8.5KB | 560.6KB | +552.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 560.6KB | +552.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net481 | 8.5KB | 560.6KB | +552.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 528.7KB | +519.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 497.0KB | +488.0KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 497.0KB | +488.0KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 488.3KB | +478.8KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| netcoreapp3.1 | 9.5KB | 486.8KB | +477.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net5.0 | 9.5KB | 434.9KB | +425.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net6.0 | 10.0KB | 355.7KB | +345.7KB | +17.2KB | +8.7KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 307.4KB | +297.4KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | | net8.0 | 9.5KB | 264.1KB | +254.6KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | | net9.0 | 9.5KB | 169.1KB | +159.6KB | +15.5KB | | +1.1KB | +3.7KB | | net10.0 | 10.0KB | 136.1KB | +126.1KB | +16.0KB | | +1.1KB | +3.7KB | diff --git a/readme.md b/readme.md index 247da631..bde9498b 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: 1161** +**API count: 1162** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 1069 | -| `net462` | 1069 | -| `net47` | 1068 | -| `net471` | 1067 | -| `net472` | 1063 | -| `net48` | 1063 | -| `net481` | 1063 | -| `netstandard2.0` | 1065 | -| `netstandard2.1` | 898 | -| `netcoreapp2.0` | 988 | -| `netcoreapp2.1` | 909 | -| `netcoreapp2.2` | 909 | -| `netcoreapp3.0` | 870 | -| `netcoreapp3.1` | 869 | -| `net5.0` | 747 | -| `net6.0` | 643 | -| `net7.0` | 502 | -| `net8.0` | 381 | +| `net461` | 1070 | +| `net462` | 1070 | +| `net47` | 1069 | +| `net471` | 1068 | +| `net472` | 1064 | +| `net48` | 1064 | +| `net481` | 1064 | +| `netstandard2.0` | 1066 | +| `netstandard2.1` | 899 | +| `netcoreapp2.0` | 989 | +| `netcoreapp2.1` | 910 | +| `netcoreapp2.2` | 910 | +| `netcoreapp3.0` | 871 | +| `netcoreapp3.1` | 870 | +| `net5.0` | 748 | +| `net6.0` | 644 | +| `net7.0` | 503 | +| `net8.0` | 382 | | `net9.0` | 243 | | `net10.0` | 182 | | `net11.0` | 58 | -| `uap10.0` | 1055 | +| `uap10.0` | 1056 | @@ -97,21 +97,21 @@ This project uses features from the newest stable SDK and C# language. As such c | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 385.5KB | +377.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netstandard2.1 | 8.5KB | 341.5KB | +333.0KB | +9.0KB | +6.5KB | +8.5KB | +13.5KB | -| net461 | 8.5KB | 384.0KB | +375.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 388.0KB | +381.0KB | +8.5KB | +6.0KB | +8.5KB | +13.0KB | +| netstandard2.1 | 8.5KB | 341.5KB | +333.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 384.5KB | +376.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net462 | 7.0KB | 388.0KB | +381.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | | net47 | 7.0KB | 387.5KB | +380.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net471 | 8.5KB | 386.5KB | +378.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 385.5KB | +377.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| net48 | 8.5KB | 385.5KB | +377.0KB | +8.5KB | +6.5KB | +9.0KB | +13.0KB | -| net481 | 8.5KB | 385.5KB | +377.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.0 | 9.0KB | 363.5KB | +354.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 344.0KB | +335.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.2 | 9.0KB | 344.0KB | +335.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.0 | 9.5KB | 342.5KB | +333.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.1 | 9.5KB | 341.0KB | +331.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| net5.0 | 9.5KB | 306.5KB | +297.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net6.0 | 10.0KB | 247.5KB | +237.5KB | +9.5KB | +6.5KB | +512bytes | +3.0KB | +| net471 | 8.5KB | 386.5KB | +378.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net472 | 8.5KB | 385.5KB | +377.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 385.5KB | +377.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net481 | 8.5KB | 385.5KB | +377.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 363.5KB | +354.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 344.5KB | +335.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 344.5KB | +335.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 342.5KB | +333.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| netcoreapp3.1 | 9.5KB | 341.0KB | +331.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net5.0 | 9.5KB | 306.5KB | +297.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net6.0 | 10.0KB | 247.5KB | +237.5KB | +9.5KB | +7.0KB | +512bytes | +3.5KB | | net7.0 | 10.0KB | 216.0KB | +206.0KB | +9.5KB | +6.0KB | +512bytes | +3.5KB | | net8.0 | 9.5KB | 186.5KB | +177.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB | | net9.0 | 9.5KB | 117.0KB | +107.5KB | +8.0KB | | +512bytes | +3.0KB | @@ -124,22 +124,22 @@ This project uses features from the newest stable SDK and C# language. As such c | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 561.3KB | +553.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netstandard2.1 | 8.5KB | 490.3KB | +481.8KB | +16.7KB | +8.2KB | +13.4KB | +18.9KB | -| net461 | 8.5KB | 560.8KB | +552.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 564.8KB | +557.8KB | +16.2KB | +7.7KB | +13.4KB | +18.4KB | -| net47 | 7.0KB | 564.0KB | +557.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net471 | 8.5KB | 562.7KB | +554.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 560.6KB | +552.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| net48 | 8.5KB | 560.6KB | +552.1KB | +16.2KB | +8.2KB | +13.9KB | +18.4KB | -| net481 | 8.5KB | 560.6KB | +552.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.0 | 9.0KB | 528.7KB | +519.7KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 496.5KB | +487.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.2 | 9.0KB | 496.5KB | +487.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.0 | 9.5KB | 488.3KB | +478.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.1 | 9.5KB | 486.8KB | +477.3KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| net5.0 | 9.5KB | 434.8KB | +425.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net6.0 | 10.0KB | 355.7KB | +345.7KB | +17.2KB | +8.2KB | +1.1KB | +3.7KB | -| net7.0 | 10.0KB | 307.3KB | +297.3KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | +| netstandard2.1 | 8.5KB | 490.3KB | +481.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 561.3KB | +552.8KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net462 | 7.0KB | 564.8KB | +557.8KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net47 | 7.0KB | 564.1KB | +557.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net471 | 8.5KB | 562.7KB | +554.2KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net472 | 8.5KB | 560.6KB | +552.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 560.6KB | +552.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net481 | 8.5KB | 560.6KB | +552.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 528.7KB | +519.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 497.0KB | +488.0KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 497.0KB | +488.0KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 488.3KB | +478.8KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| netcoreapp3.1 | 9.5KB | 486.8KB | +477.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net5.0 | 9.5KB | 434.9KB | +425.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net6.0 | 10.0KB | 355.7KB | +345.7KB | +17.2KB | +8.7KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 307.4KB | +297.4KB | +17.1KB | +7.4KB | +1.1KB | +4.2KB | | net8.0 | 9.5KB | 264.1KB | +254.6KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | | net9.0 | 9.5KB | 169.1KB | +159.6KB | +15.5KB | | +1.1KB | +3.7KB | | net10.0 | 10.0KB | 136.1KB | +126.1KB | +16.0KB | | +1.1KB | +3.7KB | @@ -1079,8 +1079,11 @@ The class `Polyfill` includes the following extension methods: * `Task ReadAllTextAsync(string, Encoding, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readalltextasync?view=net-11.0#system-io-file-readalltextasync(system-string-system-text-encoding-system-threading-cancellationtoken)) * `IAsyncEnumerable ReadLinesAsync(string, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readlinesasync?view=net-11.0#system-io-file-readalllinesasync(system-string-system-threading-cancellationtoken)) * `IAsyncEnumerable ReadLinesAsync(string, Encoding, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.readlinesasync?view=net-11.0#system-io-file-readalllinesasync(system-string-system-text-encoding-system-threading-cancellationtoken)) + * `void WriteAllBytes(string, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytes?view=net-11.0#system-io-file-writeallbytes(system-string-system-readonlyspan((system-byte)))) + * Note: Copies the span to an array and uses the array overload, so this allocates where the BCL writes straight from the span. + * Note: Only reached when the argument is already a ReadOnlySpan. A byte array binds to the BCL array overload, exactly as it does without Polyfill. * `Task WriteAllBytesAsync(string, byte[], CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytesasync?view=net-11.0#system-io-file-writeallbytesasync(system-string-system-byte()-system-threading-cancellationtoken)) - * `Task WriteAllBytesAsync(string, ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.appendallbytesasync?view=net-11.0#system-io-file-appendallbytesasync(system-string-system-readonlymemory((system-byte))-system-threading-cancellationtoken)) + * `Task WriteAllBytesAsync(string, ReadOnlyMemory, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytesasync?view=net-11.0#system-io-file-writeallbytesasync(system-string-system-readonlymemory((system-byte))-system-threading-cancellationtoken)) * `Task WriteAllLinesAsync(string, IEnumerable, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writealllinesasync?view=net-11.0#system-io-file-writealllinesasync(system-string-system-collections-generic-ienumerable((system-string))-system-threading-cancellationtoken)) * `Task WriteAllLinesAsync(string, IEnumerable, Encoding, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writealllinesasync?view=net-11.0#system-io-file-writealllinesasync(system-string-system-collections-generic-ienumerable((system-string))-system-text-encoding-system-threading-cancellationtoken)) * `void WriteAllText(string, ReadOnlySpan, Encoding)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writealltext?view=net-11.0#system-io-file-writealltext(system-string-system-readonlyspan((system-char))-system-text-encoding)) diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index be619f68..202fdef0 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -1192,6 +1192,11 @@ void File_Methods() var sourceContent = "Test content"; File.WriteAllText(TestFilePath, sourceContent); +#if FeatureMemory + ReadOnlySpan byteSpan = new byte[] { 1, 2, 3 }; + File.WriteAllBytes(TestFilePath, byteSpan); +#endif + var fileMode = File.GetUnixFileMode(TestFilePath); // Use the | bitwise OR operator to combine multiple file modes diff --git a/src/Polyfill/FilePolyfill.cs b/src/Polyfill/FilePolyfill.cs index 4ae35518..4446d404 100644 --- a/src/Polyfill/FilePolyfill.cs +++ b/src/Polyfill/FilePolyfill.cs @@ -83,13 +83,23 @@ public static void AppendAllBytes(string path, ReadOnlySpan bytes) /// /// Asynchronously creates a new file, writes the specified byte array to the file, and then closes the file. If the target file already exists, it is truncated and overwritten. /// - //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.file.appendallbytesasync?view=net-11.0#system-io-file-appendallbytesasync(system-string-system-readonlymemory((system-byte))-system-threading-cancellationtoken) + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytesasync?view=net-11.0#system-io-file-writeallbytesasync(system-string-system-readonlymemory((system-byte))-system-threading-cancellationtoken) public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) { using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } + /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.file.writeallbytes?view=net-11.0#system-io-file-writeallbytes(system-string-system-readonlyspan((system-byte))) + //Note: Copies the span to an array and uses the array overload, so this allocates where the BCL writes straight from the span. + //Note: Only reached when the argument is already a ReadOnlySpan. A byte array binds to the BCL array overload, exactly as it does without Polyfill. + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. diff --git a/src/Split/net461/FilePolyfill.cs b/src/Split/net461/FilePolyfill.cs index c6112e74..e90c5b16 100644 --- a/src/Split/net461/FilePolyfill.cs +++ b/src/Split/net461/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net462/FilePolyfill.cs b/src/Split/net462/FilePolyfill.cs index c6112e74..e90c5b16 100644 --- a/src/Split/net462/FilePolyfill.cs +++ b/src/Split/net462/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net47/FilePolyfill.cs b/src/Split/net47/FilePolyfill.cs index c6112e74..e90c5b16 100644 --- a/src/Split/net47/FilePolyfill.cs +++ b/src/Split/net47/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net471/FilePolyfill.cs b/src/Split/net471/FilePolyfill.cs index c6112e74..e90c5b16 100644 --- a/src/Split/net471/FilePolyfill.cs +++ b/src/Split/net471/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net472/FilePolyfill.cs b/src/Split/net472/FilePolyfill.cs index c6112e74..e90c5b16 100644 --- a/src/Split/net472/FilePolyfill.cs +++ b/src/Split/net472/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net48/FilePolyfill.cs b/src/Split/net48/FilePolyfill.cs index c6112e74..e90c5b16 100644 --- a/src/Split/net48/FilePolyfill.cs +++ b/src/Split/net48/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net481/FilePolyfill.cs b/src/Split/net481/FilePolyfill.cs index c6112e74..e90c5b16 100644 --- a/src/Split/net481/FilePolyfill.cs +++ b/src/Split/net481/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net5.0/FilePolyfill.cs b/src/Split/net5.0/FilePolyfill.cs index ff68c4d3..609e0b7f 100644 --- a/src/Split/net5.0/FilePolyfill.cs +++ b/src/Split/net5.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net6.0/FilePolyfill.cs b/src/Split/net6.0/FilePolyfill.cs index 87911de4..f6cd267c 100644 --- a/src/Split/net6.0/FilePolyfill.cs +++ b/src/Split/net6.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net7.0/FilePolyfill.cs b/src/Split/net7.0/FilePolyfill.cs index cd559610..02532880 100644 --- a/src/Split/net7.0/FilePolyfill.cs +++ b/src/Split/net7.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/net8.0/FilePolyfill.cs b/src/Split/net8.0/FilePolyfill.cs index cd559610..02532880 100644 --- a/src/Split/net8.0/FilePolyfill.cs +++ b/src/Split/net8.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/netcoreapp2.0/FilePolyfill.cs b/src/Split/netcoreapp2.0/FilePolyfill.cs index 3e55baa2..1fe06419 100644 --- a/src/Split/netcoreapp2.0/FilePolyfill.cs +++ b/src/Split/netcoreapp2.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/netcoreapp2.1/FilePolyfill.cs b/src/Split/netcoreapp2.1/FilePolyfill.cs index 3e55baa2..1fe06419 100644 --- a/src/Split/netcoreapp2.1/FilePolyfill.cs +++ b/src/Split/netcoreapp2.1/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/netcoreapp2.2/FilePolyfill.cs b/src/Split/netcoreapp2.2/FilePolyfill.cs index 3e55baa2..1fe06419 100644 --- a/src/Split/netcoreapp2.2/FilePolyfill.cs +++ b/src/Split/netcoreapp2.2/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/netcoreapp3.0/FilePolyfill.cs b/src/Split/netcoreapp3.0/FilePolyfill.cs index ff68c4d3..609e0b7f 100644 --- a/src/Split/netcoreapp3.0/FilePolyfill.cs +++ b/src/Split/netcoreapp3.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/netcoreapp3.1/FilePolyfill.cs b/src/Split/netcoreapp3.1/FilePolyfill.cs index ff68c4d3..609e0b7f 100644 --- a/src/Split/netcoreapp3.1/FilePolyfill.cs +++ b/src/Split/netcoreapp3.1/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/netstandard2.0/FilePolyfill.cs b/src/Split/netstandard2.0/FilePolyfill.cs index 6dda0398..68eb9a13 100644 --- a/src/Split/netstandard2.0/FilePolyfill.cs +++ b/src/Split/netstandard2.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/netstandard2.1/FilePolyfill.cs b/src/Split/netstandard2.1/FilePolyfill.cs index 3e55baa2..1fe06419 100644 --- a/src/Split/netstandard2.1/FilePolyfill.cs +++ b/src/Split/netstandard2.1/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Split/uap10.0/FilePolyfill.cs b/src/Split/uap10.0/FilePolyfill.cs index 6dda0398..68eb9a13 100644 --- a/src/Split/uap10.0/FilePolyfill.cs +++ b/src/Split/uap10.0/FilePolyfill.cs @@ -73,6 +73,12 @@ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory by await stream.WriteAsync(bytes.ToArray(), 0, bytes.Length, cancellationToken); } /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + public static void WriteAllBytes(string path, ReadOnlySpan bytes) => + File.WriteAllBytes(path, bytes.ToArray()); + /// /// Creates a new file, writes the specified string to the file, and then closes the file. /// If the target file already exists, it is truncated and overwritten. /// diff --git a/src/Tests/FileWriteAllBytesSpanTests.cs b/src/Tests/FileWriteAllBytesSpanTests.cs new file mode 100644 index 00000000..2b766df6 --- /dev/null +++ b/src/Tests/FileWriteAllBytesSpanTests.cs @@ -0,0 +1,125 @@ +#if FeatureMemory + +using System.IO; + +// Runs on every target, so net9.0 and later validate the BCL and everything below the polyfill. +// The byte array overload is the oracle: it exists unchanged on every framework and writes the +// bytes this span overload exists to write without allocating. +public class FileWriteAllBytesSpanTests +{ + [Test] + public async Task MatchesTheArrayOverload() + { + using var directory = new TempDirectory(); + + byte[][] payloads = [[], [1], [1, 2, 3], new byte[100000]]; + foreach (var payload in payloads) + { + var viaSpan = directory.File("span.bin"); + var viaArray = directory.File("array.bin"); + + WriteSpan(viaSpan, payload); + File.WriteAllBytes(viaArray, payload); + + await Assert.That(File.ReadAllBytes(viaSpan)).IsEquivalentTo(File.ReadAllBytes(viaArray)); + } + } + + [Test] + public async Task TruncatesAnExistingFile() + { + using var directory = new TempDirectory(); + var path = directory.File("truncate.bin"); + + File.WriteAllBytes(path, new byte[50]); + WriteSpan(path, [1, 2, 3]); + await Assert.That(new FileInfo(path).Length).IsEqualTo(3); + + WriteSpan(path, []); + await Assert.That(new FileInfo(path).Length).IsEqualTo(0); + await Assert.That(File.Exists(path)).IsTrue(); + } + + [Test] + public async Task CreatesAMissingFile() + { + using var directory = new TempDirectory(); + var path = directory.File("fresh.bin"); + + WriteSpan(path, [9]); + + await Assert.That(File.Exists(path)).IsTrue(); + await Assert.That(File.ReadAllBytes(path)).IsEquivalentTo(new byte[] { 9 }); + } + + [Test] + public async Task ArgumentValidationMatchesTheArrayOverload() + { + using var directory = new TempDirectory(); + var missingDirectory = Path.Combine(directory.Path, "nope", "x.bin"); + + await Assert.That(CaptureSpan(null!)).IsEqualTo(CaptureArray(null!)); + await Assert.That(CaptureSpan("")).IsEqualTo(CaptureArray("")); + await Assert.That(CaptureSpan(" ")).IsEqualTo(CaptureArray(" ")); + await Assert.That(CaptureSpan(missingDirectory)).IsEqualTo(CaptureArray(missingDirectory)); + + // and the concrete shapes, so a change in either overload is visible. Only the ones that + // are the same everywhere are pinned with their paramName: .NET Framework throws + // ArgumentException with no paramName for an empty path where .NET names it, and the + // polyfill inherits whichever the running framework does. + await Assert.That(CaptureSpan(null!)).IsEqualTo("ArgumentNullException:path"); + await Assert.That(CaptureSpan(missingDirectory)).IsEqualTo("DirectoryNotFoundException:"); + await Assert.That(TypeOfSpanFailure("")).IsEqualTo("ArgumentException"); + await Assert.That(TypeOfSpanFailure(" ")).IsEqualTo("ArgumentException"); + } + + // the span stays inside these helpers, so nothing ref struct shaped lives across an await + static void WriteSpan(string path, byte[] bytes) => + File.WriteAllBytes(path, (ReadOnlySpan) bytes); + + static string CaptureSpan(string path) => + Capture(() => File.WriteAllBytes(path, (ReadOnlySpan) new byte[] { 1 })); + + static string TypeOfSpanFailure(string path) => + CaptureSpan(path).Split(':')[0]; + + static string CaptureArray(string path) => + Capture(() => File.WriteAllBytes(path, new byte[] { 1 })); + + static string Capture(Action action) + { + try + { + action(); + return "none"; + } + catch (Exception exception) + { + return $"{exception.GetType().Name}:{(exception as ArgumentException)?.ParamName}"; + } + } + + class TempDirectory : IDisposable + { + public string Path { get; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "polyfill-" + Guid.NewGuid().ToString("N")); + + public TempDirectory() => + Directory.CreateDirectory(Path); + + public string File(string name) => + System.IO.Path.Combine(Path, name); + + public void Dispose() + { + try + { + Directory.Delete(Path, true); + } + catch + { + } + } + } +} + +#endif