diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs
new file mode 100644
index 00000000000..21026113957
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Runtime.CompilerServices;
+using Microsoft.Toolkit.HighPerformance.Buffers;
+using Microsoft.Toolkit.HighPerformance.Streams;
+using Microsoft.Toolkit.HighPerformance.Streams.Sources;
+
+namespace Microsoft.Toolkit.HighPerformance.Extensions
+{
+ ///
+ /// Helpers for working with the type.
+ ///
+ public static class ArrayPoolBufferWriterExtensions
+ {
+ ///
+ /// Returns a that can be used to write to a target an of instance.
+ ///
+ /// The target instance.
+ /// A wrapping and writing data to its underlying buffer.
+ /// The returned can only be written to and does not support seeking.
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Stream AsStream(this ArrayPoolBufferWriter writer)
+ {
+ return new IBufferWriterStream(new ArrayBufferWriterOwner(writer));
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs
index 67c30579beb..07a5bd6c2b8 100644
--- a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs
+++ b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs
@@ -4,8 +4,13 @@
using System;
using System.Buffers;
+using System.Diagnostics.Contracts;
+using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using Microsoft.Toolkit.HighPerformance.Buffers;
+using Microsoft.Toolkit.HighPerformance.Streams;
+using Microsoft.Toolkit.HighPerformance.Streams.Sources;
namespace Microsoft.Toolkit.HighPerformance.Extensions
{
@@ -14,6 +19,28 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
///
public static class IBufferWriterExtensions
{
+ ///
+ /// Returns a that can be used to write to a target an of instance.
+ ///
+ /// The target instance.
+ /// A wrapping and writing data to its underlying buffer.
+ /// The returned can only be written to and does not support seeking.
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Stream AsStream(this IBufferWriter writer)
+ {
+ if (writer.GetType() == typeof(ArrayPoolBufferWriter))
+ {
+ // If the input writer is of type ArrayPoolBufferWriter, we can use the type
+ // specific buffer writer owner to let the JIT elide callvirts when accessing it.
+ var internalWriter = Unsafe.As>(writer)!;
+
+ return new IBufferWriterStream(new ArrayBufferWriterOwner(internalWriter));
+ }
+
+ return new IBufferWriterStream(new IBufferWriterOwner(writer));
+ }
+
///
/// Writes a value of a specified type into a target instance.
///
diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs
index 331786d086d..74526cb1f0f 100644
--- a/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs
+++ b/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs
@@ -167,24 +167,24 @@ public static void Write(this Stream stream, ReadOnlySpan buffer)
/// The value read from .
/// Thrown if reaches the end.
#if SPAN_RUNTIME_SUPPORT
- // Avoid inlining as we're renting a stack buffer, which
- // cause issues if this method was called inside a loop
- [MethodImpl(MethodImplOptions.NoInlining)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public static T Read(this Stream stream)
where T : unmanaged
{
#if SPAN_RUNTIME_SUPPORT
- Span span = stackalloc byte[Unsafe.SizeOf()];
+ T result = default;
+ int length = Unsafe.SizeOf();
- if (stream.Read(span) != span.Length)
+ unsafe
{
- ThrowInvalidOperationExceptionForEndOfStream();
+ if (stream.Read(new Span(&result, length)) != length)
+ {
+ ThrowInvalidOperationExceptionForEndOfStream();
+ }
}
- ref byte r0 = ref MemoryMarshal.GetReference(span);
-
- return Unsafe.ReadUnaligned(ref r0);
+ return result;
#else
int length = Unsafe.SizeOf();
byte[] buffer = ArrayPool.Shared.Rent(length);
diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs
new file mode 100644
index 00000000000..f9b431fd904
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#if SPAN_RUNTIME_SUPPORT
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Toolkit.HighPerformance.Streams
+{
+ ///
+ internal sealed partial class IBufferWriterStream
+ {
+ ///
+ public override void CopyTo(Stream destination, int bufferSize)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return new ValueTask(Task.FromCanceled(cancellationToken));
+ }
+
+ try
+ {
+ Write(buffer.Span);
+
+ return default;
+ }
+ catch (OperationCanceledException e)
+ {
+ return new ValueTask(Task.FromCanceled(e.CancellationToken));
+ }
+ catch (Exception e)
+ {
+ return new ValueTask(Task.FromException(e));
+ }
+ }
+
+ ///
+ public override int Read(Span buffer)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override void Write(ReadOnlySpan buffer)
+ {
+ MemoryStream.ValidateDisposed(this.disposed);
+
+ Span destination = this.bufferWriter.GetSpan(buffer.Length);
+
+ if (!buffer.TryCopyTo(destination))
+ {
+ MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite();
+ }
+
+ this.bufferWriter.Advance(buffer.Length);
+ }
+ }
+}
+
+#endif
diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs
new file mode 100644
index 00000000000..2be38aa9761
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs
@@ -0,0 +1,173 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Toolkit.HighPerformance.Streams
+{
+ ///
+ /// A implementation wrapping an instance.
+ ///
+ /// The type of buffer writer to use.
+ internal sealed partial class IBufferWriterStream : Stream
+ where TWriter : struct, IBufferWriter
+ {
+ ///
+ /// The target instance to use.
+ ///
+ private readonly TWriter bufferWriter;
+
+ ///
+ /// Indicates whether or not the current instance has been disposed
+ ///
+ private bool disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The target instance to use.
+ public IBufferWriterStream(TWriter bufferWriter)
+ {
+ this.bufferWriter = bufferWriter;
+ }
+
+ ///
+ public override bool CanRead => false;
+
+ ///
+ public override bool CanSeek => false;
+
+ ///
+ public override bool CanWrite
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => !this.disposed;
+ }
+
+ ///
+ public override long Length => throw MemoryStream.GetNotSupportedException();
+
+ ///
+ public override long Position
+ {
+ get => throw MemoryStream.GetNotSupportedException();
+ set => throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override void Flush()
+ {
+ }
+
+ ///
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Task.FromCanceled(cancellationToken);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public override Task ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Task.FromCanceled(cancellationToken);
+ }
+
+ try
+ {
+ Write(buffer, offset, count);
+
+ return Task.CompletedTask;
+ }
+ catch (OperationCanceledException e)
+ {
+ return Task.FromCanceled(e.CancellationToken);
+ }
+ catch (Exception e)
+ {
+ return Task.FromException(e);
+ }
+ }
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override void SetLength(long value)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override int Read(byte[]? buffer, int offset, int count)
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override int ReadByte()
+ {
+ throw MemoryStream.GetNotSupportedException();
+ }
+
+ ///
+ public override void Write(byte[]? buffer, int offset, int count)
+ {
+ MemoryStream.ValidateDisposed(this.disposed);
+ MemoryStream.ValidateBuffer(buffer, offset, count);
+
+ Span
+ source = buffer.AsSpan(offset, count),
+ destination = this.bufferWriter.GetSpan(count);
+
+ if (!source.TryCopyTo(destination))
+ {
+ MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite();
+ }
+
+ this.bufferWriter.Advance(count);
+ }
+
+ ///
+ public override void WriteByte(byte value)
+ {
+ MemoryStream.ValidateDisposed(this.disposed);
+
+ this.bufferWriter.GetSpan(1)[0] = value;
+
+ this.bufferWriter.Advance(1);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ this.disposed = true;
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs
index cdfc24112d5..2d316df2242 100644
--- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs
+++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs
@@ -13,19 +13,28 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
internal static partial class MemoryStream
{
///
- /// Throws an when trying to write too many bytes to the target stream.
+ /// Gets a standard instance for a stream.
///
- public static void ThrowArgumentExceptionForEndOfStreamOnWrite()
+ /// A with the standard text.
+ public static Exception GetNotSupportedException()
{
- throw new ArgumentException("The current stream can't contain the requested input data.");
+ return new NotSupportedException("The requested operation is not supported for this stream.");
+ }
+
+ ///
+ /// Throws a when trying to perform a not supported operation.
+ ///
+ public static void ThrowNotSupportedException()
+ {
+ throw GetNotSupportedException();
}
///
- /// Throws a when trying to set the length of the stream.
+ /// Throws an when trying to write too many bytes to the target stream.
///
- public static void ThrowNotSupportedExceptionForSetLength()
+ public static void ThrowArgumentExceptionForEndOfStreamOnWrite()
{
- throw new NotSupportedException("Setting the length is not supported for this stream.");
+ throw new ArgumentException("The current stream can't contain the requested input data.");
}
///
@@ -77,14 +86,6 @@ private static void ThrowArgumentExceptionForLength()
throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer");
}
- ///
- /// Throws a when trying to write on a readonly stream.
- ///
- private static void ThrowNotSupportedExceptionForCanWrite()
- {
- throw new NotSupportedException("The current stream doesn't support writing.");
- }
-
///
/// Throws an when using a disposed instance.
///
diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs
index 2f35380918f..2f83e4872d7 100644
--- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs
+++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs
@@ -64,7 +64,7 @@ public static void ValidateCanWrite(bool canWrite)
{
if (!canWrite)
{
- ThrowNotSupportedExceptionForCanWrite();
+ ThrowNotSupportedException();
}
}
diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs
index b0dcd8bc908..0243dbd60d5 100644
--- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs
+++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs
@@ -220,7 +220,7 @@ public sealed override long Seek(long offset, SeekOrigin origin)
///
public sealed override void SetLength(long value)
{
- MemoryStream.ThrowNotSupportedExceptionForSetLength();
+ throw MemoryStream.GetNotSupportedException();
}
///
diff --git a/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs b/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs
new file mode 100644
index 00000000000..0a534dc2af6
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using Microsoft.Toolkit.HighPerformance.Buffers;
+
+namespace Microsoft.Toolkit.HighPerformance.Streams.Sources
+{
+ ///
+ /// An implementation wrapping an instance.
+ ///
+ internal readonly struct ArrayBufferWriterOwner : IBufferWriter
+ {
+ ///
+ /// The wrapped array.
+ ///
+ private readonly ArrayPoolBufferWriter writer;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The wrapped instance.
+ public ArrayBufferWriterOwner(ArrayPoolBufferWriter writer)
+ {
+ this.writer = writer;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Advance(int count)
+ {
+ this.writer.Advance(count);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Memory GetMemory(int sizeHint = 0)
+ {
+ return this.writer.GetMemory(sizeHint);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span GetSpan(int sizeHint = 0)
+ {
+ return this.writer.GetSpan(sizeHint);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs b/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs
new file mode 100644
index 00000000000..2580037f6d9
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using Microsoft.Toolkit.HighPerformance.Buffers;
+
+namespace Microsoft.Toolkit.HighPerformance.Streams.Sources
+{
+ ///
+ /// An implementation wrapping an instance.
+ ///
+ internal readonly struct IBufferWriterOwner : IBufferWriter
+ {
+ ///
+ /// The wrapped array.
+ ///
+ private readonly IBufferWriter writer;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The wrapped instance.
+ public IBufferWriterOwner(IBufferWriter writer)
+ {
+ this.writer = writer;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Advance(int count)
+ {
+ this.writer.Advance(count);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Memory GetMemory(int sizeHint = 0)
+ {
+ return this.writer.GetMemory(sizeHint);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span GetSpan(int sizeHint = 0)
+ {
+ return this.writer.GetSpan(sizeHint);
+ }
+ }
+}
diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs
index 3a481399422..a704aab1a64 100644
--- a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs
+++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs
@@ -188,28 +188,43 @@ public void Test_ArrayPoolBufferWriterOfT_MultipleDispose()
[TestMethod]
public void Test_ArrayPoolBufferWriterOfT_AsStream()
{
- var writer = new ArrayPoolBufferWriter();
-
- Span data = Guid.NewGuid().ToByteArray();
+ const int GuidSize = 16;
- data.CopyTo(writer.GetSpan(data.Length));
+ var writer = new ArrayPoolBufferWriter();
+ var guid = Guid.NewGuid();
- writer.Advance(data.Length);
+ // Here we first get a stream with the extension targeting ArrayPoolBufferWriter.
+ // This will wrap it into a custom internal stream type and produce a write-only
+ // stream that essentially mirrors the IBufferWriter functionality as a stream.
+ using (Stream writeStream = writer.AsStream())
+ {
+ writeStream.Write(guid);
+ }
- Assert.AreEqual(writer.WrittenCount, data.Length);
+ Assert.AreEqual(writer.WrittenCount, GuidSize);
- Stream stream = writer.AsStream();
+ // Here we get a readable stream instead, and read from it to ensure
+ // the previous data was written correctly from the writeable stream.
+ using (Stream stream = writer.WrittenMemory.AsStream())
+ {
+ Assert.AreEqual(stream.Length, GuidSize);
- Assert.AreEqual(stream.Length, data.Length);
+ byte[] result = new byte[GuidSize];
- byte[] result = new byte[16];
+ stream.Read(result, 0, result.Length);
- stream.Read(result, 0, result.Length);
+ // Read the guid data and ensure it matches our initial guid
+ Assert.IsTrue(new Guid(result).Equals(guid));
+ }
- Assert.IsTrue(data.SequenceEqual(result));
+ // Do a dummy write just to ensure the writer isn't disposed here.
+ // This is because we got a stream from a memory, not a memory owner.
+ writer.Write((byte)42);
+ writer.Advance(1);
- stream.Dispose();
+ writer.Dispose();
+ // Now check that the writer is actually disposed instead
Assert.ThrowsException(() => writer.Capacity);
}
}
diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs
new file mode 100644
index 00000000000..bd3677c079c
--- /dev/null
+++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs
@@ -0,0 +1,161 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Toolkit.HighPerformance.Buffers;
+using Microsoft.Toolkit.HighPerformance.Extensions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace UnitTests.HighPerformance.Streams
+{
+ [TestClass]
+ public class Test_IBufferWriterStream
+ {
+ [TestCategory("IBufferWriterStream")]
+ [TestMethod]
+ public void Test_IBufferWriterStream_Lifecycle()
+ {
+ ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter();
+
+ // Get a stream from a buffer writer aand validate that it can only be written to.
+ // This is to mirror the same functionality as the IBufferWriter interface.
+ Stream stream = ((IBufferWriter)writer).AsStream();
+
+ Assert.IsFalse(stream.CanRead);
+ Assert.IsFalse(stream.CanSeek);
+ Assert.IsTrue(stream.CanWrite);
+
+ Assert.ThrowsException(() => stream.Length);
+ Assert.ThrowsException(() => stream.Position);
+
+ // Dispose the stream and check that no operation is now allowed
+ stream.Dispose();
+
+ Assert.IsFalse(stream.CanRead);
+ Assert.IsFalse(stream.CanSeek);
+ Assert.IsFalse(stream.CanWrite);
+ Assert.ThrowsException(() => stream.Length);
+ Assert.ThrowsException(() => stream.Position);
+ }
+
+ [TestCategory("IBufferWriterStream")]
+ [TestMethod]
+ public void Test_IBufferWriterStream_Write_Array()
+ {
+ ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter();
+ Stream stream = ((IBufferWriter)writer).AsStream();
+
+ byte[] data = Test_MemoryStream.CreateRandomData(64);
+
+ // Write random data to the stream wrapping the buffer writer, and validate
+ // that the state of the writer is consistent, and the written content matches.
+ stream.Write(data, 0, data.Length);
+
+ Assert.AreEqual(writer.WrittenCount, data.Length);
+ Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data));
+
+ // A few tests with invalid inputs (null buffers, invalid indices, etc.)
+ Assert.ThrowsException(() => stream.Write(null, 0, 10));
+ Assert.ThrowsException(() => stream.Write(data, -1, 10));
+ Assert.ThrowsException(() => stream.Write(data, 200, 10));
+ Assert.ThrowsException(() => stream.Write(data, 0, -24));
+ Assert.ThrowsException(() => stream.Write(data, 0, 200));
+
+ stream.Dispose();
+
+ Assert.ThrowsException(() => stream.Write(data, 0, data.Length));
+ }
+
+ [TestCategory("IBufferWriterStream")]
+ [TestMethod]
+ public async Task Test_IBufferWriterStream_WriteAsync_Array()
+ {
+ ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter();
+ Stream stream = ((IBufferWriter)writer).AsStream();
+
+ byte[] data = Test_MemoryStream.CreateRandomData(64);
+
+ // Same test as above, but using an asynchronous write instead
+ await stream.WriteAsync(data, 0, data.Length);
+
+ Assert.AreEqual(writer.WrittenCount, data.Length);
+ Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data));
+
+ await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(null, 0, 10));
+ await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, -1, 10));
+ await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 200, 10));
+ await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, -24));
+ await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, 200));
+
+ stream.Dispose();
+
+ await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, data.Length));
+ }
+
+ [TestCategory("IBufferWriterStream")]
+ [TestMethod]
+ [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1500", Justification = "Array initialization")]
+ public void Test_IBufferWriterStream_WriteByte()
+ {
+ ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter();
+ Stream stream = ((IBufferWriter)writer).AsStream();
+
+ ReadOnlySpan data = stackalloc byte[] { 1, 128, 255, 32 };
+
+ foreach (var item in data.Enumerate())
+ {
+ // Since we're enumerating, we can also double check the current written count
+ // at each iteration, to ensure the writes are done correctly every time.
+ Assert.AreEqual(writer.WrittenCount, item.Index);
+
+ // Write a number of bytes one by one to test this API as well
+ stream.WriteByte(item.Value);
+ }
+
+ // Validate the final written length and actual data
+ Assert.AreEqual(writer.WrittenCount, data.Length);
+ Assert.IsTrue(data.SequenceEqual(writer.WrittenSpan));
+
+ Assert.ThrowsException(() => stream.ReadByte());
+ }
+
+ [TestCategory("IBufferWriterStream")]
+ [TestMethod]
+ public void Test_IBufferWriterStream_Write_Span()
+ {
+ ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter();
+ Stream stream = ((IBufferWriter)writer).AsStream();
+
+ Memory data = Test_MemoryStream.CreateRandomData(64);
+
+ // This will use the extension when on .NET Standard 2.0,
+ // as the Stream class doesn't have Spam or Memory
+ // public APIs there. This is the case eg. on UWP as well.
+ stream.Write(data.Span);
+
+ Assert.AreEqual(writer.WrittenCount, data.Length);
+ Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan));
+ }
+
+ [TestCategory("IBufferWriterStream")]
+ [TestMethod]
+ public async Task Test_IBufferWriterStream_WriteAsync_Memory()
+ {
+ ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter();
+ Stream stream = ((IBufferWriter)writer).AsStream();
+
+ Memory data = Test_MemoryStream.CreateRandomData(64);
+
+ // Same as the other asynchronous test above, but writing from a Memory
+ await stream.WriteAsync(data);
+
+ Assert.AreEqual(writer.WrittenCount, data.Length);
+ Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan));
+ }
+ }
+}
diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs
index f2c63182c63..f3b09be2081 100644
--- a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs
+++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs
@@ -272,7 +272,7 @@ public async Task Test_MemoryStream_ReadWriteAsync_Memory()
/// The number of array items to create.
/// The returned random array.
[Pure]
- private static byte[] CreateRandomData(int count)
+ internal static byte[] CreateRandomData(int count)
{
var random = new Random(DateTime.Now.Ticks.GetHashCode());
diff --git a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems
index 6e776be38bb..a114963de54 100644
--- a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems
+++ b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems
@@ -47,6 +47,7 @@
+