Skip to content
17 changes: 13 additions & 4 deletions PolyShim/Net100/Random.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;

Expand All @@ -17,11 +18,19 @@ internal static class MemberPolyfills_Net100_Random
// https://learn.microsoft.com/dotnet/api/system.random.gethexstring#system-random-gethexstring(system-int32-system-boolean)
public string GetHexString(int stringLength, bool lowercase = false)
{
var bytes = new byte[(stringLength + 1) / 2];
random.NextBytes(bytes);
var byteCount = (stringLength + 1) / 2;

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.

This can be inlined?

var bytes = ArrayPool<byte>.Shared.Rent(byteCount);
try
{
random.NextBytes(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);
}
finally
{
ArrayPool<byte>.Shared.Return(bytes);
}
}

// https://learn.microsoft.com/dotnet/api/system.random.getstring
Expand Down
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 c = ArrayPool<char>.Shared.Rent(length * 2);

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.

Let's rename this to var chars

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 a132bff - renamed c to chars

try
{
for (var i = 0; i < length; i++)
{
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));
}

for (var i = 0; i < length; i++)
return new string(c, 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(c);
}

return new string(c);
}

// https://learn.microsoft.com/dotnet/api/system.convert.tohexstring#system-convert-tohexstring(system-byte())
Expand Down
37 changes: 26 additions & 11 deletions PolyShim/Net60/Random.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;

file static class RandomEx
Expand All @@ -32,15 +33,22 @@ public long NextInt64(long minValue, long maxValue)

var range = (ulong)(maxValue - minValue);

ulong ulongRand;
do
var buffer = ArrayPool<byte>.Shared.Rent(8);
try
{
var buffer = new byte[8];
random.NextBytes(buffer);
ulongRand = BitConverter.ToUInt64(buffer, 0);
} while (ulongRand > ulong.MaxValue - (ulong.MaxValue % range + 1) % range);
ulong ulongRand;
do
{
random.NextBytes(buffer);
ulongRand = BitConverter.ToUInt64(buffer, 0);
} while (ulongRand > ulong.MaxValue - (ulong.MaxValue % range + 1) % range);

return (long)(ulongRand % range) + minValue;
return (long)(ulongRand % range) + minValue;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

// https://learn.microsoft.com/dotnet/api/system.random.nextint64#system-random-nextint64(system-int64)
Expand All @@ -52,11 +60,18 @@ public long NextInt64(long minValue, long maxValue)
// https://learn.microsoft.com/dotnet/api/system.random.nextsingle
public float NextSingle()
{
var buffer = new byte[4];
random.NextBytes(buffer);
var uintValue = BitConverter.ToUInt32(buffer, 0);
var buffer = ArrayPool<byte>.Shared.Rent(4);
try
{
random.NextBytes(buffer);
var uintValue = BitConverter.ToUInt32(buffer, 0);

return (uintValue >> 8) * (1.0f / (1u << 24));
return (uintValue >> 8) * (1.0f / (1u << 24));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

// https://learn.microsoft.com/dotnet/api/system.random.shared
Expand Down
29 changes: 18 additions & 11 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,21 +31,27 @@ internal static class MemberPolyfills_Net70_TextReader
public async Task<string> ReadToEndAsync(CancellationToken cancellationToken)
{
var result = new StringBuilder();
var buffer = new char[4096];

while (true)
var buffer = ArrayPool<char>.Shared.Rent(4096);

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.

Maybe can use MemoryPool here, no? And in other places where async methods that support Memory are 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 a132bff - switched to MemoryPool for cleaner disposal pattern. Note: StringBuilder doesn't support Span/Memory append in older frameworks, so we convert the Memory slice to string (which allocates, but is necessary for compatibility).

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

if (charsRead <= 0)
break;
if (charsRead <= 0)
break;

result.Append(buffer, 0, charsRead);
}
result.Append(buffer, 0, charsRead);
}

return result.ToString();
return result.ToString();
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
#endif
}
Expand Down
19 changes: 15 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,21 @@ 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 byteCount = (stringLength + 1) / 2;

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.

Can be inlined?

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 a132bff - inlined byteCount calculation

