Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 2, 2025

Implementation Plan: TextWriter WriteAsync/WriteLineAsync CancellationToken Overloads

  • Explore the repository structure and understand existing code
  • Add three new non-virtual methods to TextWriter.cs:
    • Task WriteAsync(string? value, CancellationToken token)
    • Task WriteLineAsync(CancellationToken token)
    • Task WriteLineAsync(string? value, CancellationToken token)
  • Update the API surface in System.Runtime/ref/System.Runtime.cs
  • Add tests for the new methods in TextWriterTests.cs
  • Build and test the changes - All tests pass
  • Run code review - No issues found
  • Run CodeQL security check - No issues found
  • Add XML documentation to the new methods
  • Simplify implementation and update documentation per code review
  • Final verification - Complete

Summary

Successfully implemented the approved API proposal for adding CancellationToken overloads to TextWriter. The implementation:

  • Adds three new non-virtual methods that delegate to existing ReadOnlyMemory overloads
  • Properly handles null values and cancellation tokens through the ReadOnlyMemory overload
  • Includes comprehensive tests covering all scenarios
  • Includes XML documentation following .NET documentation guidelines with proper default value documentation
  • All tests pass successfully
Original prompt

This section details on the original issue you should resolve

<issue_title>[API Proposal]: TextWriter.WriteLineAsync with CancellationToken overloads</issue_title>
<issue_description>### Background and motivation

Currently, the only way to use StreamWriter.Write[Line]Async(string) with a CancellationToken is to call AsMemory on the string, then use StreamWriter.Write[Line]Async(ReadOnlyMemory<char>, CancellationToken). In fact, this is what StreamWriter.Write[Line]Async(string) does, but it uses default(CancellationToken) (see lines 676 and 833):

public override Task WriteAsync(string? value)
{
// If we have been inherited into a subclass, the following implementation could be incorrect
// since it does not call through to Write() which a subclass might have overridden.
// To be safe we will only use this implementation in cases where we know it is safe to do so,
// and delegate to our base class (which will call into Write) when we are not sure.
if (GetType() != typeof(StreamWriter))
{
return base.WriteAsync(value);
}
if (value != null)
{
ThrowIfDisposed();
CheckAsyncTaskInProgress();
Task task = WriteAsyncInternal(value.AsMemory(), appendNewLine: false, default);
_asyncWriteTask = task;
return task;
}
else
{
return Task.CompletedTask;
}
}

public override Task WriteLineAsync(string? value)
{
if (value == null)
{
return WriteLineAsync();
}
// If we have been inherited into a subclass, the following implementation could be incorrect
// since it does not call through to Write() which a subclass might have overridden.
// To be safe we will only use this implementation in cases where we know it is safe to do so,
// and delegate to our base class (which will call into Write) when we are not sure.
if (GetType() != typeof(StreamWriter))
{
return base.WriteLineAsync(value);
}
ThrowIfDisposed();
CheckAsyncTaskInProgress();
Task task = WriteAsyncInternal(value.AsMemory(), appendNewLine: true, default);
_asyncWriteTask = task;
return task;
}

I propose CancellationToken overloads be added to TextWriter and have StreamWriter use them.

API Proposal

namespace System.IO;

public abstract class TextWriter
{
    // default impl would have token only affect the `Task.Factory.StartNew` invocation (same
    //   behavior as existing `Write[Line]Async(ReadOnlyMemory<char>, CancellationToken)`  methods)
    // alternatively, they could complete synchronously like `Stream` does
    public virtual Task WriteAsync(string? value, CancellationToken token);
    public virtual Task WriteLineAsync(CancellationToken token);
    public virtual Task WriteLineAsync(string? value, CancellationToken token);
}

public class StreamWriter : TextWriter
{
    // token would be passed to `WriteAsyncInternal`
    public override Task WriteAsync(string? value, CancellationToken token);
    public override Task WriteLineAsync(CancellationToken token);
    public override Task WriteLineAsync(string? value, CancellationToken token);
}

API Usage

