private async ValueTask DispatchIncomingRequestAsync()

in src/StreamJsonRpc/JsonRpc.cs [1969:2151]


        private async ValueTask<JsonRpcMessage> DispatchIncomingRequestAsync(JsonRpcRequest request)
        {
            Requires.NotNull(request, nameof(request));
            Requires.Argument(request.Method is not null, nameof(request), "Method must be set.");

            CancellationTokenSource? localMethodCancellationSource = null;
            CancellationTokenRegistration disconnectedRegistration = default;
            try
            {
                // Add cancelation to inboundCancellationSources before yielding to ensure that
                // it cannot be preempted by the cancellation request that would try to set it
                // Fix for https://github.com/Microsoft/vs-streamjsonrpc/issues/56
                CancellationToken cancellationToken = CancellationToken.None;
                if (request.IsResponseExpected)
                {
#pragma warning disable CA2000 // Dispose objects before losing scope
                    localMethodCancellationSource = new CancellationTokenSource();
#pragma warning restore CA2000 // Dispose objects before losing scope
                    cancellationToken = localMethodCancellationSource.Token;

                    if (this.CancelLocallyInvokedMethodsWhenConnectionIsClosed)
                    {
                        // We do NOT use CancellationTokenSource.CreateLinkedToken because that link is unbreakable,
                        // and we need to break the link (but without disposing the CTS we created) at the conclusion of this method.
                        // Disposing the CTS causes .NET Framework (in its default configuration) to throw when CancellationToken.Register is called later,
                        // which causes problems with some long-lived server methods such as those that return IAsyncEnumerable<T>.
                        disconnectedRegistration = this.DisconnectedToken.Register(state => ((CancellationTokenSource)state!).Cancel(), localMethodCancellationSource);
                    }
                }

                if (this.rpcTargetInfo.TryGetTargetMethod(request, out TargetMethod? targetMethod) && targetMethod.IsFound)
                {
                    // Add cancelation to inboundCancellationSources before yielding to ensure that
                    // it cannot be preempted by the cancellation request that would try to set it
                    // Fix for https://github.com/Microsoft/vs-streamjsonrpc/issues/56
                    if (targetMethod.AcceptsCancellationToken && request.IsResponseExpected)
                    {
                        this.CancellationStrategy?.IncomingRequestStarted(request.RequestId, localMethodCancellationSource!);
                    }

                    if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Information))
                    {
                        this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEvents.LocalInvocation, "Invoking {0}", targetMethod);
                    }

                    if (JsonRpcEventSource.Instance.IsEnabled(System.Diagnostics.Tracing.EventLevel.Verbose, System.Diagnostics.Tracing.EventKeywords.None))
                    {
                        if (request.IsResponseExpected)
                        {
                            JsonRpcEventSource.Instance.ReceivedRequest(request.RequestId.NumberIfPossibleForEvent, request.Method, JsonRpcEventSource.GetArgumentsString(request));
                        }
                        else
                        {
                            JsonRpcEventSource.Instance.ReceivedNotification(request.Method, JsonRpcEventSource.GetArgumentsString(request));
                        }
                    }

                    return await this.DispatchRequestAsync(request, targetMethod, cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    ImmutableList<JsonRpc> remoteRpcTargets = this.remoteRpcTargets;

                    // If we can't find the method or the target object does not exist or does not contain methods, we relay the message to the server.
                    // Any exceptions from the relay will be returned back to the origin since we catch all exceptions here.  The message being relayed to the
                    // server would share the same id as the message sent from origin. We just take the message objec wholesale and pass it along to the
                    // other side.
                    if (!remoteRpcTargets.IsEmpty)
                    {
                        if (request.IsResponseExpected)
                        {
                            this.CancellationStrategy?.IncomingRequestStarted(request.RequestId, localMethodCancellationSource!);
                        }

                        // Yield now so method invocation is async and we can proceed to handle other requests meanwhile.
                        // IMPORTANT: This should be the first await in this async method,
                        //            and no other await should be between this one and actually invoking the target method.
                        //            This is crucial to the guarantee that method invocation order is preserved from client to server
                        //            when a single-threaded SynchronizationContext is applied.
                        await this.SynchronizationContextOrDefault;

                        // Before we forward the request to the remote targets, we need to change the request ID so it gets a new ID in case we run into collisions.  For example,
                        // if origin issues a request destined for the remote target at the same time as a request issued by the relay to the remote target, their IDs could be mixed up.
                        // See InvokeRemoteTargetWithExistingId unit test for an example.
                        RequestId previousId = request.RequestId;

                        JsonRpcMessage? remoteResponse = null;
                        foreach (JsonRpc remoteTarget in remoteRpcTargets)
                        {
                            if (request.IsResponseExpected)
                            {
                                request.RequestId = remoteTarget.CreateNewRequestId();
                            }

                            CancellationToken token = request.IsResponseExpected ? localMethodCancellationSource!.Token : CancellationToken.None;
                            remoteResponse = await remoteTarget.InvokeCoreAsync(request, null, token).ConfigureAwait(false);

                            if (remoteResponse is JsonRpcError error && error.Error is not null)
                            {
                                if (error.Error.Code == JsonRpcErrorCode.MethodNotFound || error.Error.Code == JsonRpcErrorCode.InvalidParams)
                                {
                                    // If the result is an error and that error is method not found or invalid parameters on the remote target, then we continue on to the next target.
                                    continue;
                                }
                            }

                            // Otherwise, we simply return the json response;
                            break;
                        }

                        if (remoteResponse is not null)
                        {
                            if (remoteResponse is IJsonRpcMessageWithId messageWithId)
                            {
                                messageWithId.RequestId = previousId;
                            }

                            return remoteResponse;
                        }
                    }

                    if (targetMethod is null)
                    {
                        if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Warning))
                        {
                            this.TraceSource.TraceEvent(TraceEventType.Warning, (int)TraceEvents.RequestWithoutMatchingTarget, "No target methods are registered that match \"{0}\".", request.Method);
                        }

                        JsonRpcError errorMessage = (this.MessageHandler.Formatter as IJsonRpcMessageFactory)?.CreateErrorMessage() ?? new JsonRpcError();
                        errorMessage.RequestId = request.RequestId;
                        errorMessage.Error = new JsonRpcError.ErrorDetail
                        {
                            Code = JsonRpcErrorCode.MethodNotFound,
                            Message = string.Format(CultureInfo.CurrentCulture, Resources.RpcMethodNameNotFound, request.Method),
                        };
                        return errorMessage;
                    }
                    else
                    {
                        if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Warning))
                        {
                            this.TraceSource.TraceEvent(TraceEventType.Warning, (int)TraceEvents.RequestWithoutMatchingTarget, "Invocation of \"{0}\" cannot occur because arguments do not match any registered target methods.", request.Method);
                        }

                        JsonRpcError errorMessage = (this.MessageHandler.Formatter as IJsonRpcMessageFactory)?.CreateErrorMessage() ?? new JsonRpcError();
                        errorMessage.RequestId = request.RequestId;
                        errorMessage.Error = new JsonRpcError.ErrorDetail
                        {
                            Code = JsonRpcErrorCode.InvalidParams,
                            Message = targetMethod.LookupErrorMessage,
                            Data = targetMethod.ArgumentDeserializationFailures is object ? new CommonErrorData(targetMethod.ArgumentDeserializationFailures) : null,
                        };
                        return errorMessage;
                    }
                }
            }
            catch (Exception ex) when (!this.IsFatalException(StripExceptionToInnerException(ex)))
            {
                JsonRpcError error = this.CreateError(request, ex);

                if (error.Error is not null && JsonRpcEventSource.Instance.IsEnabled(System.Diagnostics.Tracing.EventLevel.Warning, System.Diagnostics.Tracing.EventKeywords.None))
                {
                    JsonRpcEventSource.Instance.SendingError(request.RequestId.NumberIfPossibleForEvent, error.Error.Code);
                }

                return error;
            }
            finally
            {
                if (localMethodCancellationSource is not null)
                {
                    this.CancellationStrategy?.IncomingRequestEnded(request.RequestId);

                    // Be sure to dispose the link to the local method token we created in case it is linked to our long-lived disposal token
                    // and otherwise cause a memory leak.
#if NETSTANDARD2_1_OR_GREATER
                    await disconnectedRegistration.DisposeAsync().ConfigureAwait(false);
#else
                    disconnectedRegistration.Dispose();
#endif
                }
            }
        }