Skip to content

Commit 026e95e

Browse files
Copilotstephentoub
andcommitted
Make FromBytes lazy: defer base64 encoding until Data/Blob is accessed
FromBytes now stores only the decoded bytes via a private constructor with [SetsRequiredMembers]. The Data/Blob property getter lazily encodes to base64 UTF-8 on first access. This avoids eager encoding when only DecodedData is consumed. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent d5416af commit 026e95e

2 files changed

Lines changed: 92 additions & 35 deletions

File tree

src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Buffers;
22
using System.Buffers.Text;
33
using System.Diagnostics;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Runtime.InteropServices;
56
using System.Text.Json.Serialization;
67

@@ -28,7 +29,7 @@ namespace ModelContextProtocol.Protocol;
2829
public sealed class BlobResourceContents : ResourceContents
2930
{
3031
private ReadOnlyMemory<byte>? _decodedData;
31-
private ReadOnlyMemory<byte> _blob;
32+
private ReadOnlyMemory<byte>? _blob;
3233

3334
/// <summary>
3435
/// Creates an <see cref="BlobResourceContents"/> from raw data.
@@ -40,15 +41,20 @@ public sealed class BlobResourceContents : ResourceContents
4041
/// <exception cref="InvalidOperationException"></exception>
4142
public static BlobResourceContents FromBytes(ReadOnlyMemory<byte> bytes, string uri, string? mimeType = null)
4243
{
43-
ReadOnlyMemory<byte> blob = EncodingUtilities.EncodeToBase64Utf8(bytes);
44-
45-
return new()
46-
{
47-
_decodedData = bytes,
48-
Blob = blob,
49-
MimeType = mimeType,
50-
Uri = uri
51-
};
44+
return new(bytes, uri, mimeType);
45+
}
46+
47+
/// <summary>Initializes a new instance of the <see cref="BlobResourceContents"/> class.</summary>
48+
public BlobResourceContents()
49+
{
50+
}
51+
52+
[SetsRequiredMembers]
53+
private BlobResourceContents(ReadOnlyMemory<byte> decodedData, string uri, string? mimeType)
54+
{
55+
_decodedData = decodedData;
56+
Uri = uri;
57+
MimeType = mimeType;
5258
}
5359

5460
/// <summary>
@@ -60,7 +66,16 @@ public static BlobResourceContents FromBytes(ReadOnlyMemory<byte> bytes, string
6066
[JsonPropertyName("blob")]
6167
public required ReadOnlyMemory<byte> Blob
6268
{
63-
get => _blob;
69+
get
70+
{
71+
if (_blob is null)
72+
{
73+
Debug.Assert(_decodedData is not null);
74+
_blob = EncodingUtilities.EncodeToBase64Utf8(_decodedData!.Value);
75+
}
76+
77+
return _blob.Value;
78+
}
6479
set
6580
{
6681
_blob = value;

src/ModelContextProtocol.Core/Protocol/ContentBlock.cs

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ public sealed class TextContentBlock : ContentBlock
428428
public sealed class ImageContentBlock : ContentBlock
429429
{
430430
private ReadOnlyMemory<byte>? _decodedData;
431-
private ReadOnlyMemory<byte> _data;
431+
private ReadOnlyMemory<byte>? _data;
432432

433433
/// <summary>
434434
/// Creates an <see cref="ImageContentBlock"/> from decoded image bytes.
@@ -437,22 +437,27 @@ public sealed class ImageContentBlock : ContentBlock
437437
/// <param name="mimeType">The MIME type of the image.</param>
438438
/// <returns>A new <see cref="ImageContentBlock"/> instance.</returns>
439439
/// <remarks>
440-
/// This method stores the provided bytes as <see cref="DecodedData"/> and encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
440+
/// This method stores the provided bytes as <see cref="DecodedData"/> and lazily encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
441441
/// </remarks>
442442
/// <exception cref="ArgumentNullException"><paramref name="mimeType"/> is <see langword="null"/>.</exception>
443443
/// <exception cref="ArgumentException"><paramref name="mimeType"/> is empty or composed entirely of whitespace.</exception>
444444
public static ImageContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mimeType)
445445
{
446446
Throw.IfNullOrWhiteSpace(mimeType);
447447

448-
ReadOnlyMemory<byte> data = EncodingUtilities.EncodeToBase64Utf8(bytes);
449-
450-
return new()
451-
{
452-
_decodedData = bytes,
453-
Data = data,
454-
MimeType = mimeType
455-
};
448+
return new(bytes, mimeType);
449+
}
450+
451+
/// <summary>Initializes a new instance of the <see cref="ImageContentBlock"/> class.</summary>
452+
public ImageContentBlock()
453+
{
454+
}
455+
456+
[SetsRequiredMembers]
457+
private ImageContentBlock(ReadOnlyMemory<byte> decodedData, string mimeType)
458+
{
459+
_decodedData = decodedData;
460+
MimeType = mimeType;
456461
}
457462

458463
/// <inheritdoc/>
@@ -467,7 +472,16 @@ public static ImageContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mim
467472
[JsonPropertyName("data")]
468473
public required ReadOnlyMemory<byte> Data
469474
{
470-
get => _data;
475+
get
476+
{
477+
if (_data is null)
478+
{
479+
Debug.Assert(_decodedData is not null);
480+
_data = EncodingUtilities.EncodeToBase64Utf8(_decodedData!.Value);
481+
}
482+
483+
return _data.Value;
484+
}
471485
set
472486
{
473487
_data = value;
@@ -508,15 +522,22 @@ public ReadOnlyMemory<byte> DecodedData
508522
public required string MimeType { get; set; }
509523

510524
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
511-
private string DebuggerDisplay => $"MimeType = {MimeType}, Length = {DebuggerDisplayHelper.GetBase64LengthDisplay(Data)}";
525+
private string DebuggerDisplay
526+
{
527+
get
528+
{
529+
string lengthDisplay = _decodedData is not null ? $"{_decodedData.Value.Length} bytes" : DebuggerDisplayHelper.GetBase64LengthDisplay(Data);
530+
return $"MimeType = {MimeType}, Length = {lengthDisplay}";
531+
}
532+
}
512533
}
513534

514535
/// <summary>Represents audio provided to or from an LLM.</summary>
515536
[DebuggerDisplay("{DebuggerDisplay,nq}")]
516537
public sealed class AudioContentBlock : ContentBlock
517538
{
518539
private ReadOnlyMemory<byte>? _decodedData;
519-
private ReadOnlyMemory<byte> _data;
540+
private ReadOnlyMemory<byte>? _data;
520541

521542
/// <summary>
522543
/// Creates an <see cref="AudioContentBlock"/> from decoded audio bytes.
@@ -525,22 +546,27 @@ public sealed class AudioContentBlock : ContentBlock
525546
/// <param name="mimeType">The MIME type of the audio.</param>
526547
/// <returns>A new <see cref="AudioContentBlock"/> instance.</returns>
527548
/// <remarks>
528-
/// This method stores the provided bytes as <see cref="DecodedData"/> and encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
549+
/// This method stores the provided bytes as <see cref="DecodedData"/> and lazily encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
529550
/// </remarks>
530551
/// <exception cref="ArgumentNullException"><paramref name="mimeType"/> is <see langword="null"/>.</exception>
531552
/// <exception cref="ArgumentException"><paramref name="mimeType"/> is empty or composed entirely of whitespace.</exception>
532553
public static AudioContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mimeType)
533554
{
534555
Throw.IfNullOrWhiteSpace(mimeType);
535556

536-
ReadOnlyMemory<byte> data = EncodingUtilities.EncodeToBase64Utf8(bytes);
537-
538-
return new()
539-
{
540-
_decodedData = bytes,
541-
Data = data,
542-
MimeType = mimeType
543-
};
557+
return new(bytes, mimeType);
558+
}
559+
560+
/// <summary>Initializes a new instance of the <see cref="AudioContentBlock"/> class.</summary>
561+
public AudioContentBlock()
562+
{
563+
}
564+
565+
[SetsRequiredMembers]
566+
private AudioContentBlock(ReadOnlyMemory<byte> decodedData, string mimeType)
567+
{
568+
_decodedData = decodedData;
569+
MimeType = mimeType;
544570
}
545571

546572
/// <inheritdoc/>
@@ -555,7 +581,16 @@ public static AudioContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mim
555581
[JsonPropertyName("data")]
556582
public required ReadOnlyMemory<byte> Data
557583
{
558-
get => _data;
584+
get
585+
{
586+
if (_data is null)
587+
{
588+
Debug.Assert(_decodedData is not null);
589+
_data = EncodingUtilities.EncodeToBase64Utf8(_decodedData!.Value);
590+
}
591+
592+
return _data.Value;
593+
}
559594
set
560595
{
561596
_data = value;
@@ -596,7 +631,14 @@ public ReadOnlyMemory<byte> DecodedData
596631
public required string MimeType { get; set; }
597632

598633
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
599-
private string DebuggerDisplay => $"MimeType = {MimeType}, Length = {DebuggerDisplayHelper.GetBase64LengthDisplay(Data)}";
634+
private string DebuggerDisplay
635+
{
636+
get
637+
{
638+
string lengthDisplay = _decodedData is not null ? $"{_decodedData.Value.Length} bytes" : DebuggerDisplayHelper.GetBase64LengthDisplay(Data);
639+
return $"MimeType = {MimeType}, Length = {lengthDisplay}";
640+
}
641+
}
600642
}
601643

602644
/// <summary>Represents the contents of a resource, embedded into a prompt or tool call result.</summary>

0 commit comments

Comments
 (0)