Use ReadOnlyMemory<byte> for binary data to eliminate UTF-16 transcoding (#1070)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> Co-authored-by: Eric StJohn <ericstj@microsoft.com>
This commit is contained in:
parent
a3ce9e9606
commit
80b1ad21d7
4 changed files with 400 additions and 103 deletions
|
|
@ -1,4 +1,7 @@
|
|||
using System.Buffers;
|
||||
using System.Buffers.Text;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
|
@ -9,7 +12,7 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="BlobResourceContents"/> is used when binary data needs to be exchanged through
|
||||
/// the Model Context Protocol. The binary data is represented as a base64-encoded string
|
||||
/// the Model Context Protocol. The binary data is represented as base64-encoded UTF-8 bytes
|
||||
/// in the <see cref="Blob"/> property.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
@ -24,18 +27,76 @@ namespace ModelContextProtocol.Protocol;
|
|||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public sealed class BlobResourceContents : ResourceContents
|
||||
{
|
||||
private ReadOnlyMemory<byte>? _decodedData;
|
||||
private ReadOnlyMemory<byte> _blob;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base64-encoded string representing the binary data of the item.
|
||||
/// Creates an <see cref="BlobResourceContents"/> from raw data.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The raw unencoded data.</param>
|
||||
/// <param name="uri">The URI of the blob resource.</param>
|
||||
/// <param name="mimeType">The optional MIME type of the data.</param>
|
||||
/// <returns>A new <see cref="BlobResourceContents"/> instance.</returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static BlobResourceContents FromBytes(ReadOnlyMemory<byte> bytes, string uri, string? mimeType = null)
|
||||
{
|
||||
ReadOnlyMemory<byte> blob = EncodingUtilities.EncodeToBase64Utf8(bytes);
|
||||
|
||||
return new()
|
||||
{
|
||||
_decodedData = bytes,
|
||||
Blob = blob,
|
||||
MimeType = mimeType,
|
||||
Uri = uri
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base64-encoded UTF-8 bytes representing the binary data of the item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting this value will invalidate any cached value of <see cref="DecodedData"/>.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("blob")]
|
||||
public required string Blob { get; set; }
|
||||
public required ReadOnlyMemory<byte> Blob
|
||||
{
|
||||
get => _blob;
|
||||
set
|
||||
{
|
||||
_blob = value;
|
||||
_decodedData = null; // Invalidate cache
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the decoded data represented by <see cref="Blob"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When getting, this member will decode the value in <see cref="Blob"/> and cache the result.
|
||||
/// Subsequent accesses return the cached value unless <see cref="Blob"/> is modified.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ReadOnlyMemory<byte> DecodedData
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_decodedData is null)
|
||||
{
|
||||
_decodedData = EncodingUtilities.DecodeFromBase64Utf8(Blob);
|
||||
}
|
||||
|
||||
return _decodedData.Value;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private string DebuggerDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
string lengthDisplay = DebuggerDisplayHelper.GetBase64LengthDisplay(Blob);
|
||||
string lengthDisplay = _decodedData is null ? DebuggerDisplayHelper.GetBase64LengthDisplay(Blob) : $"{DecodedData.Length} bytes";
|
||||
string mimeInfo = MimeType is not null ? $", MimeType = {MimeType}" : "";
|
||||
return $"Uri = \"{Uri}\"{mimeInfo}, Length = {lengthDisplay}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Buffers;
|
||||
using System.Buffers.Text;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
|
@ -87,7 +90,7 @@ public abstract class ContentBlock
|
|||
string? type = null;
|
||||
string? text = null;
|
||||
string? name = null;
|
||||
string? data = null;
|
||||
ReadOnlyMemory<byte>? data = null;
|
||||
string? mimeType = null;
|
||||
string? uri = null;
|
||||
string? description = null;
|
||||
|
|
@ -128,7 +131,7 @@ public abstract class ContentBlock
|
|||
break;
|
||||
|
||||
case "data":
|
||||
data = reader.GetString();
|
||||
data = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
|
||||
break;
|
||||
|
||||
case "mimeType":
|
||||
|
|
@ -279,12 +282,12 @@ public abstract class ContentBlock
|
|||
break;
|
||||
|
||||
case ImageContentBlock imageContent:
|
||||
writer.WriteString("data", imageContent.Data);
|
||||
writer.WriteString("data", imageContent.Data.Span);
|
||||
writer.WriteString("mimeType", imageContent.MimeType);
|
||||
break;
|
||||
|
||||
case AudioContentBlock audioContent:
|
||||
writer.WriteString("data", audioContent.Data);
|
||||
writer.WriteString("data", audioContent.Data.Span);
|
||||
writer.WriteString("mimeType", audioContent.MimeType);
|
||||
break;
|
||||
|
||||
|
|
@ -376,14 +379,73 @@ public sealed class TextContentBlock : ContentBlock
|
|||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public sealed class ImageContentBlock : ContentBlock
|
||||
{
|
||||
private ReadOnlyMemory<byte>? _decodedData;
|
||||
private ReadOnlyMemory<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ImageContentBlock"/> from decoded image bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The unencoded image bytes.</param>
|
||||
/// <param name="mimeType">The MIME type of the image.</param>
|
||||
/// <returns>A new <see cref="ImageContentBlock"/> instance.</returns>
|
||||
/// <remarks>
|
||||
/// This method stores the provided bytes as <see cref="DecodedData"/> and encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static ImageContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mimeType)
|
||||
{
|
||||
ReadOnlyMemory<byte> data = EncodingUtilities.EncodeToBase64Utf8(bytes);
|
||||
|
||||
return new()
|
||||
{
|
||||
_decodedData = bytes,
|
||||
Data = data,
|
||||
MimeType = mimeType
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Type => "image";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base64-encoded image data.
|
||||
/// Gets or sets the base64-encoded UTF-8 bytes representing the image data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting this value will invalidate any cached value of <see cref="DecodedData"/>.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("data")]
|
||||
public required string Data { get; set; }
|
||||
public required ReadOnlyMemory<byte> Data
|
||||
{
|
||||
get => _data;
|
||||
set
|
||||
{
|
||||
_data = value;
|
||||
_decodedData = null; // Invalidate cache
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the decoded image data represented by <see cref="Data"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When getting, this member will decode the value in <see cref="Data"/> and cache the result.
|
||||
/// Subsequent accesses return the cached value unless <see cref="Data"/> is modified.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ReadOnlyMemory<byte> DecodedData
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_decodedData is null)
|
||||
{
|
||||
_decodedData = EncodingUtilities.DecodeFromBase64Utf8(Data);
|
||||
}
|
||||
|
||||
return _decodedData.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type (or "media type") of the content, specifying the format of the data.
|
||||
|
|
@ -402,14 +464,73 @@ public sealed class ImageContentBlock : ContentBlock
|
|||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public sealed class AudioContentBlock : ContentBlock
|
||||
{
|
||||
private ReadOnlyMemory<byte>? _decodedData;
|
||||
private ReadOnlyMemory<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AudioContentBlock"/> from decoded audio bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The unencoded audio bytes.</param>
|
||||
/// <param name="mimeType">The MIME type of the audio.</param>
|
||||
/// <returns>A new <see cref="AudioContentBlock"/> instance.</returns>
|
||||
/// <remarks>
|
||||
/// This method stores the provided bytes as <see cref="DecodedData"/> and encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static AudioContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mimeType)
|
||||
{
|
||||
ReadOnlyMemory<byte> data = EncodingUtilities.EncodeToBase64Utf8(bytes);
|
||||
|
||||
return new()
|
||||
{
|
||||
_decodedData = bytes,
|
||||
Data = data,
|
||||
MimeType = mimeType
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Type => "audio";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base64-encoded audio data.
|
||||
/// Gets or sets the base64-encoded UTF-8 bytes representing the audio data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting this value will invalidate any cached value of <see cref="DecodedData"/>.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("data")]
|
||||
public required string Data { get; set; }
|
||||
public required ReadOnlyMemory<byte> Data
|
||||
{
|
||||
get => _data;
|
||||
set
|
||||
{
|
||||
_data = value;
|
||||
_decodedData = null; // Invalidate cache
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the decoded audio data represented by <see cref="Data"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When getting, this member will decode the value in <see cref="Data"/> and cache the result.
|
||||
/// Subsequent accesses return the cached value unless <see cref="Data"/> is modified.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ReadOnlyMemory<byte> DecodedData
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_decodedData is null)
|
||||
{
|
||||
_decodedData = EncodingUtilities.DecodeFromBase64Utf8(Data);
|
||||
}
|
||||
|
||||
return _decodedData.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type (or "media type") of the content, specifying the format of the data.
|
||||
|
|
|
|||
|
|
@ -27,4 +27,25 @@ internal static class DebuggerDisplayHelper
|
|||
|
||||
return "invalid base64";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the decoded length of base64 data encoded as UTF-8 bytes for debugger display.
|
||||
/// </summary>
|
||||
internal static string GetBase64LengthDisplay(ReadOnlyMemory<byte> base64Data)
|
||||
{
|
||||
#if NET
|
||||
if (System.Buffers.Text.Base64.IsValid(base64Data.Span, out int decodedLength))
|
||||
{
|
||||
return $"{decodedLength} bytes";
|
||||
}
|
||||
#else
|
||||
try
|
||||
{
|
||||
return $"{Convert.FromBase64String(System.Text.Encoding.UTF8.GetString(base64Data.ToArray())).Length} bytes";
|
||||
}
|
||||
catch { }
|
||||
#endif
|
||||
|
||||
return "invalid base64";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Buffers;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
|
@ -78,7 +79,7 @@ public abstract class ResourceContents
|
|||
|
||||
string? uri = null;
|
||||
string? mimeType = null;
|
||||
string? blob = null;
|
||||
ReadOnlyMemory<byte>? blob = null;
|
||||
string? text = null;
|
||||
JsonObject? meta = null;
|
||||
|
||||
|
|
@ -104,7 +105,7 @@ public abstract class ResourceContents
|
|||
break;
|
||||
|
||||
case "blob":
|
||||
blob = reader.GetString();
|
||||
blob = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
|
||||
break;
|
||||
|
||||
case "text":
|
||||
|
|
@ -127,7 +128,7 @@ public abstract class ResourceContents
|
|||
{
|
||||
Uri = uri ?? string.Empty,
|
||||
MimeType = mimeType,
|
||||
Blob = blob,
|
||||
Blob = blob.Value,
|
||||
Meta = meta,
|
||||
};
|
||||
}
|
||||
|
|
@ -162,7 +163,7 @@ public abstract class ResourceContents
|
|||
Debug.Assert(value is BlobResourceContents or TextResourceContents);
|
||||
if (value is BlobResourceContents blobResource)
|
||||
{
|
||||
writer.WriteString("blob", blobResource.Blob);
|
||||
writer.WriteString("blob", blobResource.Blob.Span);
|
||||
}
|
||||
else if (value is TextResourceContents textResource)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue