Skip to content

[API Proposal]: TextWriter.WriteLineAsync with CancellationToken overloads #106136

@colejohnson66

Description

@colejohnson66

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)?

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.IO

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions