diff --git a/src/Grpc.Core.Api/AsyncClientStreamingCall.cs b/src/Grpc.Core.Api/AsyncClientStreamingCall.cs index 86dc07855..789ed9c3d 100644 --- a/src/Grpc.Core.Api/AsyncClientStreamingCall.cs +++ b/src/Grpc.Core.Api/AsyncClientStreamingCall.cs @@ -180,12 +180,13 @@ public AsyncClientStreamingCallDebugView(AsyncClientStreamingCall _call.callState.State is IMethod method ? new CallDebuggerMethodDebugView(method) : null; public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.GetAwaiter().GetResult() : null; public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); public IClientStreamWriter RequestStream => _call.RequestStream; public TResponse? Response => _call.ResponseAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseAsync.Result : default; + public CallDebuggerMethodDebugView? Method => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.MethodKey) is { } method ? new CallDebuggerMethodDebugView(method) : null; + public ChannelBase? Channel => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.ChannelKey); } } diff --git a/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs b/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs index 721240f26..a9b275d3e 100644 --- a/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs +++ b/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs @@ -157,12 +157,13 @@ public AsyncDuplexStreamingCallDebugView(AsyncDuplexStreamingCall _call.callState.State is IMethod method ? new CallDebuggerMethodDebugView(method) : null; public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.Result : null; public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); public IAsyncStreamReader ResponseStream => _call.ResponseStream; public IClientStreamWriter RequestStream => _call.RequestStream; + public CallDebuggerMethodDebugView? Method => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.MethodKey) is { } method ? new CallDebuggerMethodDebugView(method) : null; + public ChannelBase? Channel => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.ChannelKey); } } diff --git a/src/Grpc.Core.Api/AsyncServerStreamingCall.cs b/src/Grpc.Core.Api/AsyncServerStreamingCall.cs index 59be107d0..efdf2b887 100644 --- a/src/Grpc.Core.Api/AsyncServerStreamingCall.cs +++ b/src/Grpc.Core.Api/AsyncServerStreamingCall.cs @@ -138,11 +138,13 @@ public AsyncServerStreamingCallDebugView(AsyncServerStreamingCall cal _call = call; } - public CallDebuggerMethodDebugView? Method => _call.callState.State is IMethod method ? new CallDebuggerMethodDebugView(method) : null; public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.Result : null; public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); public IAsyncStreamReader ResponseStream => _call.ResponseStream; + public CallDebuggerMethodDebugView? Method => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.MethodKey) is { } method ? new CallDebuggerMethodDebugView(method) : null; + public ChannelBase? Channel => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.ChannelKey); + public object? Request => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.RequestKey); } } diff --git a/src/Grpc.Core.Api/AsyncUnaryCall.cs b/src/Grpc.Core.Api/AsyncUnaryCall.cs index 8367be92c..e3292b33a 100644 --- a/src/Grpc.Core.Api/AsyncUnaryCall.cs +++ b/src/Grpc.Core.Api/AsyncUnaryCall.cs @@ -161,11 +161,13 @@ public AsyncUnaryCallDebugView(AsyncUnaryCall call) _call = call; } - public CallDebuggerMethodDebugView? Method => _call.callState.State is IMethod method ? new CallDebuggerMethodDebugView(method) : null; public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.Result : null; public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); public TResponse? Response => _call.ResponseAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseAsync.Result : default; + public CallDebuggerMethodDebugView? Method => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.MethodKey) is { } method ? new CallDebuggerMethodDebugView(method) : null; + public ChannelBase? Channel => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.ChannelKey); + public object? Request => CallDebuggerHelpers.GetDebugValue(_call.callState, CallDebuggerHelpers.RequestKey); } } diff --git a/src/Grpc.Core.Api/Internal/CallDebuggerHelpers.cs b/src/Grpc.Core.Api/Internal/CallDebuggerHelpers.cs index 0c2f7d002..a7e5a0d30 100644 --- a/src/Grpc.Core.Api/Internal/CallDebuggerHelpers.cs +++ b/src/Grpc.Core.Api/Internal/CallDebuggerHelpers.cs @@ -17,18 +17,28 @@ #endregion using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics; namespace Grpc.Core.Internal; internal static class CallDebuggerHelpers { + public const string MethodKey = "Method"; + public const string ChannelKey = "Channel"; + public const string RequestKey = "Request"; + public static string DebuggerToString(AsyncCallState callState) { string debugText = string.Empty; - if (callState.State is IMethod method) + if (GetDebugValue(callState, ChannelKey) is { } channel) + { + debugText = $"Channel = {channel.Target}, "; + } + if (GetDebugValue(callState, MethodKey) is { } method) { - debugText = $"Method = {method.FullName}, "; + debugText += $"Method = {method.FullName}, "; } var status = GetStatus(callState); @@ -40,6 +50,30 @@ public static string DebuggerToString(AsyncCallState callState) return debugText; } + public static T? GetDebugValue(AsyncCallState callState, string key) where T : class + { + // We want to get information about a call to display during debugging, but Grpc.Core.Api does + // doesn't have access to the implementation's internal fields. + // GetDebugValue accesses values by IEnumerable + key from the implementation state. + if (callState.State is IEnumerable> enumerable) + { + foreach (var entry in enumerable) + { + if (entry.Key == key) + { + if (entry.Value is T t) + { + return t; + } + + return null; + } + } + } + + return null; + } + public static Status? GetStatus(AsyncCallState callState) { // This is the only public API to get this value and there is no way to check if it's available. diff --git a/src/Grpc.Net.Client/Internal/GrpcCall.cs b/src/Grpc.Net.Client/Internal/GrpcCall.cs index 54f08c45d..15aa25325 100644 --- a/src/Grpc.Net.Client/Internal/GrpcCall.cs +++ b/src/Grpc.Net.Client/Internal/GrpcCall.cs @@ -16,6 +16,7 @@ #endregion +using System.Collections; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -53,6 +54,7 @@ internal sealed partial class GrpcCall : GrpcCall, IGrpcCal // These are set depending on the type of gRPC call private TaskCompletionSource? _responseTcs; + private TRequest? _request; public int MessagesWritten { get; private set; } public int MessagesRead { get; private set; } @@ -99,12 +101,11 @@ private void ValidateDeadline(DateTime? deadline) public object? CallWrapper { get; set; } - MethodType IMethod.Type => Method.Type; - string IMethod.ServiceName => Method.ServiceName; - string IMethod.Name => Method.Name; - string IMethod.FullName => Method.FullName; - - public void StartUnary(TRequest request) => StartUnaryCore(CreatePushUnaryContent(request)); + public void StartUnary(TRequest request) + { + _request = request; + StartUnaryCore(CreatePushUnaryContent(request)); + } public void StartClientStreaming() { @@ -1161,4 +1162,7 @@ private static void WriteDiagnosticEvent< { diagnosticSource.Write(name, value); } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerator> GetEnumerator() => GrpcProtocolConstants.GetDebugEnumerator(Channel, Method, _request); } diff --git a/src/Grpc.Net.Client/Internal/GrpcProtocolConstants.cs b/src/Grpc.Net.Client/Internal/GrpcProtocolConstants.cs index b323f80c8..e2d5f4390 100644 --- a/src/Grpc.Net.Client/Internal/GrpcProtocolConstants.cs +++ b/src/Grpc.Net.Client/Internal/GrpcProtocolConstants.cs @@ -16,6 +16,7 @@ #endregion +using System.Collections.Generic; using System.Net.Http.Headers; using Grpc.Core; using Grpc.Net.Compression; @@ -83,6 +84,24 @@ internal static string GetMessageAcceptEncoding(Dictionary + /// Gets key value pairs used by debugging. These are provided as an enumerator instead of a dictionary + /// because it's one method to implement an enumerator on gRPC calls compared to a dozen members for a dictionary. + /// + public static IEnumerator> GetDebugEnumerator(ChannelBase channel, IMethod method, object? request) + { + const string MethodKey = "Method"; + const string ChannelKey = "Channel"; + const string RequestKey = "Request"; + + yield return new KeyValuePair(ChannelKey, channel); + yield return new KeyValuePair(MethodKey, method); + if (request != null) + { + yield return new KeyValuePair(RequestKey, request); + } + } + static GrpcProtocolConstants() { UserAgentHeader = "User-Agent"; diff --git a/src/Grpc.Net.Client/Internal/IGrpcCall.cs b/src/Grpc.Net.Client/Internal/IGrpcCall.cs index 8272adb1e..07f26f8e0 100644 --- a/src/Grpc.Net.Client/Internal/IGrpcCall.cs +++ b/src/Grpc.Net.Client/Internal/IGrpcCall.cs @@ -21,7 +21,7 @@ namespace Grpc.Net.Client.Internal; -internal interface IGrpcCall : IDisposable, IMethod +internal interface IGrpcCall : IDisposable, IEnumerable> where TRequest : class where TResponse : class { diff --git a/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs b/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs index bab9fa974..e481dfda5 100644 --- a/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs +++ b/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs @@ -16,6 +16,7 @@ #endregion +using System.Collections; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Grpc.Core; @@ -35,6 +36,7 @@ internal abstract partial class RetryCallBase : IGrpcCall? _retryBaseClientStreamWriter; private Task? _responseTask; private Task? _responseHeadersTask; + private TRequest? _request; // Internal for unit testing. internal CancellationTokenRegistration? _ctsRegistration; @@ -64,11 +66,6 @@ internal abstract partial class RetryCallBase : IGrpcCall Method.Type; - string IMethod.ServiceName => Method.ServiceName; - string IMethod.Name => Method.Name; - string IMethod.FullName => Method.FullName; - protected RetryCallBase(GrpcChannel channel, Method method, CallOptions options, string loggerName, int retryAttempts) { Logger = channel.LoggerFactory.CreateLogger(loggerName); @@ -170,6 +167,7 @@ public Metadata GetTrailers() public void StartUnary(TRequest request) { + _request = request; StartCore(call => call.StartUnaryCore(CreatePushUnaryContent(request, call))); } @@ -520,7 +518,7 @@ internal void ClearRetryBuffer() protected StatusGrpcCall CreateStatusCall(Status status) { - var call = new StatusGrpcCall(status, Channel, Method, MessagesRead); + var call = new StatusGrpcCall(status, Channel, Method, MessagesRead, _request); call.CallWrapper = CallWrapper; return call; } @@ -595,4 +593,7 @@ public Exception CreateFailureStatusException(Status status) { throw new NotSupportedException(); } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerator> GetEnumerator() => GrpcProtocolConstants.GetDebugEnumerator(Channel, Method, _request); } diff --git a/src/Grpc.Net.Client/Internal/Retry/StatusGrpcCall.cs b/src/Grpc.Net.Client/Internal/Retry/StatusGrpcCall.cs index c1826a010..0b9426aaf 100644 --- a/src/Grpc.Net.Client/Internal/Retry/StatusGrpcCall.cs +++ b/src/Grpc.Net.Client/Internal/Retry/StatusGrpcCall.cs @@ -16,6 +16,7 @@ #endregion +using System.Collections; using System.Diagnostics.CodeAnalysis; using Grpc.Core; @@ -28,6 +29,7 @@ internal sealed class StatusGrpcCall : IGrpcCall _method; + private readonly TRequest? _request; private IClientStreamWriter? _clientStreamWriter; private IAsyncStreamReader? _clientStreamReader; @@ -39,17 +41,13 @@ internal sealed class StatusGrpcCall : IGrpcCall _method.Type; - string IMethod.ServiceName => _method.ServiceName; - string IMethod.Name => _method.Name; - string IMethod.FullName => _method.FullName; - - public StatusGrpcCall(Status status, GrpcChannel channel, Method method, int messagesRead) + public StatusGrpcCall(Status status, GrpcChannel channel, Method method, int messagesRead, TRequest? request) { _status = status; _channel = channel; _method = method; MessagesRead = messagesRead; + _request = request; } public void Dispose() @@ -124,6 +122,9 @@ public Exception CreateFailureStatusException(Status status) } } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerator> GetEnumerator() => GrpcProtocolConstants.GetDebugEnumerator(_channel, _method, _request); + private sealed class StatusClientStreamWriter : IClientStreamWriter { private readonly Status _status;