Skip to content
23 changes: 15 additions & 8 deletions PolyShim/Net50/Convert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// ReSharper disable PartialTypeWithSinglePart

using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;

[ExcludeFromCodeCoverage]
Expand Down Expand Up @@ -48,17 +49,23 @@ static int GetHexValue(char c)
// https://learn.microsoft.com/dotnet/api/system.convert.tohexstring#system-convert-tohexstring(system-byte()-system-int32-system-int32)
public static string ToHexString(byte[] value, int startIndex, int length)
{
var c = new char[length * 2];
var chars = ArrayPool<char>.Shared.Rent(length * 2);
try
{
for (var i = 0; i < length; i++)
{
var b = value[startIndex + i] >> 4;
chars[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7));
b = value[startIndex + i] & 0xF;
chars[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7));
}

for (var i = 0; i < length; i++)
return new string(chars, 0, length * 2);
}
finally
{
var b = value[startIndex + i] >> 4;
c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7));
b = value[startIndex + i] & 0xF;
c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7));
ArrayPool<char>.Shared.Return(chars);
}

return new string(c);
}

// https://learn.microsoft.com/dotnet/api/system.convert.tohexstring#system-convert-tohexstring(system-byte())
Expand Down
12 changes: 9 additions & 3 deletions PolyShim/Net70/TextReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// ReSharper disable PartialTypeWithSinglePart

