diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
index 1ba14db..86e804b 100644
--- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs
@@ -1069,6 +1069,125 @@ public abstract class SignalRClientToHubTestBase
}
}
+ ///
+ /// 250KB+ payload with ASCII-only strings.
+ /// Validates BWO owned-buffer path with many Grow cycles.
+ ///
+ [TestMethod]
+ public async Task RoundTrip_VeryLargeOrderList_250KB_PreservesAllData()
+ {
+ TestDataFactory.ResetIdCounter();
+ var orders = new List();
+ for (var i = 0; i < 100; i++)
+ {
+ var tag = TestDataFactory.CreateTag($"Tag_{i}");
+ var user = TestDataFactory.CreateUser($"user_{i}");
+ orders.Add(TestDataFactory.CreateOrder(
+ itemCount: 5,
+ palletsPerItem: 3,
+ measurementsPerPallet: 3,
+ pointsPerMeasurement: 5,
+ sharedTag: tag,
+ sharedUser: user));
+ }
+
+ var result = await _client.PostDataAsync, List>(
+ TestSignalRTags.TestOrderListParam, orders);
+
+ Assert.IsNotNull(result, "Deserialization returned null — likely BWO GetTotalPosition drift on large payload");
+ Assert.AreEqual(orders.Count, result.Count, "Order count mismatch");
+ for (var i = 0; i < orders.Count; i++)
+ {
+ Assert.AreEqual(orders[i].Id, result[i].Id, $"Order[{i}].Id mismatch");
+ Assert.AreEqual(orders[i].Items.Count, result[i].Items.Count, $"Order[{i}].Items.Count mismatch");
+ }
+ }
+
+ ///
+ /// Large payload with non-ASCII (Hungarian) strings — targets the WriteStringUtf8
+ /// savedPosition bug that causes BWO GetTotalPosition drift when a non-ASCII string
+ /// straddles a chunk boundary and Grow resets _position.
+ ///
+ /// Production evidence: Array=44111, BWO_pipe=56340, BWO_temp=44111
+ /// The bug only manifests when TryGetArray fails (MemoryManager-backed Memory)
+ /// AND non-ASCII strings trigger the UTF-8 fallback with savedPosition rewind.
+ ///
+ [TestMethod]
+ public async Task RoundTrip_LargeOrderList_NonAsciiStrings_PreservesAllData()
+ {
+ // Non-ASCII strings that trigger UTF-8 fallback in WriteStringUtf8
+ var hungarianNames = new[]
+ {
+ "Kürtőskalács Értékesítő Kft.",
+ "Széchenyi István Tér",
+ "Büfé és Étkezde Zrt.",
+ "Különleges Árú Raktár",
+ "Möbius Szállítmányozás",
+ "Tündérkert Óvoda és Bölcsőde",
+ "Győri Főpályaudvar",
+ "Hősök Tere Múzeum",
+ "Péterfy Sándor Utcai Kórház",
+ "Közgondnokság Ügyfélfogadó"
+ };
+
+ TestDataFactory.ResetIdCounter();
+ var orders = new List();
+ for (var i = 0; i < 80; i++)
+ {
+ var tag = TestDataFactory.CreateTag($"Cég_{i}_{hungarianNames[i % hungarianNames.Length]}");
+ var user = TestDataFactory.CreateUser($"felhasználó_{i}");
+ user.FirstName = hungarianNames[i % hungarianNames.Length];
+ user.LastName = hungarianNames[(i + 3) % hungarianNames.Length];
+ user.Email = $"felhasználó_{i}@székesfehérvár.hu";
+ user.Username = $"ügyfél_{hungarianNames[(i + 5) % hungarianNames.Length]}";
+
+ var order = TestDataFactory.CreateOrder(
+ itemCount: 4,
+ palletsPerItem: 3,
+ measurementsPerPallet: 3,
+ pointsPerMeasurement: 4,
+ sharedTag: tag,
+ sharedUser: user);
+
+ // Inject non-ASCII product names to maximize chunk-boundary collisions
+ foreach (var item in order.Items)
+ {
+ item.ProductName = $"Termék_{hungarianNames[(i + item.Id) % hungarianNames.Length]}_{i}";
+ }
+
+ order.OrderNumber = $"RND-{i:D4}-{hungarianNames[i % hungarianNames.Length]}";
+ orders.Add(order);
+ }
+
+ var result = await _client.PostDataAsync, List>(
+ TestSignalRTags.TestOrderListParam, orders);
+
+ Assert.IsNotNull(result,
+ "Deserialization returned null — BWO savedPosition drift on non-ASCII strings at chunk boundary");
+ Assert.AreEqual(orders.Count, result.Count, "Order count mismatch");
+ for (var i = 0; i < orders.Count; i++)
+ {
+ Assert.AreEqual(orders[i].Id, result[i].Id, $"Order[{i}].Id mismatch");
+ Assert.AreEqual(orders[i].OrderNumber, result[i].OrderNumber, $"Order[{i}].OrderNumber mismatch");
+ Assert.AreEqual(orders[i].Items.Count, result[i].Items.Count, $"Order[{i}].Items.Count mismatch");
+ for (var j = 0; j < orders[i].Items.Count; j++)
+ {
+ Assert.AreEqual(orders[i].Items[j].ProductName, result[i].Items[j].ProductName,
+ $"Order[{i}].Items[{j}].ProductName mismatch — non-ASCII string corrupted");
+ }
+ }
+ }
+
+ ///
+ /// Sanity check: SlabTransportWriter.GetMemory returns MemoryManager-backed Memory
+ /// so TryGetArray fails, forcing BWO into the owned-buffer code path.
+ ///
+ [TestMethod]
+ public void SlabTransportWriter_GetMemory_TryGetArrayFails()
+ {
+ SlabTransportWriter.VerifyMemoryManagerBacked();
+ }
+
#endregion
}
diff --git a/AyCode.Services.Server.Tests/SignalRs/SlabTransportWriter.cs b/AyCode.Services.Server.Tests/SignalRs/SlabTransportWriter.cs
new file mode 100644
index 0000000..f7a85c6
--- /dev/null
+++ b/AyCode.Services.Server.Tests/SignalRs/SlabTransportWriter.cs
@@ -0,0 +1,215 @@
+using System.Buffers;
+using System.Runtime.InteropServices;
+
+namespace AyCode.Services.Server.Tests.SignalRs;
+
+///
+/// Custom IBufferWriter that simulates Kestrel's PinnedBlockMemoryPool transport behavior:
+///
+/// Production: Kestrel PipeWriter → PinnedBlockMemoryPool → MemoryManager-backed Memory
+/// → TryGetArray FAILS → BWO takes owned-buffer fallback path
+///
+/// Test: SlabTransportWriter → SlabMemoryManager-backed Memory
+/// → TryGetArray FAILS → BWO takes owned-buffer fallback path (same as production)
+///
+/// Key behaviors:
+/// - GetMemory returns MemoryManager-backed Memory (TryGetArray always fails)
+/// - Fixed-size slab segments with random offsets (simulates slab allocator)
+/// - After Advance, remaining slab space is reused (offset grows)
+/// - GetMemory may return less than sizeHint (remaining slab space)
+/// - Deterministic via seeded Random for reproducible tests
+///
+internal sealed class SlabTransportWriter : IBufferWriter
+{
+ private readonly int _slabSize;
+ private readonly Random _rng;
+
+ // Committed data tracking
+ private readonly List _segments = new();
+ private int _totalCommitted;
+
+ // Current slab state
+ private byte[]? _currentSlab;
+ private int _writePos; // current write position within slab
+ private int _slabEnd; // end of usable area in slab
+
+ public SlabTransportWriter(int slabSize = 256, int seed = 42)
+ {
+ _slabSize = slabSize;
+ _rng = new Random(seed);
+ }
+
+ /// Total bytes committed via Advance.
+ public int WrittenCount => _totalCommitted;
+
+ /// Number of slab segments allocated.
+ public int SlabCount { get; private set; }
+
+ public void Advance(int count)
+ {
+ if (_currentSlab == null)
+ throw new InvalidOperationException("Call GetMemory/GetSpan before Advance");
+ if (count < 0 || _writePos + count > _slabEnd)
+ throw new InvalidOperationException(
+ $"Advance({count}) invalid: writePos={_writePos}, slabEnd={_slabEnd}, remaining={_slabEnd - _writePos}");
+
+ _segments.Add(new CommittedSegment(_currentSlab, _writePos, count));
+ _writePos += count;
+ _totalCommitted += count;
+ }
+
+ ///
+ /// Returns MemoryManager-backed Memory so that TryGetArray ALWAYS fails.
+ /// May return fewer bytes than sizeHint (remaining slab space) — legal per IBufferWriter contract.
+ /// This forces BWO to rent from ArrayPool (owned-buffer path), matching production behavior.
+ ///
+ public Memory GetMemory(int sizeHint = 0)
+ {
+ sizeHint = Math.Max(1, sizeHint);
+ var remaining = _currentSlab != null ? _slabEnd - _writePos : 0;
+
+ if (remaining <= 0)
+ {
+ // Allocate new slab — at least sizeHint to avoid starving the caller
+ AllocateNewSlab(Math.Max(sizeHint, _slabSize));
+ remaining = _slabEnd - _writePos;
+ }
+
+ // Return MemoryManager-backed Memory: TryGetArray will fail
+ return new SlabMemoryManager(_currentSlab!, _writePos, remaining).Memory;
+ }
+
+ ///
+ /// Returns Span with at least sizeHint bytes.
+ /// Used by FlushOwnedBuffer: _writer.GetSpan(bytesInChunk) must be large enough for CopyTo.
+ /// Allocates new slab if remaining space is insufficient.
+ ///
+ public Span GetSpan(int sizeHint = 0)
+ {
+ sizeHint = Math.Max(1, sizeHint);
+ var remaining = _currentSlab != null ? _slabEnd - _writePos : 0;
+
+ if (remaining < sizeHint)
+ {
+ AllocateNewSlab(Math.Max(sizeHint, _slabSize));
+ remaining = _slabEnd - _writePos;
+ }
+
+ return _currentSlab.AsSpan(_writePos, remaining);
+ }
+
+ private void AllocateNewSlab(int minSize)
+ {
+ // Random offset within slab — simulates Kestrel slab allocator non-zero offsets
+ var offset = _rng.Next(0, Math.Max(1, _slabSize / 4));
+ // ±12% size jitter for variety
+ var jitter = _rng.Next(-_slabSize / 8, _slabSize / 8 + 1);
+ var actualSize = Math.Max(minSize, _slabSize + jitter);
+ _currentSlab = new byte[actualSize + offset];
+ _writePos = offset;
+ _slabEnd = offset + actualSize;
+ SlabCount++;
+ }
+
+ ///
+ /// Get all committed bytes as a contiguous array.
+ ///
+ public byte[] ToArray()
+ {
+ var result = new byte[_totalCommitted];
+ var pos = 0;
+ foreach (var seg in _segments)
+ {
+ Buffer.BlockCopy(seg.Array, seg.Offset, result, pos, seg.Length);
+ pos += seg.Length;
+ }
+ return result;
+ }
+
+ ///
+ /// Build a multi-segment ReadOnlySequence from committed data, splitting at slab boundaries.
+ /// Each committed segment becomes a separate ReadOnlySequence segment.
+ ///
+ public ReadOnlySequence ToReadOnlySequence()
+ {
+ if (_segments.Count == 0)
+ return ReadOnlySequence.Empty;
+
+ if (_segments.Count == 1)
+ {
+ var seg = _segments[0];
+ return new ReadOnlySequence(seg.Array, seg.Offset, seg.Length);
+ }
+
+ // Build linked segment list
+ var first = new MemorySegment(new ReadOnlyMemory(_segments[0].Array, _segments[0].Offset, _segments[0].Length));
+ var current = first;
+ for (var i = 1; i < _segments.Count; i++)
+ {
+ var seg = _segments[i];
+ current = current.Append(new ReadOnlyMemory(seg.Array, seg.Offset, seg.Length));
+ }
+
+ return new ReadOnlySequence(first, 0, current, current.Memory.Length);
+ }
+
+ ///
+ /// Verify that TryGetArray fails on our Memory (sanity check for test correctness).
+ ///
+ public static void VerifyMemoryManagerBacked()
+ {
+ var writer = new SlabTransportWriter(64);
+ var mem = writer.GetMemory(16);
+ if (MemoryMarshal.TryGetArray(mem, out ArraySegment _))
+ throw new InvalidOperationException(
+ "SlabTransportWriter.GetMemory returned array-backed Memory — TryGetArray should fail!");
+ }
+
+ private record struct CommittedSegment(byte[] Array, int Offset, int Length);
+
+ ///
+ /// MemoryManager that wraps an array region but returns Memory where TryGetArray fails.
+ /// This is the key difference from production Kestrel PinnedBlockMemoryPool.
+ ///
+ private sealed class SlabMemoryManager : MemoryManager
+ {
+ private readonly byte[] _array;
+ private readonly int _offset;
+ private readonly int _length;
+
+ public SlabMemoryManager(byte[] array, int offset, int length)
+ {
+ _array = array;
+ _offset = offset;
+ _length = length;
+ }
+
+ public override Span GetSpan() => _array.AsSpan(_offset, _length);
+
+ public override MemoryHandle Pin(int elementIndex = 0)
+ => throw new NotSupportedException("SlabMemoryManager does not support pinning");
+
+ public override void Unpin()
+ => throw new NotSupportedException("SlabMemoryManager does not support pinning");
+
+ protected override void Dispose(bool disposing) { }
+ }
+
+ ///
+ /// ReadOnlySequenceSegment for building multi-segment sequences from committed data.
+ ///
+ private sealed class MemorySegment : ReadOnlySequenceSegment
+ {
+ public MemorySegment(ReadOnlyMemory memory)
+ {
+ Memory = memory;
+ }
+
+ public MemorySegment Append(ReadOnlyMemory memory)
+ {
+ var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length };
+ Next = next;
+ return next;
+ }
+ }
+}
diff --git a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
index 446e507..e0dc280 100644
--- a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
@@ -8,13 +8,19 @@ using Microsoft.AspNetCore.SignalR.Protocol;
namespace AyCode.Services.Server.Tests.SignalRs;
///
-/// Test protocol that simulates production Kestrel pipe behavior with 256-byte segments.
+/// Test protocol that simulates production Kestrel transport behavior:
///
-/// Production: SignalR → WriteMessage(PipeWriter) → Kestrel slab 4096-byte segments → PipeReader → TryParseMessage
-/// Test: SignalR → WriteMessage(PipeWriter) → FixedSizePool 256-byte segments → PipeReader → TryParseMessage
+/// WRITE SIDE (GetMessageBytesMultiSegment):
+/// WriteMessage → SlabTransportWriter (MemoryManager-backed, TryGetArray fails)
+/// → BWO takes owned-buffer fallback path (same as production Kestrel PipeWriter)
+/// → Automatic byte-by-byte comparison against ArrayBufferWriter reference
///
-/// Both sides go through a real Pipe with fixed-size memory segments,
-/// exercising BWO chunk writes and SequenceBinaryInput cross-boundary reads.
+/// READ SIDE (TryParseMessage):
+/// Bytes → Pipe(SlabSimulatingPool, 256B segments) → multi-segment ReadOnlySequence
+/// → SequenceBinaryInput cross-boundary reads (same as production Kestrel PipeReader)
+///
+/// Production: SignalR → WriteMessage(PipeWriter/Kestrel) → 4096B slab segments → TryParseMessage
+/// Test: SignalR → WriteMessage(SlabTransportWriter) → 256B slab segments → TryParseMessage
///
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
{
@@ -26,23 +32,35 @@ internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
}
///
- /// Write side: WriteMessage → PipeWriter backed by 256-byte pool segments.
- /// Same code path as production, just smaller segments.
+ /// Write side: WriteMessage → SlabTransportWriter (MemoryManager-backed Memory).
+ /// Forces BWO into owned-buffer fallback path — same code path as production Kestrel.
+ /// Validates output against ArrayBufferWriter reference on every call.
///
- public new ReadOnlyMemory GetMessageBytes(HubMessage message)
+ public ReadOnlyMemory GetMessageBytesMultiSegment(HubMessage message)
{
- var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize)));
- WriteMessage(message, pipe.Writer);
- pipe.Writer.Complete();
- pipe.Reader.TryRead(out var result);
- var bytes = result.Buffer.ToArray();
- pipe.Reader.Complete();
- return bytes;
+ // ── Transport-double path (production simulation) ──────────────────
+ var transport = new SlabTransportWriter(SegmentSize);
+ WriteMessage(message, transport);
+ var transportBytes = transport.ToArray();
+
+ // ── Reference path: ArrayBufferWriter with large capacity (no resize → lengthSpan stays valid) ──
+ // NOTE: base.GetMessageBytes uses capacity = chunkSize + 4 = 260, which causes resize
+ // on any non-trivial message → invalidates the back-patched lengthSpan.
+ // We use a large capacity to ensure no resize ever happens.
+ var referenceCapacity = Math.Max(transportBytes.Length + 256, 65536);
+ var reference = new ArrayBufferWriter(referenceCapacity);
+ WriteMessage(message, reference);
+ var referenceBytes = reference.WrittenSpan;
+
+ // ── Validate: transport output must match reference byte-for-byte ──
+ ValidateAgainstReference(transportBytes, referenceBytes);
+
+ return transportBytes;
}
///
- /// Read side: fill PipeWriter 256 bytes at a time → PipeReader gives multi-segment sequence.
- /// Same as production Kestrel PipeReader delivering 4096-byte segments.
+ /// Read side: fill Pipe with 256-byte slab segments → multi-segment ReadOnlySequence.
+ /// Same as production Kestrel PipeReader delivering slab-sized segments.
///
public override bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder,
[NotNullWhen(true)] out HubMessage? message)
@@ -65,11 +83,98 @@ internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
writer.Complete();
pipe.Reader.TryRead(out var result);
var seq = result.Buffer;
+
+ // Assert multi-segment: if payload > SegmentSize, sequence must NOT be single-segment
+ if (bytes.Length > SegmentSize)
+ {
+ AssertMultiSegment(seq, bytes.Length);
+ }
+
var success = base.TryParseMessage(ref seq, binder, out message);
pipe.Reader.Complete();
return success;
}
+ #region Validation
+
+ ///
+ /// Byte-by-byte comparison of transport-double output vs ArrayBufferWriter reference.
+ /// Catches any BWO owned-buffer path bug (position drift, data corruption, length mismatch).
+ ///
+ private static void ValidateAgainstReference(byte[] transportBytes, ReadOnlySpan referenceBytes)
+ {
+ var refArray = referenceBytes.ToArray();
+
+ // ── Total byte count ──
+ if (transportBytes.Length != refArray.Length)
+ {
+ // Also check outer length prefix for diagnostics
+ var transportPrefix = transportBytes.Length >= 4 ? BitConverter.ToInt32(transportBytes, 0) : -1;
+ var referencePrefix = refArray.Length >= 4 ? BitConverter.ToInt32(refArray, 0) : -1;
+
+ throw new InvalidOperationException(
+ $"[TRANSPORT_DOUBLE] Total byte count mismatch: transport={transportBytes.Length}, reference={refArray.Length}. " +
+ $"Diff={transportBytes.Length - refArray.Length} bytes. " +
+ $"Outer length prefix: transport={transportPrefix}, reference={referencePrefix}. " +
+ $"BWO GetTotalPosition drift detected.");
+ }
+
+ // ── Outer length prefix check (first 4 bytes) ──
+ if (transportBytes.Length >= 4)
+ {
+ var transportPrefix = BitConverter.ToInt32(transportBytes, 0);
+ var referencePrefix = BitConverter.ToInt32(refArray, 0);
+ if (transportPrefix != referencePrefix)
+ {
+ throw new InvalidOperationException(
+ $"[TRANSPORT_DOUBLE] Outer length prefix mismatch: transport={transportPrefix}, reference={referencePrefix}. " +
+ $"Total bytes match ({transportBytes.Length}) but prefix differs. " +
+ $"BWO owned-buffer path likely has position drift.");
+ }
+ }
+
+ // ── Byte-by-byte content ──
+ var minLen = Math.Min(transportBytes.Length, refArray.Length);
+ for (int i = 0; i < minLen; i++)
+ {
+ if (transportBytes[i] != refArray[i])
+ {
+ var start = Math.Max(0, i - 8);
+ var end = Math.Min(minLen, i + 16);
+ var refHex = Convert.ToHexString(refArray.AsSpan(start, end - start));
+ var trnHex = Convert.ToHexString(transportBytes.AsSpan(start, end - start));
+ throw new InvalidOperationException(
+ $"[TRANSPORT_DOUBLE] Content mismatch at byte {i}/{minLen}: " +
+ $"ref={refHex} transport={trnHex}. " +
+ $"BWO owned-buffer path writing corrupt data.");
+ }
+ }
+ }
+
+ private static void AssertMultiSegment(ReadOnlySequence seq, int totalLength)
+ {
+ if (seq.IsSingleSegment)
+ {
+ throw new InvalidOperationException(
+ $"[MULTI_SEGMENT] Expected multi-segment sequence for {totalLength} bytes " +
+ $"(> {SegmentSize}B segment size), but got single segment. " +
+ $"SlabSimulatingPool or Pipe configuration is wrong.");
+ }
+
+ // Count segments
+ var segmentCount = 0;
+ foreach (var _ in seq)
+ segmentCount++;
+
+ if (segmentCount < 2)
+ {
+ throw new InvalidOperationException(
+ $"[MULTI_SEGMENT] Expected >= 2 segments for {totalLength} bytes, got {segmentCount}.");
+ }
+ }
+
+ #endregion
+
///
/// MemoryPool that returns -byte blocks at random offsets
/// within a larger backing array — simulating Kestrel's slab allocator where segments
@@ -85,8 +190,8 @@ internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
{
var size = Math.Max(minBufferSize, segmentSize);
var offset = _rng.Next(0, segmentSize); // random slab offset
- var jitter = _rng.Next(-1, 2); // -1, 0, or +1
- var actualSize = Math.Max(1, size + jitter); // random segment size variance
+ var jitter = _rng.Next(-segmentSize / 4, segmentSize / 4 + 1); // ±25% size variance
+ var actualSize = Math.Max(16, size + jitter);
var array = new byte[actualSize + offset];
return new Owner(array, offset, actualSize);
}
diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs
index c6fb215..d0264e0 100644
--- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRClient2.cs
@@ -65,7 +65,7 @@ public class TestableSignalRClient2 : AcSignalRClientBase, IAcSignalRHubItemServ
nameof(IAcSignalRHubClient.OnReceiveMessage),
[messageTag, requestId, signalParams, data ?? Array.Empty()]);
- var bytes = _protocol.GetMessageBytes(invocation);
+ var bytes = _protocol.GetMessageBytesMultiSegment(invocation);
var sequence = new ReadOnlySequence(bytes);
if (!_protocol.TryParseMessage(ref sequence, _binder, out var parsed) || parsed is not InvocationMessage invMsg)
throw new InvalidOperationException(
diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs
index db0809e..04482e6 100644
--- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs
@@ -122,7 +122,7 @@ public class TestableSignalRHub2 : AcWebSignalRHubBase()]);
- var bytes = _protocol.GetMessageBytes(invocation);
+ var bytes = _protocol.GetMessageBytesMultiSegment(invocation);
var sequence = new ReadOnlySequence(bytes);
_protocol.TryParseMessage(ref sequence, _binder, out var parsed);