Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 118 additions & 109 deletions src/StreamJsonRpc/JsonRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1866,120 +1866,14 @@ protected Task<TResult> InvokeCoreAsync<TResult>(RequestId id, string targetName
return this.InvokeCoreAsync<TResult>(id, targetName, arguments, null, null, cancellationToken, isParameterObject);
}

#pragma warning disable CA1200 // Avoid using cref tags with a prefix
/// <summary>
/// Invokes a given method on a JSON-RPC server.
/// </summary>
/// <inheritdoc cref="InvokeCoreAsync(RequestId, string, IReadOnlyList{object?}?, IReadOnlyList{Type}?, IReadOnlyDictionary{string, Type}?, Type, bool, CancellationToken)"/>
/// <typeparam name="TResult">RPC method return type.</typeparam>
/// <param name="id">An identifier established by the Client. If the default value is given, it is assumed to be a notification.</param>
/// <param name="targetName">Name of the method to invoke. Must not be null or empty.</param>
/// <param name="arguments">Arguments to pass to the invoked method. They must be serializable using the selected <see cref="IJsonRpcMessageFormatter"/>. If <see langword="null"/>, no arguments are passed.</param>
/// <param name="positionalArgumentDeclaredTypes">
/// A list of <see cref="Type"/> objects that describe how each element in <paramref name="arguments"/> is expected by the server to be typed.
/// If specified, this must have exactly the same length as <paramref name="arguments"/> and contain no <see langword="null"/> elements.
/// This value is ignored when <paramref name="isParameterObject"/> is true.
/// </param>
/// <param name="namedArgumentDeclaredTypes">
/// A dictionary of <see cref="Type"/> objects that describe how each entry in the <see cref="IReadOnlyDictionary{TKey, TValue}"/> provided in the only element of <paramref name="arguments"/> is expected by the server to be typed.
/// If specified, this must have exactly the same set of keys as the dictionary contained in the first element of <paramref name="arguments"/>, and contain no <see langword="null"/> values.
/// </param>
/// <param name="cancellationToken">The token whose cancellation should signal the server to stop processing this request.</param>
/// <param name="isParameterObject">Value which indicates if parameter should be passed as an object.</param>
/// <returns>A task that completes with the response from the JSON-RPC server.</returns>
/// <exception cref="OperationCanceledException">
/// Thrown after <paramref name="cancellationToken"/> is canceled.
/// If the request has already been transmitted, the exception is only thrown after the server has received the cancellation notification and responded to it.
/// If the server completes the request instead of cancelling, this exception will not be thrown.
/// When the connection drops before receiving a response, this exception is thrown if <paramref name="cancellationToken"/> has been canceled.
/// </exception>
/// <exception cref="RemoteRpcException">
/// A common base class for a variety of RPC exceptions that may be thrown. Some common derived types are listed individually.
/// </exception>
/// <exception cref="RemoteInvocationException">
/// Thrown when an error is returned from the server in consequence of executing the requested method.
/// </exception>
/// <exception cref="RemoteMethodNotFoundException">
/// Thrown when the server reports that no matching method was found to invoke.
/// </exception>
/// <exception cref="ArgumentNullException">If <paramref name="targetName"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="targetName" /> is empty.</exception>
/// <exception cref="ObjectDisposedException">If this instance of <see cref="JsonRpc"/> has already been disposed prior to this call.</exception>
/// <exception cref="ConnectionLostException">
/// Thrown when the connection is terminated (by either side) before the request or while the request is in progress,
/// unless <paramref name="cancellationToken"/> is already signaled.
/// </exception>
/// <exception cref="Exception">
/// Any exception thrown by the <see cref="IJsonRpcMessageFormatter"/> (typically due to serialization failures).
/// When using <see cref="JsonMessageFormatter"/> this should be <see cref="JsonSerializationException"/>.
/// When using <see cref="MessagePackFormatter"/> this should be <see cref="T:MessagePack.MessagePackSerializationException"/>.
/// </exception>
#pragma warning disable CA1068 // CancellationToken parameters must come last
protected async Task<TResult> InvokeCoreAsync<TResult>(RequestId id, string targetName, IReadOnlyList<object?>? arguments, IReadOnlyList<Type>? positionalArgumentDeclaredTypes, IReadOnlyDictionary<string, Type>? namedArgumentDeclaredTypes, CancellationToken cancellationToken, bool isParameterObject)
#pragma warning restore CA1068 // CancellationToken parameters must come last
#pragma warning restore CA1200 // Avoid using cref tags with a prefix
{
Requires.NotNullOrEmpty(targetName, nameof(targetName));

cancellationToken.ThrowIfCancellationRequested();
Verify.NotDisposed(this);

JsonRpcRequest request = (this.MessageHandler.Formatter as IJsonRpcMessageFactory)?.CreateRequestMessage() ?? new JsonRpcRequest();
request.RequestId = id;
request.Method = targetName;
this.ActivityTracingStrategy?.ApplyOutboundActivity(request);

if (isParameterObject)
{
object? argument = arguments;
if (arguments is not null)
{
if (arguments.Count != 1 || arguments[0] is null)
{
throw new ArgumentException(Resources.ParameterNotObject);
}

argument = arguments[0];
}

request.Arguments = argument;
if (namedArgumentDeclaredTypes is object)
{
Requires.Argument(namedArgumentDeclaredTypes.Count == request.ArgumentCount, nameof(namedArgumentDeclaredTypes), Resources.TypedArgumentsLengthMismatch);
request.NamedArgumentDeclaredTypes = namedArgumentDeclaredTypes;
}
}
else
{
request.Arguments = arguments ?? Array.Empty<object>();

if (positionalArgumentDeclaredTypes is object)
{
Requires.Argument(positionalArgumentDeclaredTypes.Count == request.ArgumentCount, nameof(positionalArgumentDeclaredTypes), Resources.TypedArgumentsLengthMismatch);
request.ArgumentListDeclaredTypes = positionalArgumentDeclaredTypes;
}
}

JsonRpcMessage? response = await this.InvokeCoreAsync(request, typeof(TResult), cancellationToken).ConfigureAwait(false);

if (request.IsResponseExpected)
{
if (response is JsonRpcError error)
{
throw this.CreateExceptionFromRpcError(request, error);
}
else if (response is JsonRpcResult result)
{
return result.GetResult<TResult>();
}
else
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.ResponseUnexpectedFormat, response?.GetType().FullName ?? "(null)"));
}
}
else
{
return default!;
}
JsonRpcResult? result = await this.InvokeCoreAsync(id, targetName, arguments, positionalArgumentDeclaredTypes, namedArgumentDeclaredTypes, typeof(TResult), isParameterObject, cancellationToken).ConfigureAwait(false);
return result is null ? default! : result.GetResult<TResult>();
}

