-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[API Proposal]: Provide an Stream abstraction centered on best-practices #79261
Comments
Tagging subscribers to this area: @dotnet/area-system-io Issue DetailsBackground and motivationStream is a class that has lived to see
Given that is easy to fall into these trpas, specially if you are not familiar with This proposal tries to provide a path to fulfill the checkbox "Making it easier to create custom streams" in #58216 by providing an abstraction that is centered in implementing the Stream members according to the most-performant standards and removing the burden of being forced to implement members that can be considered stalled. API ProposalNew Stream abstraction with Template Method Pattern.To ensure the latest and shiniest Read[Async]/Write[Async] APIs are used, we need to seal the old methods. This can be achieved with the template method pattern. namespace System.IO;
public abstract class NewStream : Stream // Name TBD
{
public sealed override int Read(byte[] buffer, int offset, int count) { throw null; }
public sealed override int Read(Span<byte> buffer) { throw null; }
public sealed override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { throw null; }
public sealed override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) { throw null; }
public sealed override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { throw null; }
public sealed override int EndRead(IAsyncResult asyncResult) { throw null; }
protected abstract int ReadCore(Span<byte> buffer);
protected abstract ValueTask<int> ReadCoreAsync(Memory<byte> buffer, CancellationToken cancellationToken);
public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { throw null; }
public sealed override void EndWrite(IAsyncResult asyncResult) { throw null; }
public sealed override void Write(byte[] buffer, int offset, int count) { throw null; }
public sealed override void Write(ReadOnlySpan<byte> buffer) { throw null; }
public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { throw null; }
public sealed override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) { throw null; }
protected abstract void WriteCore(ReadOnlySpan<byte> buffer);
protected abstract ValueTask WriteCoreAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken);
public override long Seek(long offset, SeekOrigin origin) { throw null; }
protected abstract long SeekCore(long offset, SeekOrigin origin);
public override void SetLength(long value) { throw null; }
protected abstract void SetLengthCore(long value);
protected override void Dispose(bool disposing) { throw null; }
} API Usage~ Alternative DesignsNewStream abstraction that promotes the latest and shiniest Read[Async]/Write[Async] methods to become abstract and seal the old ones.This is another way to achieve the same goal of enforcing the use of the best-performant methods but with minimal changes and using This approach is more convenient for future-proof as any new Stream API that would replace one of the current best-performant ones could be enabled by adding another class on top. See versioning proof-of-concept here. public abstract class NewStream : Stream // Name TBD
{
// seal the methods that DO NOT align with best practices, we will provide an impl. that aligns with efficiency according to the newest paradigms in Stream.
public sealed override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { throw null; }
public sealed override int EndRead(IAsyncResult asyncResult) { throw null; }
public sealed override int Read(byte[] buffer, int offset, int count) { throw null; }
public sealed override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { throw null; }
public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { throw null; }
public sealed override void EndWrite(IAsyncResult asyncResult) { throw null; }
public sealed override void Write(byte[] buffer, int offset, int count) { throw null; }
public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { throw null; }
// promote to abstract the methods that DO align with best practices.
public abstract override int Read(Span<byte> buffer);
public abstract override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default);
public abstract override void Write(ReadOnlySpan<byte> buffer);
public abstract override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default);
} RisksFuture-proof is a risk, as much as we plan today, we may not be prepared for the next big thing, similar to how Stream was planned without Task or Span in mind. As proposed above, the alternative design can allow for versioning and friendly patching of the class with new abstractions on top.
|
Why do we need both when they accept same arguments and return same result? protected abstract int ReadCore(Span<byte> buffer);
public sealed override int Read(Span<byte> buffer) { throw null; } |
@adamsitnik I think it is because |
This is actually a lot more reasonable than I expected from the title.
|
It's from the Template Method Pattern. |
@Joe4evr yes to both of your questions. |
Have you considered using source generators to fill this boilerplate? |
How does a source generator would make it better? As reference, this is how I envision |
Here's how I imagine the source generator: [GenerateStreamBoilerplate]
public partial MyStream
{
// (Read|Write)Core(Async)? would be provided by the user.
// The Stream overrides would be provided by the source generator.
} Here are some observations of mine about a subclass-based approach:
|
I had on my todo list the action item of writing up almost this exact same idea :) |
@teo-tsirpanis so, if I follow correctly, you are saying that we can add a How does this solve the issue of users implementing say Do you see any other drawbacks on this approach? I'm still not very familiar with source generators. |
As I imagine it, the generator would add a |
I thought your attribute was meant to be used in any Stream subclass, not only the ones inheriting from the NewStream Subclass. |
Yes, there won't be a subclass. In source generators we usually create partial methods to have the generator implement it, but here it will be the opposite; the generator will create a partial method to have us implement it. Implementing the (synchronous) span methods will be a required minimum; it is the new way forward for those who want to use the boilerplate generator. |
@teo-tsirpanis @jozkee I think the source generator design seems pretty compelling. One of the challenges today with implementing
An attribute-based approach could help us with this in a way that is much cleaner than a base class, for example:
This way, you can mix and match desired stream capabilities and have the compiler guide you towards exactly what needs to be implemented, filling in the boilerplate correctly for all other cases. |
Based on the discussions here and our promising findings on the source generator experiment, I'm going to close this proposal issue. We will work to get that source generator merged into runtimelab and advertise it as an experiment that we'd like to get feedback on. We'll try it out ourselves too, to see if it can reduce the code we manually author/maintain in our own Stream derivatives. |
Background and motivation
Stream is a class that has lived to see
Task
s andSpan
s and has been extended to align with them by addingvirtual
members/overloads. This has the consequence thatStream
has grown more and more complex over time and has become a pit of failure for new implementations, to name a few cases:Given that is easy to fall into these traps, specially if you are not familiar with
Stream
s, I think that we could provide an abstraction that covers the pit-of-failures and enforces implementers to use the methods that align with current standards.This proposal tries to provide a path to fulfill the checkbox "Making it easier to create custom streams" in #58216 by providing an abstraction that is centered in implementing the Stream members according to the most-performant standards and removing the burden of being forced to implement members that can be considered stalled.
API Proposal
New Stream abstraction with Template Method Pattern.
To ensure the latest and shiniest Read[Async]/Write[Async] APIs are used, we need to seal the old methods. This can be achieved with the template method pattern.
On top of that, we can extend the pattern to other methods that require boilerplate validations. This way a class deriving from
NewStream
only needs to worry for implementing theCore
methods and work with validated arguments.Alternative Designs
NewStream abstraction that promotes the latest and shiniest Read[Async]/Write[Async] methods to become abstract and seal the old ones.
This is another way to achieve the same goal of enforcing the use of the best-performant methods but with minimal changes and using
abstract override
to "promote" existing virtual methods.This approach is more convenient for future-proof as any new Stream API that would replace one of the current best-performant ones could be enabled by adding another class on top. See versioning proof-of-concept here.
Risks
Future-proof is a risk, as much as we plan today, we may not be prepared for the next big thing, similar to how Stream was planned without Task or Span in mind. As proposed above, the alternative design can allow for versioning and friendly patching of the class with new abstractions on top.
The text was updated successfully, but these errors were encountered: