See the question and my original answer on StackOverflow

You can use IStream which is a fairly well-known Windows (COM) type and implement it with C# (both ways from native to .NET and from .NET to native), for example like this:

public sealed class ManagedIStream : System.Runtime.InteropServices.ComTypes.IStream
{
    private readonly Stream _stream;

    public ManagedIStream(Stream stream)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
    }

    public void Read(byte[] pv, int cb, IntPtr pcbRead)
    {
        if (pv == null)
            throw new ArgumentNullException(nameof(pv));

        var read = _stream.Read(pv, 0, cb);
        if (pcbRead != IntPtr.Zero)
        {
            Marshal.WriteInt32(pcbRead, read);
        }
    }

    public void Write(byte[] pv, int cb, IntPtr pcbWritten)
    {
        if (pv == null)
            throw new ArgumentNullException(nameof(pv));

        _stream.Write(pv, 0, cb);
        if (pcbWritten != IntPtr.Zero)
        {
            Marshal.WriteInt32(pcbWritten, cb);
        }
    }

    public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
    {
        var newPos = _stream.Seek(dlibMove, (SeekOrigin)dwOrigin);
        if (plibNewPosition != IntPtr.Zero)
        {
            Marshal.WriteInt64(plibNewPosition, newPos);
        }
    }

    public void SetSize(long libNewSize) => _stream.SetLength(libNewSize);
    public void Commit(int grfCommitFlags) => _stream.Flush();

    public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag)
    {
        pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG
        {
            type = (int)STGTY.STGTY_STREAM,
            cbSize = _stream.Length,
            grfMode = 0
        };

        if (_stream.CanRead && _stream.CanWrite)
        {
            pstatstg.grfMode |= (int)STGM.STGM_READWRITE;
            return;
        }

        if (_stream.CanRead)
        {
            pstatstg.grfMode |= (int)STGM.STGM_READ;
            return;
        }

        if (_stream.CanWrite)
        {
            pstatstg.grfMode |= (int)STGM.STGM_WRITE;
            return;
        }

        throw new IOException();
    }

    public void CopyTo(System.Runtime.InteropServices.ComTypes.IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten)
    {
        if (pstm == null)
            throw new ArgumentNullException(nameof(pstm));

        long count;
        using (var stream = new StreamOnIStream(pstm))
        {
            count = CopyTo(_stream, stream, cb);
        }

        if (pcbRead != IntPtr.Zero)
        {
            Marshal.WriteInt64(pcbRead, count);
        }

        if (pcbWritten != IntPtr.Zero)
        {
            Marshal.WriteInt64(pcbWritten, count);
        }
    }

    private static long CopyTo(Stream input, Stream output, long count = long.MaxValue, int bufferSize = 0x14000)
    {
        if (input == null)
            throw new ArgumentNullException(nameof(input));

        if (output == null)
            throw new ArgumentNullException(nameof(output));

        if (count <= 0)
            throw new ArgumentException(null, nameof(count));

        if (bufferSize <= 0)
            throw new ArgumentException(null, nameof(bufferSize));

        if (count < bufferSize)
        {
            bufferSize = (int)count;
        }

        var bytes = new byte[bufferSize];
        var total = 0;
        do
        {
            var max = (int)Math.Min(count - total, bytes.Length);
            var read = input.Read(bytes, 0, max);
            if (read == 0)
                break;

            output.Write(bytes, 0, read);
            total += read;
            if (total == count)
                break;
        }
        while (true);
        return total;
    }

    public void Revert() => throw new NotSupportedException();
    public void LockRegion(long libOffset, long cb, int dwLockType) => throw new NotSupportedException();
    public void UnlockRegion(long libOffset, long cb, int dwLockType) => throw new NotSupportedException();
    public void Clone(out System.Runtime.InteropServices.ComTypes.IStream ppstm) => throw new NotSupportedException();
}

public class StreamOnIStream : Stream
{
    private const int STATFLAG_NONAME = 1;

    private System.Runtime.InteropServices.ComTypes.IStream _stream;
    private IntPtr _ptr;
    private long _position;

