From 07cbe5d4a4fbbce89f0922511091deaa44f0d24e Mon Sep 17 00:00:00 2001 From: atheate Date: Thu, 29 Oct 2026 10:53:31 +0100 Subject: [PATCH] Fix #399 --- Directory.Packages.props | 1 + .../PsmSerializerTestFixture.cs | 93 +++++++++++++++++++ .../SysML2.NET.Serializer.Json.Tests.csproj | 1 + SysML2.NET.Serializer.Json/Serializer.cs | 45 ++++++--- 4 files changed, 125 insertions(+), 15 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c14a9d9d2..b584267de 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -48,6 +48,7 @@ + diff --git a/SysML2.NET.Serializer.Json.Tests/PsmSerializerTestFixture.cs b/SysML2.NET.Serializer.Json.Tests/PsmSerializerTestFixture.cs index 6eb249323..73ecaa569 100644 --- a/SysML2.NET.Serializer.Json.Tests/PsmSerializerTestFixture.cs +++ b/SysML2.NET.Serializer.Json.Tests/PsmSerializerTestFixture.cs @@ -12,8 +12,17 @@ namespace SysML2.NET.Serializer.Json.Tests using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Text; using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.TestHost; + using Microsoft.Extensions.Hosting; using NUnit.Framework; @@ -86,6 +95,7 @@ public void VerifySerializeResponse() { Assert.That(json, Does.Contain("\"created\":\"1976-08-20T00:00:00Z\"")); Assert.That(localJson, Does.Match("\"created\":\"[^\"]+[+-][0-9]{2}:[0-9]{2}\"")); + Assert.That(JsonDocument.Parse(localJson).RootElement.GetProperty("created").GetDateTime().Kind, Is.EqualTo(DateTimeKind.Local)); } @@ -288,6 +298,73 @@ public void VerifySerializeUnsupportedMode() Throws.TypeOf()); } + [Test] + public async Task VerifySerializeResponseAsync() + { + var project = new Project { Id = ProjectIdentifier, Name = "the name" }; + + await using var stream = new AsyncOnlyStream(); + + Assert.That(() => this.serializer.SerializeResponse(project, SerializationModeKind.JSON, false, stream, this.jsonWriterOptions), + Throws.TypeOf()); + + await this.serializer.SerializeResponseAsync(project, SerializationModeKind.JSON, false, stream, this.jsonWriterOptions, CancellationToken.None); + await this.serializer.SerializeResponseAsync([project], SerializationModeKind.JSON, false, stream, this.jsonWriterOptions, CancellationToken.None); + + Assert.That(stream.Length, Is.GreaterThan(0)); + } + + [Test] + public async Task SerializeResponseAsync_ToTestServerResponseBody_RoundTripsWithoutDataLoss() + { + IResponse[] responses = + [ + new Project + { + Id = ProjectIdentifier, + Alias = ["first", "second"], + Created = new DateTime(1976, 8, 20, 0, 0, 0, DateTimeKind.Utc), + DefaultBranch = BranchIdentifier, + Description = "the description", + Name = "the name" + }, + new Branch + { + Id = BranchIdentifier, + Created = new DateTime(1976, 8, 21, 0, 0, 0, DateTimeKind.Utc), + Head = DataVersionIdentifier, + Name = "the branch", + OwningProject = ProjectIdentifier, + ReferencedCommit = DataVersionIdentifier + } + ]; + + using var host = await new HostBuilder() + .ConfigureWebHost(webHost => webHost.UseTestServer().Configure(app => app.Run(context => context.Request.Path == "/sync" + ? Task.Run(() => this.serializer.SerializeResponse(responses, SerializationModeKind.JSON, false, context.Response.Body, this.jsonWriterOptions)) + : this.serializer.SerializeResponseAsync(responses, SerializationModeKind.JSON, false, context.Response.Body, this.jsonWriterOptions, context.RequestAborted)))) + .StartAsync(); + + using var client = host.GetTestClient(); + + await Assert.ThatAsync(() => client.GetAsync("/sync"), Throws.TypeOf()); + + using var response = await client.GetAsync("/"); + await using var body = await response.Content.ReadAsStreamAsync(); + + var roundTripped = (await new DeSerializer().DeSerializeResponsesAsync(body, SerializationModeKind.JSON, false, CancellationToken.None)).ToList(); + + var expected = Serialize(stream => this.serializer.SerializeResponse(responses, SerializationModeKind.JSON, false, stream, this.jsonWriterOptions)); + var actual = Serialize(stream => this.serializer.SerializeResponse(roundTripped, SerializationModeKind.JSON, false, stream, this.jsonWriterOptions)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(response.IsSuccessStatusCode, Is.True); + Assert.That(roundTripped, Has.Count.EqualTo(2)); + Assert.That(actual, Is.EqualTo(expected)); + } + } + private static string Serialize(Action serialize) { using var stream = new MemoryStream(); @@ -296,5 +373,21 @@ private static string Serialize(Action serialize) return Encoding.UTF8.GetString(stream.ToArray()); } + + /// + /// A that rejects synchronous writes, as an ASP.NET Core response body does. + /// + private sealed class AsyncOnlyStream : MemoryStream + { + public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); + + public override void Write(ReadOnlySpan buffer) => throw new InvalidOperationException(); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + base.Write(buffer.ToArray(), 0, buffer.Length); + return ValueTask.CompletedTask; + } + } } } diff --git a/SysML2.NET.Serializer.Json.Tests/SysML2.NET.Serializer.Json.Tests.csproj b/SysML2.NET.Serializer.Json.Tests/SysML2.NET.Serializer.Json.Tests.csproj index a8e8f8224..19d1b0daf 100644 --- a/SysML2.NET.Serializer.Json.Tests/SysML2.NET.Serializer.Json.Tests.csproj +++ b/SysML2.NET.Serializer.Json.Tests/SysML2.NET.Serializer.Json.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/SysML2.NET.Serializer.Json/Serializer.cs b/SysML2.NET.Serializer.Json/Serializer.cs index 153c6babb..7356e4e26 100644 --- a/SysML2.NET.Serializer.Json/Serializer.cs +++ b/SysML2.NET.Serializer.Json/Serializer.cs @@ -48,7 +48,8 @@ public class Serializer : ISerializer /// /// The to use /// - public void Serialize(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions) + public void Serialize(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions) { using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); @@ -132,7 +133,8 @@ public void Serialize(IIdentified dataItem, SerializationModeKind serializationM /// /// The used to cancel the operation /// - public async Task SerializeAsync(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) + public async Task SerializeAsync(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) { await using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); @@ -180,7 +182,8 @@ public async Task SerializeAsync(IEnumerable dataItems, Serializati /// /// The used to cancel the operation /// - public async Task SerializeAsync(IIdentified dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) + public async Task SerializeAsync(IIdentified dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) { await using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); @@ -206,11 +209,14 @@ public async Task SerializeAsync(IIdentified dataItem, SerializationModeKind ser /// Asserts that derived properties should also be part of the serialization. /// The target . /// The to use. - public void SerializeRequest(IRequest dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions) + public void SerializeRequest(IRequest dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions) { using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); WriteSingle(dataItem, writer, serializationModeKind, includeDerivedProperties); + + writer.Flush(); } /// @@ -221,11 +227,14 @@ public void SerializeRequest(IRequest dataItem, SerializationModeKind serializat /// Asserts that derived properties should also be part of the serialization. /// The target . /// The to use. - public void SerializeRequest(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions) + public void SerializeRequest(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions) { using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); WriteMany(dataItems, writer, serializationModeKind, includeDerivedProperties); + + writer.Flush(); } /// @@ -236,11 +245,14 @@ public void SerializeRequest(IEnumerable dataItems, SerializationModeK /// Asserts that derived properties should also be part of the serialization. /// The target . /// The to use. - public void SerializeResponse(IResponse dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions) + public void SerializeResponse(IResponse dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions) { using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); WriteSingle(dataItem, writer, serializationModeKind, includeDerivedProperties); + + writer.Flush(); } /// @@ -251,11 +263,14 @@ public void SerializeResponse(IResponse dataItem, SerializationModeKind serializ /// Asserts that derived properties should also be part of the serialization. /// The target . /// The to use. - public void SerializeResponse(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions) + public void SerializeResponse(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions) { using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); WriteMany(dataItems, writer, serializationModeKind, includeDerivedProperties); + + writer.Flush(); } /// @@ -268,7 +283,8 @@ public void SerializeResponse(IEnumerable dataItems, SerializationMod /// The to use. /// The used to cancel the operation. /// an awaitable . - public async Task SerializeRequestAsync(IRequest dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) + public async Task SerializeRequestAsync(IRequest dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) { await using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); @@ -287,7 +303,8 @@ public async Task SerializeRequestAsync(IRequest dataItem, SerializationModeKind /// The to use. /// The used to cancel the operation. /// an awaitable . - public async Task SerializeRequestAsync(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) + public async Task SerializeRequestAsync(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) { await using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); @@ -306,7 +323,8 @@ public async Task SerializeRequestAsync(IEnumerable dataItems, Seriali /// The to use. /// The used to cancel the operation. /// an awaitable . - public async Task SerializeResponseAsync(IResponse dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) + public async Task SerializeResponseAsync(IResponse dataItem, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) { await using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); @@ -325,7 +343,8 @@ public async Task SerializeResponseAsync(IResponse dataItem, SerializationModeKi /// The to use. /// The used to cancel the operation. /// an awaitable . - public async Task SerializeResponseAsync(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) + public async Task SerializeResponseAsync(IEnumerable dataItems, SerializationModeKind serializationModeKind, bool includeDerivedProperties, Stream stream, + JsonWriterOptions jsonWriterOptions, CancellationToken cancellationToken) { await using var writer = new Utf8JsonWriter(stream, jsonWriterOptions); @@ -346,8 +365,6 @@ private static void WriteSingle(object dataItem, Utf8JsonWriter writer, Serializ var serializationAction = PsmSerializationProvider.Provide(dataItem.GetType()); serializationAction(dataItem, writer, serializationModeKind, includeDerivedProperties); - - writer.Flush(); } /// @@ -369,8 +386,6 @@ private static void WriteMany(IEnumerable dataItems, Utf8JsonWriter writ } writer.WriteEndArray(); - - writer.Flush(); } } }