Skip to content

Instantly share code, notes, and snippets.

@Sl4vP0weR
Created August 31, 2025 18:25
Show Gist options
  • Save Sl4vP0weR/26d1702ccc10c351ce77dcf23142d8fa to your computer and use it in GitHub Desktop.
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.
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