From 31eaaf8ed86702013bab136653bfc6f32fe423cf Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 17 Feb 2021 20:44:36 +1300 Subject: [PATCH] Support WinHttp --- src/Grpc.Net.Client/Internal/GrpcCall.cs | 20 +++-- .../Internal/Http/PushUnaryContent.cs | 8 +- ...UnaryContent.cs => WinHttpUnaryContent.cs} | 74 ++++++++++--------- .../Internal/Retry/RetryCallBase.cs | 12 ++- .../AsyncUnaryCallTests.cs | 34 +++++++++ .../Infrastructure/WinHttpHandler.cs | 28 +++++++ 6 files changed, 128 insertions(+), 48 deletions(-) rename src/Grpc.Net.Client/Internal/Http/{LengthUnaryContent.cs => WinHttpUnaryContent.cs} (60%) create mode 100644 test/Grpc.Net.Client.Tests/Infrastructure/WinHttpHandler.cs diff --git a/src/Grpc.Net.Client/Internal/GrpcCall.cs b/src/Grpc.Net.Client/Internal/GrpcCall.cs index ff5fc2a95..ff82d0c1a 100644 --- a/src/Grpc.Net.Client/Internal/GrpcCall.cs +++ b/src/Grpc.Net.Client/Internal/GrpcCall.cs @@ -106,10 +106,7 @@ public CancellationToken CancellationToken IClientStreamWriter? IGrpcCall.ClientStreamWriter => ClientStreamWriter; IAsyncStreamReader? IGrpcCall.ClientStreamReader => ClientStreamReader; - public void StartUnary(TRequest request) => StartUnaryCore(new PushUnaryContent(stream => - { - return WriteMessageAsync(stream, request, Options); - })); + public void StartUnary(TRequest request) => StartUnaryCore(CreatePushUnaryContent(request)); public void StartClientStreaming() { @@ -119,10 +116,19 @@ public void StartClientStreaming() StartClientStreamingCore(clientStreamWriter, content); } - public void StartServerStreaming(TRequest request) => StartServerStreamingCore(new PushUnaryContent(stream => + public void StartServerStreaming(TRequest request) => StartServerStreamingCore(CreatePushUnaryContent(request)); + + private HttpContent CreatePushUnaryContent(TRequest request) { - return WriteMessageAsync(stream, request, Options); - })); + return !Channel.IsWinHttp + ? new PushUnaryContent(request, WriteAsync) + : new WinHttpUnaryContent(request, WriteAsync, this); + + ValueTask WriteAsync(TRequest request, Stream stream) + { + return WriteMessageAsync(stream, request, Options); + } + } public void StartDuplexStreaming() { diff --git a/src/Grpc.Net.Client/Internal/Http/PushUnaryContent.cs b/src/Grpc.Net.Client/Internal/Http/PushUnaryContent.cs index d7f3a92e4..01a9ac761 100644 --- a/src/Grpc.Net.Client/Internal/Http/PushUnaryContent.cs +++ b/src/Grpc.Net.Client/Internal/Http/PushUnaryContent.cs @@ -33,10 +33,12 @@ internal class PushUnaryContent : HttpContent where TRequest : class where TResponse : class { - private readonly Func _startCallback; + private readonly TRequest _request; + private readonly Func _startCallback; - public PushUnaryContent(Func startCallback) + public PushUnaryContent(TRequest request, Func startCallback) { + _request = request; _startCallback = startCallback; Headers.ContentType = GrpcProtocolConstants.GrpcContentTypeHeaderValue; } @@ -44,7 +46,7 @@ public PushUnaryContent(Func startCallback) protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) { #pragma warning disable CA2012 // Use ValueTasks correctly - var writeMessageTask = _startCallback(stream); + var writeMessageTask = _startCallback(_request, stream); #pragma warning restore CA2012 // Use ValueTasks correctly if (writeMessageTask.IsCompletedSuccessfully()) { diff --git a/src/Grpc.Net.Client/Internal/Http/LengthUnaryContent.cs b/src/Grpc.Net.Client/Internal/Http/WinHttpUnaryContent.cs similarity index 60% rename from src/Grpc.Net.Client/Internal/Http/LengthUnaryContent.cs rename to src/Grpc.Net.Client/Internal/Http/WinHttpUnaryContent.cs index cdd1d3b1d..945b6c2aa 100644 --- a/src/Grpc.Net.Client/Internal/Http/LengthUnaryContent.cs +++ b/src/Grpc.Net.Client/Internal/Http/WinHttpUnaryContent.cs @@ -37,64 +37,66 @@ namespace Grpc.Net.Client.Internal.Http /// The payload is then written directly to the request using specialized context /// and serializer method. /// - internal class LengthUnaryContent : HttpContent + internal class WinHttpUnaryContent : HttpContent where TRequest : class where TResponse : class { - private readonly TRequest _content; + private readonly TRequest _request; + private readonly Func _startCallback; private readonly GrpcCall _call; - private byte[]? _payload; - public LengthUnaryContent(TRequest content, GrpcCall call, MediaTypeHeaderValue mediaType) + public WinHttpUnaryContent(TRequest request, Func startCallback, GrpcCall call) { - _content = content; + _request = request; + _startCallback = startCallback; _call = call; - Headers.ContentType = mediaType; + Headers.ContentType = GrpcProtocolConstants.GrpcContentTypeHeaderValue; } - // Serialize message. Need to know size to prefix the length in the header. - private byte[] SerializePayload() + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) { - var serializationContext = _call.SerializationContext; - serializationContext.CallOptions = _call.Options; - serializationContext.Initialize(); - - try - { - _call.Method.RequestMarshaller.ContextualSerializer(_content, serializationContext); - - return serializationContext.GetWrittenPayload().ToArray(); - } - finally +#pragma warning disable CA2012 // Use ValueTasks correctly + var writeMessageTask = _startCallback(_request, stream); +#pragma warning restore CA2012 // Use ValueTasks correctly + if (writeMessageTask.IsCompletedSuccessfully()) { - serializationContext.Reset(); + GrpcEventSource.Log.MessageSent(); + return Task.CompletedTask; } + + return WriteMessageCore(writeMessageTask); } - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) + private static async Task WriteMessageCore(ValueTask writeMessageTask) { - if (_payload == null) - { - _payload = SerializePayload(); - } - - await _call.WriteMessageAsync( - stream, - _payload, - _call.Options.CancellationToken).ConfigureAwait(false); - + await writeMessageTask.ConfigureAwait(false); GrpcEventSource.Log.MessageSent(); } protected override bool TryComputeLength(out long length) { - if (_payload == null) + // This will serialize the request message again. + // Consider caching serialized content if it is a problem. + length = GetPayloadLength(); + return true; + } + + private int GetPayloadLength() + { + var serializationContext = _call.SerializationContext; + serializationContext.CallOptions = _call.Options; + serializationContext.Initialize(); + + try { - _payload = SerializePayload(); - } + _call.Method.RequestMarshaller.ContextualSerializer(_request, serializationContext); - length = _payload.Length; - return true; + return serializationContext.GetWrittenPayload().Length; + } + finally + { + serializationContext.Reset(); + } } } } diff --git a/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs b/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs index 7c0d4ddc9..97000254e 100644 --- a/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs +++ b/src/Grpc.Net.Client/Internal/Retry/RetryCallBase.cs @@ -21,6 +21,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Grpc.Core; @@ -168,9 +169,16 @@ public void StartDuplexStreaming() }); } - private PushUnaryContent CreatePushUnaryContent(TRequest request, GrpcCall call) + private HttpContent CreatePushUnaryContent(TRequest request, GrpcCall call) { - return new PushUnaryContent(stream => WriteNewMessage(call, stream, call.Options, request)); + return !Channel.IsWinHttp + ? new PushUnaryContent(request, WriteAsync) + : new WinHttpUnaryContent(request, WriteAsync, call); + + ValueTask WriteAsync(TRequest request, Stream stream) + { + return WriteNewMessage(call, stream, call.Options, request); + } } private PushStreamContent CreatePushStreamContent(GrpcCall call, HttpContentClientStreamWriter clientStreamWriter) diff --git a/test/Grpc.Net.Client.Tests/AsyncUnaryCallTests.cs b/test/Grpc.Net.Client.Tests/AsyncUnaryCallTests.cs index 966b4ddea..4720c2edd 100644 --- a/test/Grpc.Net.Client.Tests/AsyncUnaryCallTests.cs +++ b/test/Grpc.Net.Client.Tests/AsyncUnaryCallTests.cs @@ -72,6 +72,7 @@ public async Task AsyncUnaryCall_Success_HttpRequestMessagePopulated() Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content?.Headers?.ContentType); Assert.AreEqual(GrpcProtocolConstants.TEHeaderValue, httpRequestMessage.Headers.TE.Single().Value); Assert.AreEqual("identity,gzip", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.MessageAcceptEncodingHeader).Single()); + Assert.AreEqual(null, httpRequestMessage!.Content!.Headers!.ContentLength); var userAgent = httpRequestMessage.Headers.UserAgent.Single()!; Assert.AreEqual("grpc-dotnet", userAgent.Product?.Name); @@ -83,6 +84,39 @@ public async Task AsyncUnaryCall_Success_HttpRequestMessagePopulated() Assert.IsTrue(userAgent.Product!.Version!.Length <= 10); } + [Test] + public async Task AsyncUnaryCall_HasWinHttpHandler_ContentLengthOnHttpRequestMessagePopulated() + { + // Arrange + HttpRequestMessage? httpRequestMessage = null; + + var handler = TestHttpMessageHandler.Create(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await ClientTestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + // Just need to have a type called WinHttpHandler to activate new behavior. + var winHttpHandler = new WinHttpHandler(handler); + var invoker = HttpClientCallInvokerFactory.Create(winHttpHandler, "https://localhost"); + + // Act + var rs = await invoker.AsyncUnaryCall(ClientTestHelpers.ServiceMethod, string.Empty, new CallOptions(), new HelloRequest { Name = "Hello world" }); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(18, httpRequestMessage!.Content!.Headers!.ContentLength); + } + [Test] public async Task AsyncUnaryCall_Success_RequestContentSent() { diff --git a/test/Grpc.Net.Client.Tests/Infrastructure/WinHttpHandler.cs b/test/Grpc.Net.Client.Tests/Infrastructure/WinHttpHandler.cs new file mode 100644 index 000000000..06f6a1c66 --- /dev/null +++ b/test/Grpc.Net.Client.Tests/Infrastructure/WinHttpHandler.cs @@ -0,0 +1,28 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +// Namespace and class name needs to resolve to System.Net.Http.WinHttpHandler. +namespace System.Net.Http +{ + public class WinHttpHandler : DelegatingHandler + { + public WinHttpHandler(HttpMessageHandler innerHandler) : base(innerHandler) + { + } + } +}