Skip to content

Latest commit

 

History

History
446 lines (334 loc) · 15.3 KB

contributing.md

File metadata and controls

446 lines (334 loc) · 15.3 KB

Contributing

Solution Structure

Polyfill project

The main project that produces the nuget.

Tests project

A NUnit test project that verifies all the APIs.

NoRefsTests project

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.

PublicTests project

Polyfill supports making all APIs public. The PublicTests project tests that scenario.

UnsafeTests project

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>.

Consume project

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

ConsumeClassicReferences Project

Test the scenario when references are added through <Reference instead of <PackageReference.

Submitting a new polyfill API

Valid APIs

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.

Raise Pull Request not an Issue

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.

Add the new API to the Polyfill project

Conditional Compilation

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 disabled

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

ReSharper / Rider

Any potential ReSharper or Rider code formatting issues should be disabled. For example:

// ReSharper disable RedundantUsingDirective
// ReSharper disable UnusedMember.Global

Assume Implicit usings is disabled

Having Implicit usings enabled is optional for the consuming project. So ensure all using statements are included.

Make public if

Polyfill supports making all APIs public. This is done by making types public if PolyPublic. For example:

#if PolyPublic
public
#endif
sealed class ...

XML API comment

The XML API comments should match the actual API.

If the API is attribute based

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

snippet source | anchor

If the API is a missing instance method

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
}

snippet source | anchor

Add a test

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);
    }
}

snippet source | anchor

Add documentation

Add documentation for the API to the readme.md.

Add to the Consume project

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.