using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading;
Expand All @@ -30,18 +31,23 @@ internal static class MemberPolyfills_Net70_TextReader
public async Task<string> ReadToEndAsync(CancellationToken cancellationToken)
{
var result = new StringBuilder();
var buffer = new char[4096];
using var buffer = MemoryPool<char>.Shared.Rent(4096);

while (true)
{
var charsRead = await reader
.ReadAsync(buffer, cancellationToken)
.ReadAsync(buffer.Memory, cancellationToken)
.ConfigureAwait(false);

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using TextReader.ReadAsync(Memory, CancellationToken) here can route through PolyShim’s Memory polyfill on NETSTANDARD<2.1/NETCOREAPP<2.1, which allocates a new char[] (buffer.ToArray()) per call. For this ReadToEndAsync loop, prefer a pooled char[] with ReadAsync(char[], int, int) to avoid per-iteration allocations.

Copilot uses AI. Check for mistakes.

if (charsRead <= 0)
break;

result.Append(buffer, 0, charsRead);
// Append char by char to avoid string allocation
var span = buffer.Memory.Span;
for (var i = 0; i < charsRead; i++)
{
result.Append(span[i]);
}
}

return result.ToString();
Expand Down
16 changes: 12 additions & 4 deletions PolyShim/Net80/RandomNumberGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// ReSharper disable PartialTypeWithSinglePart

using System;
using System.Buffers;
using System.Text;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
Expand Down Expand Up @@ -46,11 +47,18 @@ public static void Shuffle<T>(Span<T> items)
// https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean)
public static string GetHexString(int stringLength, bool lowercase = false)
{
var bytes = new byte[(stringLength + 1) / 2];
RandomNumberGenerator.Fill(bytes);
var bytes = ArrayPool<byte>.Shared.Rent((stringLength + 1) / 2);
try
{
RandomNumberGenerator.Fill(bytes);

var hex = lowercase ? Convert.ToHexStringLower(bytes) : Convert.ToHexString(bytes);
return hex.Substring(0, stringLength);
var hex = lowercase ? Convert.ToHexStringLower(bytes) : Convert.ToHexString(bytes);
return hex.Substring(0, stringLength);

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayPool.Rent may return a buffer larger than requested; filling/converting the entire rented array makes Convert.ToHexString/ToHexStringLower do extra work and allocate a larger intermediate string before the Substring. Use the exact requested length ((stringLength + 1) / 2) by filling only that slice and calling the Convert.ToHexString overload that takes (byte[], startIndex, length).

Copilot uses AI. Check for mistakes.
}
finally
{
ArrayPool<byte>.Shared.Return(bytes);

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider returning the rented byte[] with clearArray: true. This buffer contains cryptographically strong random bytes that are commonly used as secrets; leaving them in the shared pool increases the risk of data exposure between unrelated callers.

Suggested change
ArrayPool<byte>.Shared.Return(bytes);
ArrayPool<byte>.Shared.Return(bytes, clearArray: true);

Copilot uses AI. Check for mistakes.
}
}

// https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring
Expand Down
20 changes: 14 additions & 6 deletions PolyShim/NetCore10/Stream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// ReSharper disable InconsistentNaming
// ReSharper disable PartialTypeWithSinglePart

using System.Buffers;
using System.IO;
using System.Diagnostics.CodeAnalysis;

Expand All @@ -16,14 +17,21 @@ internal static class MemberPolyfills_NetCore10_Stream
// https://learn.microsoft.com/dotnet/api/system.io.stream.copyto#system-io-stream-copyto(system-io-stream-system-int32)
public void CopyTo(Stream destination, int bufferSize)
{
var buffer = new byte[bufferSize];
while (true)
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
var bytesRead = source.Read(buffer, 0, buffer.Length);
if (bytesRead <= 0)
break;
while (true)
{
var bytesRead = source.Read(buffer, 0, buffer.Length);
if (bytesRead <= 0)
break;

destination.Write(buffer, 0, bytesRead);
destination.Write(buffer, 0, bytesRead);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

Expand Down
31 changes: 23 additions & 8 deletions PolyShim/NetCore20/File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// ReSharper disable InconsistentNaming
// ReSharper disable PartialTypeWithSinglePart

using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Text;
Expand Down Expand Up @@ -130,19 +131,26 @@ public static async Task<string[]> ReadAllLinesAsync(string path, Encoding encod
// https://learn.microsoft.com/dotnet/api/system.io.file.readalltextasync#system-io-file-readalltextasync(system-string-system-threading-cancellationtoken)
public static async Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
const int bufferSize = 4096;
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, true);
using var reader = new StreamReader(stream);

var content = new StringBuilder();
var buffer = new char[4096];
using var buffer = MemoryPool<char>.Shared.Rent(bufferSize);

cancellationToken.ThrowIfCancellationRequested();

int charsRead;
while ((charsRead = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
while ((charsRead = await reader.ReadAsync(buffer.Memory, cancellationToken).ConfigureAwait(false)) > 0)
{

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this NetCore20 polyfill, calling StreamReader.ReadAsync(Memory, CancellationToken) will resolve to PolyShim’s TextReader.ReadAsync(Memory, ...) polyfill on NETSTANDARD<2.1/NETCOREAPP<2.1, which allocates a new char[] via buffer.ToArray() on every loop iteration (see PolyShim/NetCore21/TextReader.cs). That defeats the goal of reducing allocations; consider using ArrayPool (or a rented char[] via MemoryMarshal.TryGetArray) and the array-based ReadAsync(char[], int, int) overload instead.

Copilot uses AI. Check for mistakes.
cancellationToken.ThrowIfCancellationRequested();
content.Append(buffer, 0, charsRead);

// Append char by char to avoid string allocation
var span = buffer.Memory.Span;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline this (and apply to other places where this pattern is used)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 65c8a88 - inlined buffer.Memory.Span access directly in the for loop, applied to all 3 locations (File.cs ReadAllTextAsync both overloads, and TextReader.cs ReadToEndAsync)

for (var i = 0; i < charsRead; i++)
{
content.Append(span[i]);
}
}

return content.ToString();
Expand All @@ -151,19 +159,26 @@ public static async Task<string> ReadAllTextAsync(string path, CancellationToken
// https://learn.microsoft.com/dotnet/api/system.io.file.readalltextasync#system-io-file-readalltextasync(system-string-system-text-encoding-system-threading-cancellationtoken)
public static async Task<string> ReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken = default)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
const int bufferSize = 4096;
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, true);
using var reader = new StreamReader(stream, encoding);

var content = new StringBuilder();
var buffer = new char[4096];
using var buffer = MemoryPool<char>.Shared.Rent(bufferSize);

cancellationToken.ThrowIfCancellationRequested();

int charsRead;
while ((charsRead = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
while ((charsRead = await reader.ReadAsync(buffer.Memory, cancellationToken).ConfigureAwait(false)) > 0)
{

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue in the encoding overload: StreamReader.ReadAsync(Memory, CancellationToken) on older TFMs routes through the PolyShim TextReader.ReadAsync(Memory, ...) polyfill, which allocates via buffer.ToArray() each call. To actually reduce allocations, prefer a pooled char[] + ReadAsync(char[], int, int) here.

Copilot uses AI. Check for mistakes.
cancellationToken.ThrowIfCancellationRequested();
content.Append(buffer, 0, charsRead);

// Append char by char to avoid string allocation
var span = buffer.Memory.Span;
for (var i = 0; i < charsRead; i++)
{
content.Append(span[i]);
}
}

return content.ToString();
Expand Down