diff --git a/src/messaging/dotnet/src/Core/MessageBuffer.cs b/src/messaging/dotnet/src/Core/MessageBuffer.cs index 17e7be2f9..4ee512529 100644 --- a/src/messaging/dotnet/src/Core/MessageBuffer.cs +++ b/src/messaging/dotnet/src/Core/MessageBuffer.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; +using CommunityToolkit.HighPerformance.Buffers; namespace MorganStanley.ComposeUI.Messaging; @@ -53,16 +54,9 @@ public ReadOnlySpan GetSpan() /// True, if the decoding was successful, False otherwise. public bool TryGetBase64Bytes(IBufferWriter bufferWriter) { - var utf8Span = new Span(_bytes, 0, _length); - var span = bufferWriter.GetSpan(Base64.GetMaxDecodedFromUtf8Length(utf8Span.Length)); - var status = Base64.DecodeFromUtf8(utf8Span, span, out _, out var bytesWritten); - - if (status != OperationStatus.Done) - return false; - - bufferWriter.Advance(bytesWritten); + ThrowIfDisposed(); - return true; + return TryGetBase64BytesCore(bufferWriter); } /// @@ -123,6 +117,11 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Acts as a stub for extensions methods that create instances with various formats. + /// + public static MessageBufferFactory Factory { get; } = new(); + /// /// Creates a new from a string. /// @@ -161,6 +160,14 @@ public static MessageBuffer Create(ReadOnlySpan utf8Bytes) return new MessageBuffer(buffer, utf8Bytes.Length); } + /// + /// Creates a new from a memory block containing the raw UTF8 bytes. + /// + /// + /// + /// The content of the buffer is not a valid UTF8 byte sequence. + public static MessageBuffer Create(ReadOnlyMemory utf8Bytes) => Create(utf8Bytes.Span); + /// /// Creates a new from a sequence containing the raw UTF8 bytes. /// @@ -247,6 +254,25 @@ public static MessageBuffer CreateBase64(ReadOnlySequence bytes) } } + /// + /// Returns an that can be used to build byte arrays in a memory-efficient way. + /// + /// + public static ArrayPoolBufferWriter GetBufferWriter() + { + return new ArrayPoolBufferWriter(Pool); + } + + /// + /// Returns an that can be used to build byte arrays in a memory-efficient way. + /// + /// The initial capacity of the buffer + /// + public static ArrayPoolBufferWriter GetBufferWriter(int capacity) + { + return new ArrayPoolBufferWriter(Pool, capacity); + } + /// /// Creates a new using the provided buffer. /// The buffer must have been allocated by calling @@ -304,6 +330,20 @@ private static void ValidateUtf8Bytes(ReadOnlySpan bytes) } } + private bool TryGetBase64BytesCore(IBufferWriter bufferWriter) + { + var utf8Span = new Span(_bytes, 0, _length); + var span = bufferWriter.GetSpan(Base64.GetMaxDecodedFromUtf8Length(utf8Span.Length)); + var status = Base64.DecodeFromUtf8(utf8Span, span, out _, out var bytesWritten); + + if (status != OperationStatus.Done) + return false; + + bufferWriter.Advance(bytesWritten); + + return true; + } + ~MessageBuffer() { DisposeCore(); @@ -311,9 +351,16 @@ private static void ValidateUtf8Bytes(ReadOnlySpan bytes) private static readonly UTF8Encoding Encoding = new(false, true); + /// + /// + /// + public sealed class MessageBufferFactory + { + } + private static class ThrowHelper { - public static InvalidOperationException InvalidBase64() + public static FormatException InvalidBase64() { return new("The current buffer is not Base64-encoded"); } diff --git a/src/messaging/dotnet/src/Core/MessageBufferJsonExtensions.cs b/src/messaging/dotnet/src/Core/MessageBufferJsonExtensions.cs index 184ab0230..7dcdeab49 100644 --- a/src/messaging/dotnet/src/Core/MessageBufferJsonExtensions.cs +++ b/src/messaging/dotnet/src/Core/MessageBufferJsonExtensions.cs @@ -37,16 +37,21 @@ public static class MessageBufferJsonExtensions /// /// Creates a from the provided value serialized to JSON. /// + /// /// /// /// /// - public static MessageBuffer CreateJson(T value, JsonSerializerOptions? options = null) + public static MessageBuffer CreateJson( + this MessageBuffer.MessageBufferFactory factory, + T value, + JsonSerializerOptions? options = null) { - using var stream = new RecyclableMemoryStream(RecyclableMemoryStreamManager); - JsonSerializer.Serialize(stream, value, options); - return MessageBuffer.Create(stream.GetReadOnlySequence()); + using var bufferWriter = MessageBuffer.GetBufferWriter(); + using var jsonWriter = new Utf8JsonWriter(bufferWriter); + JsonSerializer.Serialize(jsonWriter, value, options); + jsonWriter.Flush(); + + return MessageBuffer.Create(bufferWriter.WrittenMemory); } - - private static readonly RecyclableMemoryStreamManager RecyclableMemoryStreamManager = new(); -} +} \ No newline at end of file diff --git a/src/messaging/dotnet/src/Core/MessageRouterJsonExtensions.cs b/src/messaging/dotnet/src/Core/MessageRouterJsonExtensions.cs index 459bdb5ba..b29bf22b5 100644 --- a/src/messaging/dotnet/src/Core/MessageRouterJsonExtensions.cs +++ b/src/messaging/dotnet/src/Core/MessageRouterJsonExtensions.cs @@ -40,7 +40,7 @@ public static ValueTask PublishJsonAsync( { return messageRouter.PublishAsync( topic, - MessageBufferJsonExtensions.CreateJson(payload, jsonSerializerOptions), + MessageBuffer.Factory.CreateJson(payload, jsonSerializerOptions), publishOptions, cancellationToken); } diff --git a/src/messaging/dotnet/src/Core/MorganStanley.ComposeUI.Messaging.Core.csproj b/src/messaging/dotnet/src/Core/MorganStanley.ComposeUI.Messaging.Core.csproj index 331e341c1..8b4f6946d 100644 --- a/src/messaging/dotnet/src/Core/MorganStanley.ComposeUI.Messaging.Core.csproj +++ b/src/messaging/dotnet/src/Core/MorganStanley.ComposeUI.Messaging.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/src/messaging/dotnet/test/Core.Tests/MessageBuffer.Tests.cs b/src/messaging/dotnet/test/Core.Tests/MessageBuffer.Tests.cs index d66c4be2d..0d09a597b 100644 --- a/src/messaging/dotnet/test/Core.Tests/MessageBuffer.Tests.cs +++ b/src/messaging/dotnet/test/Core.Tests/MessageBuffer.Tests.cs @@ -11,6 +11,7 @@ // and limitations under the License. using System.Buffers; +using System.Buffers.Text; using System.Text; using MorganStanley.ComposeUI.Messaging.TestUtils; @@ -29,6 +30,15 @@ public void GetString_returns_the_original_string(string input) buffer.GetString().Should().Be(input); } + [Fact] + public void GetString_throws_if_disposed() + { + var buffer = MessageBuffer.Create("x"); + buffer.Dispose(); + + Assert.Throws(() => buffer.GetString()); + } + [Theory] [InlineData("")] [InlineData(StringConstants.LoremIpsum)] @@ -41,6 +51,15 @@ public void GetSpan_returns_the_original_string_encoded_as_UTF8(string input) span.ToArray().Should().Equal(Encoding.UTF8.GetBytes(input)); } + [Fact] + public void GetSpan_throws_if_disposed() + { + var buffer = MessageBuffer.Create("x"); + buffer.Dispose(); + + Assert.Throws(() => buffer.GetSpan()); + } + [Theory] [InlineData("")] [InlineData(StringConstants.LoremIpsum)] @@ -65,6 +84,58 @@ public void GetSpan_returns_the_original_UTF8_bytes(string input) span.ToArray().Should().Equal(bytes); } + [Fact] + public void TryGetBase64Bytes_gets_the_decoded_bytes_and_returns_true_when_the_data_is_valid_Base64() + { + var bytes = GetRandomBytes(100); + var base64String = Convert.ToBase64String(bytes); + var bufferWriter = new ArrayBufferWriter(); + var buffer = MessageBuffer.Create(base64String); + + buffer.TryGetBase64Bytes(out var decodedBytes).Should().BeTrue(); + buffer.TryGetBase64Bytes(bufferWriter).Should().BeTrue(); + decodedBytes.Should().Equal(bytes); + bufferWriter.WrittenMemory.ToArray().Should().Equal(bytes); + } + + [Fact] + public void TryGetBase64Bytes_throws_if_disposed() + { + var buffer = MessageBuffer.Create("x"); + buffer.Dispose(); + + Assert.Throws(() => buffer.TryGetBase64Bytes(out _)); + Assert.Throws(() => buffer.TryGetBase64Bytes(new ArrayBufferWriter())); + } + + [Fact] + public void GetBase64Bytes_throws_if_disposed() + { + var buffer = MessageBuffer.Create("x"); + buffer.Dispose(); + + Assert.Throws(() => buffer.GetBase64Bytes()); + Assert.Throws(() => buffer.GetBase64Bytes(new ArrayBufferWriter())); + } + + [Fact] + public void TryGetBase64Bytes_returns_false_if_the_data_is_not_valid_Base64() + { + var buffer = MessageBuffer.Create("****"); + + buffer.TryGetBase64Bytes(out _).Should().BeFalse(); + buffer.TryGetBase64Bytes(new ArrayBufferWriter()).Should().BeFalse(); + } + + [Fact] + public void GetBase64Bytes_throws_if_the_data_is_not_valid_Base64() + { + var buffer = MessageBuffer.Create("****"); + + Assert.Throws(() => buffer.GetBase64Bytes()); + Assert.Throws(() => buffer.GetBase64Bytes(new ArrayBufferWriter())); + } + [Fact] public void Create_throws_when_called_with_invalid_UTF8() { diff --git a/src/messaging/dotnet/test/Core.Tests/MessageBufferJsonExtensions.Tests.cs b/src/messaging/dotnet/test/Core.Tests/MessageBufferJsonExtensions.Tests.cs index 35cb9db89..12cf6bb3b 100644 --- a/src/messaging/dotnet/test/Core.Tests/MessageBufferJsonExtensions.Tests.cs +++ b/src/messaging/dotnet/test/Core.Tests/MessageBufferJsonExtensions.Tests.cs @@ -54,6 +54,20 @@ public void ReadJson_respects_the_provided_JsonSerializerOptions() }); } + [Fact] + public void CreateJson_creates_a_MessageBuffer_with_the_JSON_string() + { + var payload = new TestPayload + { + Name = "test-name", + Value = "test-value" + }; + + var buffer = MessageBuffer.Factory.CreateJson(payload); + + JsonSerializer.Deserialize(buffer.GetString()).Should().BeEquivalentTo(payload); + } + private class TestPayload { public string Name { get; set; }