Skip to content

Commit 66083ea

Browse files
committed
Use span-based APIs for event ID parsing
1 parent bcbf247 commit 66083ea

File tree

2 files changed

+78
-0
lines changed

2 files changed

+78
-0
lines changed

src/ModelContextProtocol.Core/Server/DistributedCacheEventIdFormatter.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
// This is a shared source file included in both ModelContextProtocol.Core and the test project.
55
// Do not reference symbols internal to the core project, as they won't be available in tests.
66

7+
#if NET
8+
using System.Buffers;
9+
using System.Buffers.Text;
10+
using System.Diagnostics.CodeAnalysis;
11+
12+
#endif
713
using System.Text;
814

915
namespace ModelContextProtocol.Server;
@@ -39,6 +45,34 @@ public static bool TryParse(string eventId, out string sessionId, out string str
3945
streamId = string.Empty;
4046
sequence = 0;
4147

48+
#if NET
49+
ReadOnlySpan<char> eventIdSpan = eventId.AsSpan();
50+
Span<Range> partRanges = stackalloc Range[4];
51+
int rangeCount = eventIdSpan.Split(partRanges, Separator);
52+
if (rangeCount != 3)
53+
{
54+
return false;
55+
}
56+
57+
try
58+
{
59+
ReadOnlySpan<char> sessionBase64 = eventIdSpan[partRanges[0]];
60+
ReadOnlySpan<char> streamBase64 = eventIdSpan[partRanges[1]];
61+
ReadOnlySpan<char> sequenceSpan = eventIdSpan[partRanges[2]];
62+
63+
if (!TryDecodeBase64ToString(sessionBase64, out sessionId!) ||
64+
!TryDecodeBase64ToString(streamBase64, out streamId!))
65+
{
66+
return false;
67+
}
68+
69+
return long.TryParse(sequenceSpan, out sequence);
70+
}
71+
catch
72+
{
73+
return false;
74+
}
75+
#else
4276
var parts = eventId.Split(Separator);
4377
if (parts.Length != 3)
4478
{
@@ -55,5 +89,30 @@ public static bool TryParse(string eventId, out string sessionId, out string str
5589
{
5690
return false;
5791
}
92+
#endif
93+
}
94+
95+
#if NET
96+
private static bool TryDecodeBase64ToString(ReadOnlySpan<char> base64Chars, [NotNullWhen(true)] out string? result)
97+
{
98+
// Use a single buffer: base64 chars are ASCII (1:1 with UTF8 bytes),
99+
// and decoded data is always smaller than encoded, so we can decode in-place.
100+
int bufferLength = base64Chars.Length;
101+
Span<byte> buffer = bufferLength <= 256
102+
? stackalloc byte[bufferLength]
103+
: new byte[bufferLength];
104+
105+
Encoding.UTF8.GetBytes(base64Chars, buffer);
106+
107+
OperationStatus status = Base64.DecodeFromUtf8InPlace(buffer, out int bytesWritten);
108+
if (status != OperationStatus.Done)
109+
{
110+
result = null;
111+
return false;
112+
}
113+
114+
result = Encoding.UTF8.GetString(buffer[..bytesWritten]);
115+
return true;
58116
}
117+
#endif
59118
}

tests/ModelContextProtocol.Tests/Server/DistributedCacheEventStreamStoreTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,25 @@ public void EventIdFormatter_TryParse_RoundTripsSuccessfully()
15231523
Assert.Equal(originalSequence, sequence);
15241524
}
15251525

1526+
[Fact]
1527+
public void EventIdFormatter_TryParse_HandlesEmptySessionAndStreamIds()
1528+
{
1529+
// Arrange
1530+
var originalSessionId = "";
1531+
var originalStreamId = "";
1532+
var originalSequence = 42L;
1533+
1534+
// Act
1535+
var eventId = DistributedCacheEventIdFormatter.Format(originalSessionId, originalStreamId, originalSequence);
1536+
var parsed = DistributedCacheEventIdFormatter.TryParse(eventId, out var sessionId, out var streamId, out var sequence);
1537+
1538+
// Assert
1539+
Assert.True(parsed);
1540+
Assert.Equal(originalSessionId, sessionId);
1541+
Assert.Equal(originalStreamId, streamId);
1542+
Assert.Equal(originalSequence, sequence);
1543+
}
1544+
15261545
[Fact]
15271546
public void EventIdFormatter_TryParse_HandlesSpecialCharactersInSessionId()
15281547
{

0 commit comments

Comments
 (0)