Avoid new JsonSerializerOptions instances (#34)
We try hard to avoid creating unnecessary JsonSerializerOptions, as each can have a non-trivial overhead due to how reflection is used by JsonSerializer and the JSO instance serving as a cache. Rather than creating a new JSO for each transport / endpoint, this can just use a singleton. The only thing it loses is logging when the converter fails, but all of those paths are also throwing an exception, which can be logged higher in the call stack.
This commit is contained in:
parent
e5c26d6285
commit
9f0beec63a
7 changed files with 14 additions and 53 deletions
|
|
@ -266,33 +266,6 @@ internal static partial class Log
|
|||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaned up for {endpointName}")]
|
||||
internal static partial void TransportCleanedUp(this ILogger logger, string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message start object token expected")]
|
||||
internal static partial void JsonRpcMessageConverterExpectedStartObjectToken(this ILogger logger);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message invalid version")]
|
||||
internal static partial void JsonRpcMessageConverterInvalidJsonRpcVersion(this ILogger logger);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "JSON-RPC message deserializing error response: {rawText}")]
|
||||
internal static partial void JsonRpcMessageConverterDeserializingErrorResponse(this ILogger logger, string rawText);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "JSON-RPC message deserializing response: {rawText}")]
|
||||
internal static partial void JsonRpcMessageConverterDeserializingResponse(this ILogger logger, string rawText);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message response must have result or error: {rawText}")]
|
||||
internal static partial void JsonRpcMessageConverterResponseMustHaveResultOrError(this ILogger logger, string rawText);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "JSON-RPC message deserializing notification: {rawText}")]
|
||||
internal static partial void JsonRpcMessageConverterDeserializingNotification(this ILogger logger, string rawText);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "JSON-RPC message deserializing request: {rawText}")]
|
||||
internal static partial void JsonRpcMessageConverterDeserializingRequest(this ILogger logger, string rawText);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message invalid format: {rawText}")]
|
||||
internal static partial void JsonRpcMessageConverterInvalidMessageFormat(this ILogger logger, string rawText);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message write unknown message type: {type}")]
|
||||
internal static partial void JsonRpcMessageConverterWriteUnknownMessageType(this ILogger logger, string type);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Sending notification payload for {endpointName}: {payload}")]
|
||||
internal static partial void SendingNotificationPayload(this ILogger logger, string endpointName, string payload);
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
_httpClient = new HttpClient();
|
||||
_connectionCts = new CancellationTokenSource();
|
||||
_logger = loggerFactory.CreateLogger<SseClientTransport>();
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
|
||||
_connectionEstablished = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
_options = options;
|
||||
_serverConfig = serverConfig;
|
||||
_logger = loggerFactory.CreateLogger<StdioClientTransport>();
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
|||
{
|
||||
_serverName = serverName;
|
||||
_logger = loggerFactory.CreateLogger<StdioClientTransport>();
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal abstract class McpJsonRpcEndpoint
|
|||
_pendingRequests = new();
|
||||
_notificationHandlers = new();
|
||||
_nextRequestId = 1;
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
|
||||
_logger = loggerFactory.CreateLogger<McpClient>();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,21 +9,13 @@ namespace McpDotNet.Utils.Json;
|
|||
/// <summary>
|
||||
/// JSON converter for IJsonRpcMessage that handles polymorphic deserialization of different message types.
|
||||
/// </summary>
|
||||
internal class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
|
||||
internal sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
|
||||
{
|
||||
private readonly ILogger<JsonRpcMessageConverter> _logger;
|
||||
|
||||
public JsonRpcMessageConverter(ILogger<JsonRpcMessageConverter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IJsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
_logger.JsonRpcMessageConverterExpectedStartObjectToken();
|
||||
throw new JsonException("Expected StartObject token");
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +26,6 @@ internal class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
|
|||
if (!root.TryGetProperty("jsonrpc", out var versionProperty) ||
|
||||
versionProperty.GetString() != "2.0")
|
||||
{
|
||||
_logger.JsonRpcMessageConverterInvalidJsonRpcVersion();
|
||||
throw new JsonException("Invalid or missing jsonrpc version");
|
||||
}
|
||||
|
||||
|
|
@ -51,32 +42,27 @@ internal class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
|
|||
// Messages with an error property are error responses
|
||||
if (hasError)
|
||||
{
|
||||
_logger.JsonRpcMessageConverterDeserializingErrorResponse(rawText);
|
||||
return JsonSerializer.Deserialize<JsonRpcError>(rawText, options);
|
||||
}
|
||||
// Messages with a result property are success responses
|
||||
else if (root.TryGetProperty("result", out _))
|
||||
{
|
||||
_logger.JsonRpcMessageConverterDeserializingResponse(rawText);
|
||||
return JsonSerializer.Deserialize<JsonRpcResponse>(rawText, options);
|
||||
}
|
||||
_logger.JsonRpcMessageConverterResponseMustHaveResultOrError(rawText);
|
||||
|
||||
throw new JsonException("Response must have either result or error");
|
||||
}
|
||||
// Messages with a method but no id are notifications
|
||||
else if (hasMethod && !hasId)
|
||||
{
|
||||
_logger.JsonRpcMessageConverterDeserializingNotification(rawText);
|
||||
return JsonSerializer.Deserialize<JsonRpcNotification>(rawText, options);
|
||||
}
|
||||
// Messages with both method and id are requests
|
||||
else if (hasMethod && hasId)
|
||||
{
|
||||
_logger.JsonRpcMessageConverterDeserializingRequest(rawText);
|
||||
return JsonSerializer.Deserialize<JsonRpcRequest>(rawText, options);
|
||||
}
|
||||
|
||||
_logger.JsonRpcMessageConverterInvalidMessageFormat(rawText);
|
||||
throw new JsonException("Invalid JSON-RPC message format");
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +84,6 @@ internal class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
|
|||
JsonSerializer.Serialize(writer, error, options);
|
||||
break;
|
||||
default:
|
||||
_logger.JsonRpcMessageConverterWriteUnknownMessageType(value.GetType().ToString());
|
||||
throw new JsonException($"Unknown JSON-RPC message type: {value.GetType()}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,24 @@ using System.Text.Json;
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace McpDotNet.Utils.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for configuring System.Text.Json serialization options for MCP.
|
||||
/// </summary>
|
||||
internal static class JsonSerializerOptionsExtensions
|
||||
{
|
||||
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Configures JsonSerializerOptions with MCP-specific settings and converters.
|
||||
/// Creates default options to use for MCP-related serialization.
|
||||
/// </summary>
|
||||
/// <param name="options">The options to configure.</param>
|
||||
/// <param name="loggerFactory">The logger factory to use for logging.</param>
|
||||
/// <returns>The configured options.</returns>
|
||||
public static JsonSerializerOptions ConfigureForMcp(this JsonSerializerOptions options, ILoggerFactory loggerFactory)
|
||||
private static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
JsonSerializerOptions options = new();
|
||||
|
||||
// Add custom converters
|
||||
options.Converters.Add(new JsonRpcMessageConverter(loggerFactory.CreateLogger<JsonRpcMessageConverter>()));
|
||||
options.Converters.Add(new JsonRpcMessageConverter());
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
// Configure general options
|
||||
|
|
|
|||
Loading…
Reference in a new issue