-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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):
runtime/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs
Lines 660 to 685 in 1d1bf92
| 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; | |
| } | |
| } |
runtime/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs
Lines 814 to 837 in 1d1bf92
| 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 = defaultin the signatures)?