/// <summary>
Expand Down Expand Up @@ -2241,6 +2135,121 @@ private static bool TryGetTaskFromValueTask(object? result, [NotNullWhen(true)]
return false;
}

/// <summary>
/// Invokes a given method on a JSON-RPC server.
/// </summary>
/// <param name="id">An identifier established by the Client. If the default value is given, it is assumed to be a notification.</param>
/// <param name="targetName">Name of the method to invoke. Must not be null or empty.</param>
/// <param name="arguments">Arguments to pass to the invoked method. They must be serializable using the selected <see cref="IJsonRpcMessageFormatter"/>. If <see langword="null"/>, no arguments are passed.</param>
/// <param name="positionalArgumentDeclaredTypes">
/// A list of <see cref="Type"/> objects that describe how each element in <paramref name="arguments"/> is expected by the server to be typed.
/// If specified, this must have exactly the same length as <paramref name="arguments"/> and contain no <see langword="null"/> elements.
/// This value is ignored when <paramref name="isParameterObject"/> is true.
/// </param>
/// <param name="namedArgumentDeclaredTypes">
/// A dictionary of <see cref="Type"/> objects that describe how each entry in the <see cref="IReadOnlyDictionary{TKey, TValue}"/> provided in the only element of <paramref name="arguments"/> is expected by the server to be typed.
/// If specified, this must have exactly the same set of keys as the dictionary contained in the first element of <paramref name="arguments"/>, and contain no <see langword="null"/> values.
/// </param>
/// <param name="expectedResultType">The expected type of the result returned by the server.</param>
/// <param name="isParameterObject">Value which indicates if parameter should be passed as an object.</param>
/// <param name="cancellationToken">The token whose cancellation should signal the server to stop processing this request.</param>
/// <returns>A task that completes with the response from the JSON-RPC server.</returns>
/// <exception cref="OperationCanceledException">
/// Thrown after <paramref name="cancellationToken"/> is canceled.
/// If the request has already been transmitted, the exception is only thrown after the server has received the cancellation notification and responded to it.
/// If the server completes the request instead of cancelling, this exception will not be thrown.
/// When the connection drops before receiving a response, this exception is thrown if <paramref name="cancellationToken"/> has been canceled.
/// </exception>
/// <exception cref="RemoteRpcException">
/// A common base class for a variety of RPC exceptions that may be thrown. Some common derived types are listed individually.
/// </exception>
/// <exception cref="RemoteInvocationException">
/// Thrown when an error is returned from the server in consequence of executing the requested method.
/// </exception>
/// <exception cref="RemoteMethodNotFoundException">
/// Thrown when the server reports that no matching method was found to invoke.
/// </exception>
/// <exception cref="ArgumentNullException">If <paramref name="targetName"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="targetName" /> is empty.</exception>
/// <exception cref="ObjectDisposedException">If this instance of <see cref="JsonRpc"/> has already been disposed prior to this call.</exception>
/// <exception cref="ConnectionLostException">
/// Thrown when the connection is terminated (by either side) before the request or while the request is in progress,
/// unless <paramref name="cancellationToken"/> is already signaled.
/// </exception>
/// <exception cref="Exception">
/// Any exception thrown by the <see cref="IJsonRpcMessageFormatter"/> (typically due to serialization failures).
/// When using <see cref="JsonMessageFormatter"/> this should be <see cref="JsonSerializationException"/>.
/// When using <see cref="MessagePackFormatter"/> this should be <see cref="T:MessagePack.MessagePackSerializationException"/>.
/// </exception>
#if NET
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
#endif
private async ValueTask<JsonRpcResult?> InvokeCoreAsync(RequestId id, string targetName, IReadOnlyList<object?>? arguments, IReadOnlyList<Type>? positionalArgumentDeclaredTypes, IReadOnlyDictionary<string, Type>? namedArgumentDeclaredTypes, Type expectedResultType, bool isParameterObject, CancellationToken cancellationToken)
{
Requires.NotNullOrEmpty(targetName, nameof(targetName));

cancellationToken.ThrowIfCancellationRequested();
Verify.NotDisposed(this);

JsonRpcRequest request = (this.MessageHandler.Formatter as IJsonRpcMessageFactory)?.CreateRequestMessage() ?? new JsonRpcRequest();
request.RequestId = id;
request.Method = targetName;
this.ActivityTracingStrategy?.ApplyOutboundActivity(request);

if (isParameterObject)
{
object? argument = arguments;
if (arguments is not null)
{
if (arguments.Count != 1 || arguments[0] is null)
{
throw new ArgumentException(Resources.ParameterNotObject);
}

argument = arguments[0];
}

request.Arguments = argument;
if (namedArgumentDeclaredTypes is object)
{
Requires.Argument(namedArgumentDeclaredTypes.Count == request.ArgumentCount, nameof(namedArgumentDeclaredTypes), Resources.TypedArgumentsLengthMismatch);
request.NamedArgumentDeclaredTypes = namedArgumentDeclaredTypes;
}
}
else
{
request.Arguments = arguments ?? Array.Empty<object>();

if (positionalArgumentDeclaredTypes is object)
{
Requires.Argument(positionalArgumentDeclaredTypes.Count == request.ArgumentCount, nameof(positionalArgumentDeclaredTypes), Resources.TypedArgumentsLengthMismatch);
request.ArgumentListDeclaredTypes = positionalArgumentDeclaredTypes;
}
}

JsonRpcMessage? response = await this.InvokeCoreAsync(request, expectedResultType, cancellationToken).ConfigureAwait(false);

if (request.IsResponseExpected)
{
if (response is JsonRpcError error)
{
throw this.CreateExceptionFromRpcError(request, error);
}
else if (response is JsonRpcResult result)
{
return result;
Comment thread
AArnott marked this conversation as resolved.
Resolved
}
else
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.ResponseUnexpectedFormat, response?.GetType().FullName ?? "(null)"));
}
}
else
{
return null;
}
}

private JsonRpcError CreateCancellationResponse(JsonRpcRequest request)
{
JsonRpcError errorMessage = (this.MessageHandler.Formatter as IJsonRpcMessageFactory)?.CreateErrorMessage() ?? new JsonRpcError();
Expand Down
Loading