    public StreamOnIStream(System.Runtime.InteropServices.ComTypes.IStream stream)
    {
        _stream = stream;
        _ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf<long>()); // works for 32b & 64b
        CanRead = true;
        CanSeek = true;
        CanWrite = true;
    }

    public System.Runtime.InteropServices.ComTypes.IStream NativeStream => CheckDisposed();
    public override bool CanTimeout => false;
    public override int ReadTimeout => Timeout.Infinite;
    public override int WriteTimeout => Timeout.Infinite;
    public override bool CanRead { get; }
    public override bool CanSeek { get; }
    public override bool CanWrite { get; }
    public override long Position { get => _position; set => Seek(value, SeekOrigin.Begin); }
    public override long Length { get { CheckDisposed().Stat(out var stat, STATFLAG_NONAME); return stat.cbSize; } }
    public DateTimeOffset CreationTime { get { CheckDisposed().Stat(out var stat, STATFLAG_NONAME); return ToDateTimeOffset(stat.ctime); } }
    public DateTimeOffset LastWriteTime { get { CheckDisposed().Stat(out var stat, STATFLAG_NONAME); return ToDateTimeOffset(stat.mtime); } }
    public DateTimeOffset LastAccessTime { get { CheckDisposed().Stat(out var stat, STATFLAG_NONAME); return ToDateTimeOffset(stat.atime); } }
    public Guid Clsid { get { CheckDisposed().Stat(out var stat, STATFLAG_NONAME); return stat.clsid; } }
    public string Name { get { CheckDisposed().Stat(out var stat, 0); return stat.pwcsName; } }
    public STGM StorageMode { get { CheckDisposed().Stat(out var stat, STATFLAG_NONAME); return (STGM)stat.grfMode; } }
    public STGTY StorageType { get { CheckDisposed().Stat(out var stat, STATFLAG_NONAME); return (STGTY)stat.type; } }

    private static DateTimeOffset ToDateTimeOffset(System.Runtime.InteropServices.ComTypes.FILETIME fileTime)
    {
        var ft = (((long)fileTime.dwHighDateTime) << 32) + fileTime.dwLowDateTime;
        return DateTimeOffset.FromFileTime(ft);
    }

    public virtual void Flush(STGC options) => CheckDisposed().Commit((int)options);
    public override void Flush() => Flush(STGC.STGC_DEFAULT);

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (buffer == null)
            throw new ArgumentNullException(nameof(buffer));

        if (offset < 0)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (count == 0)
            return 0;

        if (offset == 0)
            return Read(buffer, count);

        var bytes = new byte[count];
        var read = Read(bytes, bytes.Length);
        if (read > 0)
        {
            Array.Copy(bytes, 0, buffer, offset, read);
        }
        return read;
    }

    private int Read(byte[] buffer, int count)
    {
        CheckDisposed().Read(buffer, count, _ptr);
        var read = Marshal.ReadInt32(_ptr);
        _position += read;
        return read;
    }

    public override void SetLength(long value) => CheckDisposed().SetSize(value);
    public override long Seek(long offset, SeekOrigin origin)
    {
        CheckDisposed().Seek(offset, (int)origin, _ptr);
        _position = Marshal.ReadInt64(_ptr);
        return _position;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        if (buffer == null)
            throw new ArgumentNullException(nameof(buffer));

        if (offset < 0)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (count == 0)
            return;

        if (offset == 0)
        {
            CheckDisposed().Write(buffer, count, _ptr);
        }
        else
        {
            var bytes = new byte[count];
            Array.Copy(buffer, offset, bytes, 0, count);
            CheckDisposed().Write(bytes, bytes.Length, _ptr);
        }

        var written = Marshal.ReadInt32(_ptr);
        _position += written;
    }

    private System.Runtime.InteropServices.ComTypes.IStream CheckDisposed()
    {
        var stream = _stream;
        if (stream == null)
            throw new ObjectDisposedException(nameof(NativeStream));

        return stream;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        var stream = Interlocked.Exchange(ref _stream, null);
        if (stream != null)
        {
            try
            {
                stream.Commit((int)STGC.STGC_DEFAULT);
            }
#pragma warning disable CA1031 // Do not catch general exception types
            catch
            {
                //+ do nothing
            }
#pragma warning restore CA1031 // Do not catch general exception types
        }

        var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero);
        if (ptr != IntPtr.Zero)
        {
            Marshal.FreeCoTaskMem(ptr);
        }
    }
}

[Flags]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1712:Do not prefix enum values with type name")]
public enum STGC
{
    STGC_DEFAULT = 0x0,
    STGC_OVERWRITE = 0x1,
    STGC_ONLYIFCURRENT = 0x2,
    STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE = 0x4,
    STGC_CONSOLIDATE = 0x8
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1712:Do not prefix enum values with type name")]
[Flags]
public enum STGM
{
    STGM_DIRECT = 0x00000000,
    STGM_TRANSACTED = 0x00010000,
    STGM_SIMPLE = 0x08000000,
    STGM_READ = 0x00000000,
    STGM_WRITE = 0x00000001,
    STGM_READWRITE = 0x00000002,
    STGM_SHARE_DENY_NONE = 0x00000040,
    STGM_SHARE_DENY_READ = 0x00000030,
    STGM_SHARE_DENY_WRITE = 0x00000020,
    STGM_SHARE_EXCLUSIVE = 0x00000010,
    STGM_PRIORITY = 0x00040000,
    STGM_DELETEONRELEASE = 0x04000000,
    STGM_NOSCRATCH = 0x00100000,
    STGM_CREATE = 0x00001000,
    STGM_CONVERT = 0x00020000,
    STGM_FAILIFTHERE = 0x00000000,
    STGM_NOSNAPSHOT = 0x00200000,
    STGM_DIRECT_SWMR = 0x00400000,
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1712:Do not prefix enum values with type name")]
public enum STGTY
{
    STGTY_STORAGE = 1,
    STGTY_STREAM = 2,
    STGTY_LOCKBYTES = 3,
    STGTY_PROPERTY = 4,
}