Created
August 31, 2025 18:25
-
-
Save Sl4vP0weR/26d1702ccc10c351ce77dcf23142d8fa to your computer and use it in GitHub Desktop.
Thread-safe stream wrapper that discards certain operations on the underlying stream as a lifetime control measure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.IO; | |
| using System.Threading; | |
| using System.Threading.Tasks; | |
| /// <summary> | |
| /// Thread-safe stream wrapper that discards certain operations on the underlying stream as a lifetime control measure.<br/> | |
| /// If intention is only to read, external code should only read from the stream, other unintended operations will be discarded.<br/> | |
| /// In case external code tries to manage lifetime of the stream, on <see cref="Stream.Dispose()"/> the underlying stream isn't being disposed. | |
| /// </summary> | |
| /// <remarks> | |
| /// Based on Microsoft implementation of <see cref="Stream"/>.<see cref="Stream.Synchronized(Stream)"/>. | |
| /// </remarks> | |
| public sealed class StreamWrapper : Stream | |
| { | |
| /// <inheritdoc /> | |
| public override long Length => | |
| synchronizedDestinationStream.Length; | |
| /// <inheritdoc /> | |
| public override long Position | |
| { | |
| get => synchronizedDestinationStream.Position; | |
| set => GetUnderlyingStream(Options.AllowSeek).Position = value; | |
| } | |
| /// <inheritdoc /> | |
| public override bool CanRead => synchronizedDestinationStream.CanRead; | |
| /// <inheritdoc /> | |
| public override bool CanSeek => synchronizedDestinationStream.CanSeek; | |
| /// <inheritdoc /> | |
| public override bool CanWrite => synchronizedDestinationStream.CanWrite; | |
| /// <inheritdoc /> | |
| public override bool CanTimeout => synchronizedDestinationStream.CanTimeout; | |
| /// <inheritdoc /> | |
| public override int ReadTimeout => synchronizedDestinationStream.ReadTimeout; | |
| /// <inheritdoc /> | |
| public override int WriteTimeout => synchronizedDestinationStream.WriteTimeout; | |
| /// <summary> | |
| /// Determines if this wrapper was closed. | |
| /// </summary> | |
| public bool IsClosed { get; private set; } | |
| /// <summary> | |
| /// Options of this stream wrapper. | |
| /// </summary> | |
| public readonly StreamWrapperOptions Options; | |
| /// <inheritdoc cref="StreamWrapperOptions.DestinationStream" /> | |
| public readonly Stream DestinationStream; | |
| /// <summary> | |
| /// Thread-safe synchronized stream to perform operations. | |
| /// </summary> | |
| private readonly Stream synchronizedDestinationStream; | |
| private StreamWrapper(StreamWrapperOptions options) | |
| { | |
| Options = options; | |
| DestinationStream = options.DestinationStream; | |
| synchronizedDestinationStream = Stream.Synchronized(DestinationStream); | |
| } | |
| /// <summary> | |
| /// Create a stream wrapper intended for any operation. | |
| /// </summary> | |
| public static StreamWrapper For(Stream destinationStream) => FromOptions(new() | |
| { | |
| DestinationStream = destinationStream | |
| }); | |
| /// <summary> | |
| /// Create a stream wrapper from specified options. | |
| /// </summary> | |
| public static StreamWrapper FromOptions(StreamWrapperOptions options) => new(options); | |
| /// <summary> | |
| /// Create a stream wrapper for read-only operations. | |
| /// </summary> | |
| public static StreamWrapper ForReadOnly(Stream destinationStream) => FromOptions(new() | |
| { | |
| DestinationStream = destinationStream, | |
| AllowWrite = false, | |
| AllowFlush = false, | |
| AllowResize = false | |
| }); | |
| /// <inheritdoc /> | |
| public override long Seek(long offset, SeekOrigin origin) => | |
| GetUnderlyingStream(Options.AllowSeek).Seek(offset, origin); | |
| /// <inheritdoc /> | |
| public override void SetLength(long value) => | |
| GetUnderlyingStream(Options.AllowResize).SetLength(value); | |
| /// <inheritdoc /> | |
| public override void Flush() => | |
| GetUnderlyingStream(Options.AllowFlush).Flush(); | |
| /// <inheritdoc /> | |
| public override Task FlushAsync(CancellationToken cancellationToken) => | |
| GetUnderlyingStream(Options.AllowFlush).FlushAsync(cancellationToken); | |
| /// <inheritdoc /> | |
| public override int Read(byte[] buffer, int offset, int count) => | |
| GetUnderlyingStream(Options.AllowRead).Read(buffer, offset, count); | |
| /// <inheritdoc /> | |
| public override int Read(Span<byte> buffer) => | |
| GetUnderlyingStream(Options.AllowRead).Read(buffer); | |
| /// <inheritdoc /> | |
| public override int ReadByte() => | |
| GetUnderlyingStream(Options.AllowRead).ReadByte(); | |
| /// <inheritdoc /> | |
| public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => | |
| GetUnderlyingStream(Options.AllowRead).ReadAsync(buffer, offset, count, cancellationToken); | |
| /// <inheritdoc /> | |
| public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => | |
| GetUnderlyingStream(Options.AllowRead).ReadAsync(buffer, cancellationToken); | |
| /// <inheritdoc /> | |
| public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => | |
| GetUnderlyingStream(Options.AllowRead).BeginRead(buffer, offset, count, callback, state); | |
| /// <inheritdoc /> | |
| public override int EndRead(IAsyncResult asyncResult) => | |
| GetUnderlyingStream(Options.AllowRead).EndRead(asyncResult); | |
| /// <inheritdoc /> | |
| public override void Write(byte[] buffer, int offset, int count) => | |
| GetUnderlyingStream(Options.AllowWrite).Write(buffer, offset, count); | |
| /// <inheritdoc /> | |
| public override void Write(ReadOnlySpan<byte> buffer) => | |
| GetUnderlyingStream(Options.AllowWrite).Write(buffer); | |
| /// <inheritdoc /> | |
| public override void WriteByte(byte value) => | |
| GetUnderlyingStream(Options.AllowWrite).WriteByte(value); | |
| /// <inheritdoc /> | |
| public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => | |
| GetUnderlyingStream(Options.AllowWrite).WriteAsync(buffer, cancellationToken); | |
| /// <inheritdoc /> | |
| public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => | |
| GetUnderlyingStream(Options.AllowWrite).WriteAsync(buffer, offset, count, cancellationToken); | |
| /// <inheritdoc /> | |
| public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => | |
| GetUnderlyingStream(Options.AllowWrite).BeginWrite(buffer, offset, count, callback, state); | |
| /// <inheritdoc /> | |
| public override void EndWrite(IAsyncResult asyncResult) => | |
| GetUnderlyingStream(Options.AllowWrite).EndWrite(asyncResult); | |
| /// <inheritdoc /> | |
| protected override void Dispose(bool disposing) | |
| { | |
| if (!IsClosed) | |
| IsClosed = true; | |
| base.Dispose(disposing); | |
| } | |
| private Stream GetUnderlyingStream(bool shouldAllowOperation) | |
| { | |
| EnsureNotClosed(); | |
| return shouldAllowOperation ? synchronizedDestinationStream : Stream.Null; | |
| } | |
| private void EnsureNotClosed() | |
| { | |
| if (!IsClosed) | |
| return; | |
| throw new ObjectDisposedException(null, "Cannot access a closed Stream."); | |
| } | |
| } | |
| /// <summary> | |
| /// Options for the <see cref="StreamWrapper"/> type. | |
| /// </summary> | |
| public sealed record StreamWrapperOptions | |
| { | |
| private readonly Stream destinationStream = null!; | |
| /// <summary> | |
| /// Destination or original stream that will be used as a target for wrapper. | |
| /// </summary> | |
| /// <exception cref="ArgumentNullException">Stream was null, target stream can't be null.</exception> | |
| public required Stream DestinationStream | |
| { | |
| get => destinationStream; | |
| init => destinationStream = value ?? throw new ArgumentNullException(nameof(DestinationStream)); | |
| } | |
| /// <summary> | |
| /// Copies instance with all the properties of this type and uses different destination stream. | |
| /// </summary> | |
| /// <param name="destinationStream">New destination stream.</param> | |
| public StreamWrapperOptions CopyFor(Stream destinationStream) => | |
| this with { DestinationStream = destinationStream }; | |
| /// <summary> | |
| /// Allow read operations. | |
| /// </summary> | |
| public bool AllowRead { get; init; } = true; | |
| /// <summary> | |
| /// Allow write operations. | |
| /// </summary> | |
| public bool AllowWrite { get; init; } = true; | |
| /// <summary> | |
| /// Allow flush operations. | |
| /// </summary> | |
| public bool AllowFlush { get; init; } = true; | |
| /// <summary> | |
| /// Allow resize operations. | |
| /// </summary> | |
| public bool AllowResize { get; init; } = true; | |
| /// <summary> | |
| /// Allow seek operations. | |
| /// </summary> | |
| public bool AllowSeek { get; init; } = true; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment