The main project that produces the nuget.
A NUnit test project that verifies all the APIs.
Some features of Polyfill require nuget references to be enabled. The NoRefsTests project had none of those refecences and tests the subset of features that do not require references.
Polyfill supports making all APIs public. The PublicTests project tests that scenario.
Some feature of Polyfill leverage unsafe code for better performance. For example Append(this StringBuilder, ReadOnlySpan<char>)
. The UnsafeTests project tests this scenario vie enabling <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
.
Polyfill supports back to net461
and netcoreapp2.0
. However NUnit only support back to net462
and netcoreapp3.1
. The Consume project targets all frameworks that Polyfill supports, and consumes all APIs to ensure that they all compile on those frameworks
Test the scenario when references are added through <Reference
instead of <PackageReference
.
An API is a valid candidate to be polyfilled if it exists in the current stable version of .net or is planned for a future version .net
APIs that require a reference to a bridging nuget (similar to System.ValueTuple or System.Memory) will only be accepted if, in a future version of .net that nuget is not required.
If a new API is valid, dont bother raising a GitHub issue to ask about it. Instead submit a Pull Request that adds that API. Any discussion can happen in the PR comments.
The code for the API should be wrapped in conditional compilation statements. For example:
#if NETFRAMEWORK || NETSTANDARD || NETCOREAPP2X
The following additional compilation constants are provided:
NETCOREAPPX
: indicates if netcore is being targeted.NETCOREAPP2X
: indicates if any major or minor version of netcore 2 is being targeted.NETCOREAPP3X
: indicates if any major or minor version of netcore 3 is being targeted.NET46X
: indicates if any major or minor version of NET46 is being targeted.NET47X
: indicates if any major or minor version of NET47 is being targeted.NET48X
: indicates if any major or minor version of NET48 is being targeted.MEMORYREFERENCED
: indicates if System.Memory) is referenced.TASKSEXTENSIONSREFERENCED
: indicates if System.Threading.Tasks.Extensions) is referenced.VALUETUPLEREFERENCED
: indicates if System.ValueTuple) is referenced.
Warnings must be disabled with a pragma.
#pragma warning disable
This is required to prevent custom code formatting rule in consuming projects from giving false warnings
Any potential ReSharper or Rider code formatting issues should be disabled. For example:
// ReSharper disable RedundantUsingDirective
// ReSharper disable UnusedMember.Global
Having Implicit usings enabled is optional for the consuming project. So ensure all using statements are included.
Polyfill supports making all APIs public. This is done by making types public if PolyPublic
. For example:
#if PolyPublic
public
#endif
sealed class ...
The XML API comments should match the actual API.
Add a new class containing the Attribute
Example:
// <auto-generated />
#pragma warning disable
#if !NET5_0_OR_GREATER
namespace System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
/// <summary>
/// Used to indicate to the compiler that a method should be called
/// in its containing module's initializer.
/// </summary>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute
[ExcludeFromCodeCoverage]
[DebuggerNonUserCode]
[AttributeUsage(
validOn: AttributeTargets.Method,
Inherited = false)]
#if PolyPublic
public
#endif
sealed class ModuleInitializerAttribute :
Attribute;
#else
using System.Runtime.CompilerServices;
[assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute))]
#endif
Add an extension method to Polyfill_TYPE.cs
where TYPE
is the type the method extending. So, for example, APIs that target StreamWriter
go in Polyfill_StreamWriter.cs
.
Example:
// <auto-generated />
#pragma warning disable
namespace Polyfills;
using System;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
#if FeatureMemory
using System.Buffers;
#endif
static partial class Polyfill
{
#if !NET8_0_OR_GREATER
//https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs#L670
/// <summary>
/// Asynchronously clears all buffers for the current writer and causes any buffered data to
/// be written to the underlying device.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous flush operation.</returns>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.flushasync#system-io-textwriter-flushasync(system-threading-cancellationtoken)
public static Task FlushAsync(this TextWriter target, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
return target.FlushAsync()
.WaitAsync(cancellationToken);
}
#endif
#if !NETCOREAPP3_0_OR_GREATER
/// <summary>
/// Equivalent to Write(stringBuilder.ToString()) however it uses the
/// StringBuilder.GetChunks() method to avoid creating the intermediate string
/// </summary>
/// <param name="value">The string (as a StringBuilder) to write to the stream</param>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-text-stringbuilder)
public static void Write(this TextWriter target, StringBuilder? value)
{
if (value == null)
{
return;
}
#if FeatureMemory
foreach (ReadOnlyMemory<char> chunk in value.GetChunks())
{
target.Write(chunk.Span);
}
#else
target.Write(value.ToString());
#endif
}
/// <summary>
/// Equivalent to WriteAsync(stringBuilder.ToString()) however it uses the
/// StringBuilder.GetChunks() method to avoid creating the intermediate string
/// </summary>
/// <param name="value">The string (as a StringBuilder) to write to the stream</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeasync#system-io-textwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)
public static Task WriteAsync(this TextWriter target, StringBuilder? value, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
if (value == null)
{
return Task.CompletedTask;
}
return WriteAsyncCore(value, cancellationToken);
async Task WriteAsyncCore(StringBuilder builder, CancellationToken cancel)
{
#if FeatureValueTask && FeatureMemory
foreach (ReadOnlyMemory<char> chunk in builder.GetChunks())
{
await target.WriteAsync(chunk, cancel).ConfigureAwait(false);
}
#else
await target.WriteAsync(builder.ToString())
.WaitAsync(cancellationToken);
#endif
}
}
#endif
#if (NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0) && FeatureMemory
#if FeatureValueTask
/// <summary>
/// Asynchronously writes a character memory region to the stream.
/// </summary>
/// <param name="buffer">The character memory region to write to the stream.</param>
/// <param name="cancellationToken">
/// The token to monitor for cancellation requests.
/// The default value is <see cref="CancellationToken.None"/>.
/// </param>
/// <returns>A task that represents the asynchronous write operation.</returns>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeasync#system-io-textwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)
public static ValueTask WriteAsync(
this TextWriter target,
ReadOnlyMemory<char> buffer,
CancellationToken cancellationToken = default)
{
// StreamReader doesn't accept cancellation token (pre-netstd2.1)
cancellationToken.ThrowIfCancellationRequested();
if (!MemoryMarshal.TryGetArray(buffer, out var segment))
{
segment = new(buffer.ToArray());
}
var task = target.WriteAsync(segment.Array!, segment.Offset, segment.Count)
.WaitAsync(cancellationToken);
return new(task);
}
/// <summary>
/// Asynchronously writes the text representation of a character memory region to the stream, followed by a line terminator.
/// </summary>
/// <param name="buffer">The character memory region to write to the stream.</param>
/// <param name="cancellationToken">
/// The token to monitor for cancellation requests.
/// The default value is <see cref="CancellationToken.None"/>.
/// </param>
/// <returns>A task that represents the asynchronous write operation.</returns>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync#system-io-textwriter-writelineasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)
public static ValueTask WriteLineAsync(
this TextWriter target,
ReadOnlyMemory<char> buffer,
CancellationToken cancellationToken = default)
{
// StreamReader doesn't accept cancellation token (pre-netstd2.1)
cancellationToken.ThrowIfCancellationRequested();
if (!MemoryMarshal.TryGetArray(buffer, out var segment))
{
segment = new(buffer.ToArray());
}
var task = target.WriteLineAsync(segment.Array!, segment.Offset, segment.Count)
.WaitAsync(cancellationToken);
return new(task);
}
#endif
/// <summary>
/// Writes a character span to the text stream.
/// </summary>
/// <param name="buffer">The character span to write.</param>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-readonlyspan((system-char)))
public static void Write(
this TextWriter target,
ReadOnlySpan<char> buffer)
{
var pool = ArrayPool<char>.Shared;
var array = pool.Rent(buffer.Length);
try
{
buffer.CopyTo(new(array));
target.Write(array, 0, buffer.Length);
}
finally
{
pool.Return(array);
}
}
/// <summary>
/// Writes the text representation of a character span to the text stream, followed by a line terminator.
/// </summary>
/// <param name="buffer">The char span value to write to the text stream.</param>
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.writeline#system-io-textwriter-writeline(system-readonlyspan((system-char)))
public static void WriteLine(
this TextWriter target,
ReadOnlySpan<char> buffer)
{
var pool = ArrayPool<char>.Shared;
var array = pool.Rent(buffer.Length);
try
{
buffer.CopyTo(new(array));
target.WriteLine(array, 0, buffer.Length);
}
finally
{
pool.Return(array);
}
}
#endif
}
Add a for the new API to the Tests project.
Extension method tests to PolyfillTests_TYPE.cs
where TYPE
is the type the method extending. So, for example, APIs that target StreamWriter
go in PolyfillTests_StreamWriter.cs
. For example:
partial class PolyfillTests
{
[Test]
public async Task StreamReaderReadAsync()
{
using var stream = new MemoryStream("value"u8.ToArray());
var result = new char[5];
var memory = new Memory<char>(result);
using var reader = new StreamReader(stream);
var read = await reader.ReadAsync(memory);
Assert.AreEqual(5, read);
Assert.IsTrue("value".SequenceEqual(result));
}
[Test]
public async Task StreamReaderReadToEndAsync()
{
using var stream = new MemoryStream("value"u8.ToArray());
using var reader = new StreamReader(stream);
var read = await reader.ReadToEndAsync(Cancel.None);
Assert.AreEqual("value", read);
}
[Test]
public async Task StreamReaderReadLineAsync()
{
using var stream = new MemoryStream("line1\nline2"u8.ToArray());
using var reader = new StreamReader(stream);
var read = await reader.ReadLineAsync(CancellationToken.None);
Assert.AreEqual("line1", read);
}
}
Add documentation for the API to the readme.md
.
Add a simple usage of the API to the Consume project. The usage is there to check it compiles on old runtimes, not correctness.
Put a usage of polyfilled class method into a method in Consume.cs
with suffix _Methods
(e.g. Stream_Methods
for Stream
type methods). Keep method names in alphabetical order, do not use modifiers.
If new API is a compiler API (e.g. that polyfilled deconstruct method can be used in foreach loop), put usage into Compiler Features region.