var bytes = ArrayPool<byte>.Shared.Rent(byteCount);
try
{
RandomNumberGenerator.Fill(bytes.AsSpan(0, byteCount));

var hex = lowercase ? Convert.ToHexStringLower(bytes) : Convert.ToHexString(bytes);
return hex.Substring(0, stringLength);
var hex = lowercase
? Convert.ToHexStringLower(bytes, 0, byteCount)
: Convert.ToHexString(bytes, 0, byteCount);
return hex.Substring(0, stringLength);
}
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, bufferSize);

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.

Read up to buffer.Length

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 d57680f - changed to read up to buffer.Length

if (bytesRead <= 0)
break;

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

Expand Down
45 changes: 29 additions & 16 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 @@ -134,18 +135,24 @@ public static async Task<string> ReadAllTextAsync(string path, CancellationToken
using var reader = new StreamReader(stream);

var content = new StringBuilder();
var buffer = new char[4096];
var buffer = ArrayPool<char>.Shared.Rent(4096);
try
{
cancellationToken.ThrowIfCancellationRequested();

cancellationToken.ThrowIfCancellationRequested();
int charsRead;
while ((charsRead = await reader.ReadAsync(buffer, 0, 4096).ConfigureAwait(false)) > 0)

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.

Can read into entire buffer, it's okay if there's more bytes

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 a132bff - changed to read into full buffer (buffer.Length) instead of limiting to bufferSize

{
cancellationToken.ThrowIfCancellationRequested();
content.Append(buffer, 0, charsRead);
}

int charsRead;
while ((charsRead = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
return content.ToString();
}
finally
{
cancellationToken.ThrowIfCancellationRequested();
content.Append(buffer, 0, charsRead);
ArrayPool<char>.Shared.Return(buffer);
}

return content.ToString();
}

// https://learn.microsoft.com/dotnet/api/system.io.file.readalltextasync#system-io-file-readalltextasync(system-string-system-text-encoding-system-threading-cancellationtoken)
Expand All @@ -155,18 +162,24 @@ public static async Task<string> ReadAllTextAsync(string path, Encoding encoding
using var reader = new StreamReader(stream, encoding);

var content = new StringBuilder();
var buffer = new char[4096];
var buffer = ArrayPool<char>.Shared.Rent(4096);
try
{
cancellationToken.ThrowIfCancellationRequested();

cancellationToken.ThrowIfCancellationRequested();
int charsRead;
while ((charsRead = await reader.ReadAsync(buffer, 0, 4096).ConfigureAwait(false)) > 0)

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.

Same here, it's okay if we read more bytes. Apply this logic in other relevant places

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 a132bff - applied the same change to both ReadAllTextAsync overloads

{
cancellationToken.ThrowIfCancellationRequested();
content.Append(buffer, 0, charsRead);
}

int charsRead;
while ((charsRead = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
return content.ToString();
}
finally
{
cancellationToken.ThrowIfCancellationRequested();
content.Append(buffer, 0, charsRead);
ArrayPool<char>.Shared.Return(buffer);
}

return content.ToString();
}

// https://learn.microsoft.com/dotnet/api/system.io.file.writeallbytesasync#system-io-file-writeallbytesasync(system-string-system-byte()-system-threading-cancellationtoken)
Expand Down
14 changes: 11 additions & 3 deletions PolyShim/NetCore20/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.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;

Expand All @@ -20,9 +21,16 @@ public void GetBytes(byte[] data, int offset, int count)
if (count == 0)
return;

var buffer = new byte[count];
rng.GetBytes(buffer);
Array.Copy(buffer, 0, data, offset, count);
var buffer = ArrayPool<byte>.Shared.Rent(count);
try
{
rng.GetBytes(buffer);
Array.Copy(buffer, 0, data, offset, count);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

// https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte())
Expand Down
14 changes: 11 additions & 3 deletions PolyShim/NetCore21/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.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;

Expand All @@ -20,9 +21,16 @@ public void GetBytes(Span<byte> data)
if (data.Length == 0)
return;

var buffer = new byte[data.Length];
rng.GetBytes(buffer);
buffer.CopyTo(data);
var buffer = ArrayPool<byte>.Shared.Rent(data.Length);
try
{
rng.GetBytes(buffer);
buffer.AsSpan(0, data.Length).CopyTo(data);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

// https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte)))
Expand Down
Loading