Old code:

await using StreamWriter writer = new(...);
await writer.WriteLine(string.Create(CultureInfo.InvariantCulture, $"...").AsMemory(), token);

New code:

await using StreamWriter writer = new(...);
await writer.WriteLine(string.Create(CultureInfo.InvariantCulture, $"..."), token);

Alternative Designs

An analyzer to turn StreamWriter.Write[Line]Async(string) into StreamWriter.Write[Line]Async(ReadOnlyMemory<char>, CancellationToken)

Risks

Code previously using TextWriter.WriteLineAsync() and TextWriter.Write[Line]Async(string) with a CancellationToken in scope will get new warnings to pass in that token.

Open Questions

  • Should the cancellation token have a default value (i.e., CancellationToken token = default in the signatures)?</issue_description>

<agent_instructions>Please implement the approved API on the base TextWriter class. You'll need to update not only the TextWriter.cs class, but also the corresponding surface area in the relevant ref file, and add tests.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@stephentoub > I stumbled over the same problem. I thought about giving it a shot, would this be a good first issue?

Thanks for the interest. We need to review the proposal first. </comment_new>
<comment_new>@bartonjs
Video

  • Approved as non-virtual (they can defer to the ReadOnlyMemory implementation). If we need virtual later we can do that.
namespace System.IO;

public abstract class TextWriter
{
    public Task WriteAsync(string? value, CancellationToken token);
    public Task WriteLineAsync(CancellationToken token);
    public Task WriteLineAsync(string? value, CancellationToken token);
}
```</body></comment_new>
</comments>

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@stephentoub stephentoub marked this pull request as ready for review December 2, 2025 23:31
Copilot AI review requested due to automatic review settings December 2, 2025 23:31
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements the approved API proposal to add CancellationToken overloads to TextWriter for WriteAsync and WriteLineAsync methods. The changes provide a more convenient way to pass cancellation tokens when writing strings asynchronously, avoiding the need to call .AsMemory() on strings.

  • Added three non-virtual methods to TextWriter: WriteAsync(string?, CancellationToken), WriteLineAsync(CancellationToken), and WriteLineAsync(string?, CancellationToken)
  • Updated the public API surface in System.Runtime.cs
  • Added comprehensive test coverage for all three new methods, including happy path, null handling, and cancellation scenarios

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs Implements three new non-virtual methods that delegate to existing ReadOnlyMemory<char> overloads with proper cancellation token handling
src/libraries/System.Runtime/ref/System.Runtime.cs Updates the public API surface to expose the three new method signatures
src/libraries/System.Runtime/tests/System.IO.Tests/TextWriter/TextWriterTests.cs Adds comprehensive test coverage with 6 test methods covering normal operation, null handling, and cancellation for all three new methods

@stephentoub
Copy link
Member

@copilot, please address the feedback, specifically about adding XML comments to the new APIs. These will be used to seed the docs and should mirror the existing documentation (from learn.microsoft.com) for the existing overloads.

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Dec 3, 2025

@copilot, please address the feedback, specifically about adding XML comments to the new APIs. These will be used to seed the docs and should mirror the existing documentation (from learn.microsoft.co...

Added XML documentation to all three new methods in commit 4ff346c. The documentation follows the pattern from the existing overloads and includes proper param descriptions, return value documentation, and uses <see langword> for null references.

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
/// <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>
public Task WriteAsync(string? value, CancellationToken cancellationToken) =>
WriteAsync(value.AsMemory(), cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

@bartonjs, was there any discussion in API review about instead adding an implicit operator from string to ReadOnlyMemory<char> (just as it has one to span)? Two of the three overloads being added here would then seem unnecessary.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

LGTM, thank you @stephentoub and @copilot ;)

@stephentoub stephentoub merged commit d31e599 into main Dec 11, 2025
141 of 145 checks passed
@jkotas jkotas deleted the copilot/add-cancellationtoken-overloads branch December 22, 2025 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: TextWriter.WriteLineAsync with CancellationToken overloads

3 participants