diff --git a/Directory.Build.props b/Directory.Build.props
index b8e6f2fcd..7d05141e9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -9,6 +9,9 @@
$(WarningsNotAsErrors);CS1591
+
+
+ preview
diff --git a/Grpc.AspNetCore.sln b/Grpc.AspNetCore.sln
index 32cad6ca8..bbddb2873 100644
--- a/Grpc.AspNetCore.sln
+++ b/Grpc.AspNetCore.sln
@@ -56,11 +56,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Proto", "Proto", "{BF1393D4-6099-4EF9-85BB-7EE6CBEB920C}"
ProjectSection(SolutionItems) = preProject
+ testassets\Proto\authorize.proto = testassets\Proto\authorize.proto
testassets\Proto\chat.proto = testassets\Proto\chat.proto
testassets\Proto\compression.proto = testassets\Proto\compression.proto
testassets\Proto\count.proto = testassets\Proto\count.proto
+ testassets\Proto\empty.proto = testassets\Proto\empty.proto
testassets\Proto\greet.proto = testassets\Proto\greet.proto
+ testassets\Proto\lifetime.proto = testassets\Proto\lifetime.proto
testassets\Proto\message.proto = testassets\Proto\message.proto
+ testassets\Proto\messages.proto = testassets\Proto\messages.proto
+ testassets\Proto\nested.proto = testassets\Proto\nested.proto
+ testassets\Proto\singleton.proto = testassets\Proto\singleton.proto
+ testassets\Proto\test.proto = testassets\Proto\test.proto
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionalTestsWebsite", "testassets\FunctionalTestsWebsite\FunctionalTestsWebsite.csproj", "{7B95289B-4992-4C0D-B26F-8EC58F81FC96}"
@@ -110,6 +117,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.AspNetCore.Server.Refl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reflector", "examples\Clients\Reflector\Reflector.csproj", "{86AD33E9-2C07-45BD-B599-420C2618188D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropTestsClient", "testassets\InteropTestsClient\InteropTestsClient.csproj", "{291E5BA5-608D-406D-A2DB-389412D907F3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.NetCore.HttpClient.Tests", "test\Grpc.NetCore.HttpClient.Tests\Grpc.NetCore.HttpClient.Tests.csproj", "{2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -188,6 +199,14 @@ Global
{86AD33E9-2C07-45BD-B599-420C2618188D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{86AD33E9-2C07-45BD-B599-420C2618188D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{86AD33E9-2C07-45BD-B599-420C2618188D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.Build.0 = Release|Any CPU
+ {291E5BA5-608D-406D-A2DB-389412D907F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {291E5BA5-608D-406D-A2DB-389412D907F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {291E5BA5-608D-406D-A2DB-389412D907F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {291E5BA5-608D-406D-A2DB-389412D907F3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -221,6 +240,8 @@ Global
{39320CA8-D8F0-45B6-B704-A04C16870226} = {310E5783-455A-4D09-A7AE-39DC2AB09504}
{55813F20-1269-4B19-B03E-7E4A90148F92} = {8C62055F-8CD7-4859-9001-634D544DF2AE}
{86AD33E9-2C07-45BD-B599-420C2618188D} = {F6E0F9D7-64E5-4C7B-A9BC-3C2AD687710B}
+ {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49} = {CECC4AE8-9C4E-4727-939B-517CC2E58D65}
+ {291E5BA5-608D-406D-A2DB-389412D907F3} = {59C7B1F0-EE4D-4098-8596-0ADDBC305234}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CD5C2B19-49B4-480A-990C-36D98A719B07}
diff --git a/build/dependencies.props b/build/dependencies.props
index edae8b3f7..090dc3daa 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -1,6 +1,7 @@
0.11.3
+ 3.0.0
3.7.0
1.20.0-pre3
1.20.0-pre3
@@ -12,7 +13,7 @@
16.0.0-preview-20181205-02
1.0.0-beta2-18618-05
4.10.0
- 12.0.1
+ 12.0.2
3.11.0
3.12.0
4.6.0-preview.19073.11
diff --git a/build/sources.props b/build/sources.props
index 104d54c7c..2d2fb609b 100644
--- a/build/sources.props
+++ b/build/sources.props
@@ -4,6 +4,7 @@
$(RestoreSources);
https://api.nuget.org/v3/index.json;
https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json;
+ https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json;
$(RestoreSources);
diff --git a/examples/Clients/Counter/Counter.csproj b/examples/Clients/Counter/Counter.csproj
index e18ad41a9..10d1873c5 100644
--- a/examples/Clients/Counter/Counter.csproj
+++ b/examples/Clients/Counter/Counter.csproj
@@ -3,7 +3,6 @@
Exe
netcoreapp3.0
- latest
@@ -22,4 +21,5 @@
+
diff --git a/examples/Clients/Counter/Program.cs b/examples/Clients/Counter/Program.cs
index 0fd565f12..e6c4e8937 100644
--- a/examples/Clients/Counter/Program.cs
+++ b/examples/Clients/Counter/Program.cs
@@ -17,6 +17,7 @@
#endregion
using System;
+using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Common;
@@ -36,9 +37,24 @@ static async Task Main(string[] args)
var channel = new Channel("localhost:50051", credentials);
var client = new Counter.CounterClient(channel);
- var reply = client.IncrementCount(new Google.Protobuf.WellKnownTypes.Empty());
+ await UnaryCallExample(client);
+
+ await ClientStreamingCallExample(client);
+
+ Console.WriteLine("Shutting down");
+ await channel.ShutdownAsync();
+ Console.WriteLine("Press any key to exit...");
+ Console.ReadKey();
+ }
+
+ private static async Task UnaryCallExample(Counter.CounterClient client)
+ {
+ var reply = await client.IncrementCountAsync(new Google.Protobuf.WellKnownTypes.Empty());
Console.WriteLine("Count: " + reply.Count);
+ }
+ private static async Task ClientStreamingCallExample(Counter.CounterClient client)
+ {
using (var call = client.AccumulateCount())
{
for (int i = 0; i < 3; i++)
@@ -50,13 +66,10 @@ static async Task Main(string[] args)
}
await call.RequestStream.CompleteAsync();
- Console.WriteLine($"Count: {(await call.ResponseAsync).Count}");
- }
- Console.WriteLine("Shutting down");
- await channel.ShutdownAsync();
- Console.WriteLine("Press any key to exit...");
- Console.ReadKey();
+ var response = await call;
+ Console.WriteLine($"Count: {response.Count}");
+ }
}
}
}
diff --git a/examples/Clients/Greeter/Greeter.csproj b/examples/Clients/Greeter/Greeter.csproj
index 20faebda9..c11bb2333 100644
--- a/examples/Clients/Greeter/Greeter.csproj
+++ b/examples/Clients/Greeter/Greeter.csproj
@@ -3,7 +3,6 @@
Exe
netcoreapp3.0
- latest
@@ -22,4 +21,5 @@
+
diff --git a/examples/Clients/Mailer/Mailer.csproj b/examples/Clients/Mailer/Mailer.csproj
index 4f96ecea4..621e8dc73 100644
--- a/examples/Clients/Mailer/Mailer.csproj
+++ b/examples/Clients/Mailer/Mailer.csproj
@@ -3,7 +3,6 @@
Exe
netcoreapp3.0
- latest
diff --git a/examples/Clients/Mailer/Program.cs b/examples/Clients/Mailer/Program.cs
index 0b454e061..7bcefb37e 100644
--- a/examples/Clients/Mailer/Program.cs
+++ b/examples/Clients/Mailer/Program.cs
@@ -43,7 +43,7 @@ static async Task Main(string[] args)
await channel.ConnectAsync();
Console.WriteLine("Connected");
- Console.WriteLine("Press escape to exit. Press any other key to forward mail.");
+ Console.WriteLine("Press escape to disconnect. Press any other key to forward mail.");
var client = new Mailer.MailerClient(channel);
using (var mailbox = client.Mailbox(headers: new Metadata { new Metadata.Entry("mailbox-name", mailboxName) }))
@@ -78,6 +78,9 @@ static async Task Main(string[] args)
Console.WriteLine("Disconnecting");
await channel.ShutdownAsync();
+
+ Console.WriteLine("Disconnected. Press any key to exit.");
+ Console.ReadKey();
}
private static string GetMailboxName(string[] args)
diff --git a/examples/Clients/Reflector/Reflector.csproj b/examples/Clients/Reflector/Reflector.csproj
index ea7110ca9..d23ab6d4b 100644
--- a/examples/Clients/Reflector/Reflector.csproj
+++ b/examples/Clients/Reflector/Reflector.csproj
@@ -3,7 +3,6 @@
Exe
netcoreapp3.0
- latest
diff --git a/examples/Server/Services/Greeter.cs b/examples/Server/Services/Greeter.cs
index 7fa9a7984..695f9c26a 100644
--- a/examples/Server/Services/Greeter.cs
+++ b/examples/Server/Services/Greeter.cs
@@ -34,9 +34,6 @@ public GreeterService(ILoggerFactory loggerFactory)
//Server side handler of the SayHello RPC
public override Task SayHello(HelloRequest request, ServerCallContext context)
{
- var httpContext = context.GetHttpContext();
- _logger.LogInformation($"Connection id: {httpContext.Connection.Id}");
-
_logger.LogInformation($"Sending hello to {request.Name}");
return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
}
diff --git a/global.json b/global.json
index 04bcb8999..aa30320ef 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"sdk": {
- "version": "3.0.100-preview4-011108"
+ "version": "3.0.100-preview6-011568"
}
}
diff --git a/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj b/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj
index 5ed86e53e..494916146 100644
--- a/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj
+++ b/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj
@@ -3,7 +3,6 @@
Exe
netcoreapp3.0
- latest
diff --git a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj
index b769e7b29..a5788cf7c 100644
--- a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj
+++ b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj
@@ -4,7 +4,6 @@
netcoreapp3.0
$(BenchmarksTargetFramework)
Exe
- latest
InProcess
@@ -35,5 +34,6 @@
+
diff --git a/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs b/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs
index c62aa9ab0..162fd1bb2 100644
--- a/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs
+++ b/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs
@@ -205,7 +205,7 @@ public static void SetStatusTrailers(HttpResponse response, Status status)
{
// Use SetTrailer here because we want to overwrite any that was set earlier
SetTrailer(response, GrpcProtocolConstants.StatusTrailer, status.StatusCode.ToTrailerString());
- SetTrailer(response, GrpcProtocolConstants.MessageTrailer, status.Detail);
+ SetTrailer(response, GrpcProtocolConstants.MessageTrailer, !string.IsNullOrEmpty(status.Detail) ? status.Detail : null);
}
private static void SetTrailer(HttpResponse response, string trailerName, StringValues trailerValues)
diff --git a/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs b/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs
deleted file mode 100644
index a608f9b56..000000000
--- a/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-#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
-
-using System;
-using System.IO;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Grpc.Core;
-
-namespace Grpc.NetCore.HttpClient
-{
- internal class ClientAsyncStreamReader : IAsyncStreamReader
- {
- private Task _sendTask;
- private Stream _responseStream;
- private Func _deserializer;
-
- public ClientAsyncStreamReader(Task sendTask, Func deserializer)
- {
- _sendTask = sendTask;
- _deserializer = deserializer;
- }
-
- public TResponse Current { get; private set; }
-
- public void Dispose()
- {
- }
-
- public async Task MoveNext(CancellationToken cancellationToken)
- {
- try
- {
- if (_responseStream == null)
- {
- var responseMessage = await _sendTask;
- _responseStream = await responseMessage.Content.ReadAsStreamAsync();
- }
-
- Current = _responseStream.ReadSingleMessage(_deserializer);
- return true;
- }
- catch
- {
- return false;
- }
- }
- }
-}
diff --git a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj
index 88ce1c68f..57a36a628 100644
--- a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj
+++ b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj
@@ -12,15 +12,18 @@
- netstandard2.0
+
+ netcoreapp3.0
8.0
true
true
+
+
+
-
diff --git a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs
index 394f0fc3e..cca471861 100644
--- a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs
+++ b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs
@@ -42,14 +42,37 @@ public static TClient Create(string baseAddress, X509Certificate certif
// Needs to be set before creating the HttpClientHandler
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
- var handler = new HttpClientHandler();
- handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true;
+ var httpClientHandler = new HttpClientHandler();
+ httpClientHandler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true;
if (certificate != null)
{
- handler.ClientCertificates.Add(certificate);
+ httpClientHandler.ClientCertificates.Add(certificate);
}
- return Cache.Instance.Activator(new HttpClientCallInvoker(handler, new Uri(baseAddress, UriKind.RelativeOrAbsolute)));
+ return CreateCore(baseAddress, httpClientHandler);
+ }
+
+ ///
+ /// Creates a gRPC client using the specified address and handler.
+ ///
+ /// The type of the gRPC client. This type will typically be defined using generated code from a *.proto file.
+ /// The base address to use when making gRPC requests.
+ /// The .
+ /// A gRPC client.
+ public static TClient Create(string baseAddress, HttpClientHandler httpClientHandler) where TClient : ClientBase
+ {
+ return CreateCore(baseAddress, httpClientHandler);
+ }
+
+ private static TClient CreateCore(string baseAddress, HttpClientHandler httpClientHandler) where TClient : ClientBase
+ {
+ // Needs to be set before creating the HttpClientHandler
+ AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
+
+ var httpClient = new System.Net.Http.HttpClient(httpClientHandler);
+ httpClient.BaseAddress = new Uri(baseAddress, UriKind.RelativeOrAbsolute);
+
+ return Cache.Instance.Activator(new HttpClientCallInvoker(httpClient));
}
private class Cache
diff --git a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs
index 63dbae751..1e9f37f2f 100644
--- a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs
+++ b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs
@@ -21,6 +21,7 @@
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
+using Grpc.NetCore.HttpClient.Internal;
namespace Grpc.NetCore.HttpClient
{
@@ -31,16 +32,8 @@ public class HttpClientCallInvoker : CallInvoker
{
private System.Net.Http.HttpClient _client;
- ///
- /// Initializes a new instance of the class.
- ///
- /// The primary client handler to use for gRPC requests.
- /// The base address to use when making gRPC requests.
- public HttpClientCallInvoker(HttpClientHandler handler, Uri baseAddress)
- {
- _client = new System.Net.Http.HttpClient(handler);
- _client.BaseAddress = baseAddress;
- }
+ // Override the current time for unit testing
+ internal ISystemClock Clock = SystemClock.Instance;
///
/// Initializes a new instance of the class.
@@ -80,22 +73,16 @@ public HttpClientCallInvoker(System.Net.Http.HttpClient client)
///
public override AsyncClientStreamingCall AsyncClientStreamingCall(Method method, string host, CallOptions options)
{
- var pipeContent = new PipeContent();
- var message = new HttpRequestMessage(HttpMethod.Post, method.FullName);
- message.Content = pipeContent;
- message.Version = new Version(2, 0);
-
- var sendTask = SendRequestMessageAsync(() => Task.CompletedTask, _client, message);
+ var call = CreateGrpcCall(method, options);
+ call.StartClientStreaming(_client);
return new AsyncClientStreamingCall(
- requestStream: new PipeClientStreamWriter(pipeContent.PipeWriter, method.RequestMarshaller.Serializer, options.WriteOptions),
- responseAsync: GetResponseAsync(sendTask, method.ResponseMarshaller.Deserializer),
- responseHeadersAsync: GetResponseHeadersAsync(sendTask),
- // Cannot implement due to trailers being unimplemented
- getStatusFunc: () => new Status(),
- // Cannot implement due to trailers being unimplemented
- getTrailersFunc: () => new Metadata(),
- disposeAction: () => { });
+ requestStream: call.ClientStreamWriter,
+ responseAsync: call.GetResponseAsync(),
+ responseHeadersAsync: call.GetResponseHeadersAsync(),
+ getStatusFunc: call.GetStatus,
+ getTrailersFunc: call.GetTrailers,
+ disposeAction: call.Dispose);
}
///
@@ -105,22 +92,16 @@ public override AsyncClientStreamingCall AsyncClientStreami
///
public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall(Method method, string host, CallOptions options)
{
- var pipeContent = new PipeContent();
- var message = new HttpRequestMessage(HttpMethod.Post, method.FullName);
- message.Content = pipeContent;
- message.Version = new Version(2, 0);
-
- var sendTask = SendRequestMessageAsync(() => Task.CompletedTask, _client, message);
+ var call = CreateGrpcCall(method, options);
+ call.StartDuplexStreaming(_client);
return new AsyncDuplexStreamingCall(
- requestStream: new PipeClientStreamWriter(pipeContent.PipeWriter, method.RequestMarshaller.Serializer, options.WriteOptions),
- responseStream: new ClientAsyncStreamReader(sendTask, method.ResponseMarshaller.Deserializer),
- responseHeadersAsync: GetResponseHeadersAsync(sendTask),
- // Cannot implement due to trailers being unimplemented
- getStatusFunc: () => new Status(),
- // Cannot implement due to trailers being unimplemented
- getTrailersFunc: () => new Metadata(),
- disposeAction: () => { });
+ requestStream: call.ClientStreamWriter,
+ responseStream: call.ClientStreamReader,
+ responseHeadersAsync: call.GetResponseHeadersAsync(),
+ getStatusFunc: call.GetStatus,
+ getTrailersFunc: call.GetTrailers,
+ disposeAction: call.Dispose);
}
///
@@ -129,28 +110,15 @@ public override AsyncDuplexStreamingCall AsyncDuplexStreami
///
public override AsyncServerStreamingCall AsyncServerStreamingCall(Method method, string host, CallOptions options, TRequest request)
{
- var content = new PipeContent();
- var message = new HttpRequestMessage(HttpMethod.Post, method.FullName);
- message.Content = content;
- message.Version = new Version(2, 0);
-
- // Write request body
- var sendTask = SendRequestMessageAsync(
- async () =>
- {
- await content.PipeWriter.WriteMessageCoreAsync(method.RequestMarshaller.Serializer(request), true);
- content.PipeWriter.Complete();
- },
- _client, message);
+ var call = CreateGrpcCall(method, options);
+ call.StartServerStreaming(_client, request);
return new AsyncServerStreamingCall(
- responseStream: new ClientAsyncStreamReader(sendTask, method.ResponseMarshaller.Deserializer),
- responseHeadersAsync: GetResponseHeadersAsync(sendTask),
- // Cannot implement due to trailers being unimplemented
- getStatusFunc: () => new Status(),
- // Cannot implement due to trailers being unimplemented
- getTrailersFunc: () => new Metadata(),
- disposeAction: () => { });
+ responseStream: call.ClientStreamReader,
+ responseHeadersAsync: call.GetResponseHeadersAsync(),
+ getStatusFunc: call.GetStatus,
+ getTrailersFunc: call.GetTrailers,
+ disposeAction: call.Dispose);
}
///
@@ -158,28 +126,15 @@ public override AsyncServerStreamingCall AsyncServerStreamingCall
public override AsyncUnaryCall AsyncUnaryCall(Method method, string host, CallOptions options, TRequest request)
{
- var content = new PipeContent();
- var message = new HttpRequestMessage(HttpMethod.Post, method.FullName);
- message.Content = content;
- message.Version = new Version(2, 0);
-
- // Write request body
- var sendTask = SendRequestMessageAsync(
- async () =>
- {
- await content.PipeWriter.WriteMessageCoreAsync(method.RequestMarshaller.Serializer(request), true);
- content.PipeWriter.Complete();
- },
- _client, message);
+ var call = CreateGrpcCall(method, options);
+ call.StartUnary(_client, request);
return new AsyncUnaryCall(
- responseAsync: GetResponseAsync(sendTask, method.ResponseMarshaller.Deserializer),
- responseHeadersAsync: GetResponseHeadersAsync(sendTask),
- // Cannot implement due to trailers being unimplemented
- getStatusFunc: () => new Status(),
- // Cannot implement due to trailers being unimplemented
- getTrailersFunc: () => new Metadata(),
- disposeAction: () => { });
+ responseAsync: call.GetResponseAsync(),
+ responseHeadersAsync: call.GetResponseHeadersAsync(),
+ getStatusFunc: call.GetStatus,
+ getTrailersFunc: call.GetTrailers,
+ disposeAction: call.Dispose);
}
///
@@ -187,74 +142,13 @@ public override AsyncUnaryCall AsyncUnaryCall(Me
///
public override TResponse BlockingUnaryCall(Method method, string host, CallOptions options, TRequest request)
{
- return AsyncUnaryCall(method, host, options, request)?.GetAwaiter().GetResult();
- }
-
- private static async Task SendRequestMessageAsync(Func writeMessageTask, System.Net.Http.HttpClient client, HttpRequestMessage message)
- {
- await writeMessageTask();
- return await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead);
- }
-
- private static async Task GetResponseAsync(Task sendTask, Func deserializer)
- {
- // We can't use pipes here since we can't control how much is read and response trailers causes InvalidOperationException
- var response = await sendTask;
- var responseStream = await response.Content.ReadAsStreamAsync();
-
- return responseStream.ReadSingleMessage(deserializer);
+ var call = AsyncUnaryCall(method, host, options, request);
+ return call.ResponseAsync.GetAwaiter().GetResult();
}
- private static async Task GetResponseHeadersAsync(Task sendTask)
+ private GrpcCall CreateGrpcCall(Method method, CallOptions options)
{
- var response = await sendTask;
-
- var headers = new Metadata();
-
- foreach (var header in response.Headers)
- {
- // ASP.NET Core includes pseudo headers in the set of request headers
- // whereas, they are not in gRPC implementations. We will filter them
- // out when we construct the list of headers on the context.
- if (header.Key.StartsWith(":", StringComparison.Ordinal))
- {
- continue;
- }
- else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase))
- {
- headers.Add(header.Key, ParseBinaryHeader(string.Join(",", header.Value)));
- }
- else
- {
- headers.Add(header.Key, string.Join(",", header.Value));
- }
- }
- return null;
- }
-
- private static byte[] ParseBinaryHeader(string base64)
- {
- string decodable;
- switch (base64.Length % 4)
- {
- case 0:
- // base64 has the required padding
- decodable = base64;
- break;
- case 2:
- // 2 chars padding
- decodable = base64 + "==";
- break;
- case 3:
- // 3 chars padding
- decodable = base64 + "=";
- break;
- default:
- // length%4 == 1 should be illegal
- throw new FormatException("Invalid base64 header value");
- }
-
- return Convert.FromBase64String(decodable);
+ return new GrpcCall(method, options, Clock);
}
}
}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs
new file mode 100644
index 000000000..db76ef9ba
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs
@@ -0,0 +1,486 @@
+#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
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using Grpc.Core;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal class GrpcCall : IDisposable
+ {
+ private readonly CancellationTokenSource _callCts;
+ private readonly CancellationTokenRegistration? _ctsRegistration;
+ private readonly ISystemClock _clock;
+ private readonly TimeSpan? _timeout;
+ private readonly Timer _deadlineTimer;
+ private Metadata _trailers;
+ private CancellationTokenRegistration? _writerCtsRegistration;
+ private string _headerValidationError;
+
+ public bool DeadlineReached { get; private set; }
+ public bool Disposed { get; private set; }
+ public bool ResponseFinished { get; private set; }
+ public HttpResponseMessage HttpResponse { get; private set; }
+ public CallOptions Options { get; }
+ public Method Method { get; }
+ public Task SendTask { get; private set; }
+ public HttpContentClientStreamWriter ClientStreamWriter { get; private set; }
+ public HttpContentClientStreamReader ClientStreamReader { get; private set; }
+
+ public GrpcCall(Method method, CallOptions options, ISystemClock clock)
+ {
+ // Validate deadline before creating any objects that require cleanup
+ ValidateDeadline(options.Deadline);
+
+ _callCts = new CancellationTokenSource();
+ Method = method;
+ Options = options;
+ _clock = clock;
+
+ if (options.CancellationToken.CanBeCanceled)
+ {
+ // The cancellation token will cancel the call CTS
+ _ctsRegistration = options.CancellationToken.Register(CancelCall);
+ }
+
+ if (options.Deadline != null && options.Deadline != DateTime.MaxValue)
+ {
+ var timeout = options.Deadline.Value - _clock.UtcNow;
+ _timeout = (timeout > TimeSpan.Zero) ? timeout : TimeSpan.Zero;
+ }
+
+ if (_timeout != null)
+ {
+ // Deadline timer will cancel the call CTS
+ _deadlineTimer = new Timer(DeadlineExceeded, null, _timeout.Value, Timeout.InfiniteTimeSpan);
+ }
+ }
+
+ private void ValidateDeadline(DateTime? deadline)
+ {
+ if (deadline != null && deadline != DateTime.MaxValue && deadline != DateTime.MinValue && deadline.Value.Kind != DateTimeKind.Utc)
+ {
+ throw new InvalidOperationException("Deadline must have a kind DateTimeKind.Utc or be equal to DateTime.MaxValue or DateTime.MinValue.");
+ }
+ }
+
+ public CancellationToken CancellationToken
+ {
+ get { return _callCts.Token; }
+ }
+
+ public bool IsCancellationRequested
+ {
+ get { return _callCts.IsCancellationRequested; }
+ }
+
+ public void StartUnary(System.Net.Http.HttpClient client, TRequest request)
+ {
+ var message = CreateHttpRequestMessage();
+ SetMessageContent(request, message);
+ StartSend(client, message);
+ }
+
+ public void StartClientStreaming(System.Net.Http.HttpClient client)
+ {
+ var message = CreateHttpRequestMessage();
+ ClientStreamWriter = CreateWriter(message);
+ StartSend(client, message);
+ }
+
+ public void StartServerStreaming(System.Net.Http.HttpClient client, TRequest request)
+ {
+ var message = CreateHttpRequestMessage();
+ SetMessageContent(request, message);
+ StartSend(client, message);
+ ClientStreamReader = new HttpContentClientStreamReader(this);
+ }
+
+ public void StartDuplexStreaming(System.Net.Http.HttpClient client)
+ {
+ var message = CreateHttpRequestMessage();
+ ClientStreamWriter = CreateWriter(message);
+ StartSend(client, message);
+ ClientStreamReader = new HttpContentClientStreamReader(this);
+ }
+
+ ///
+ /// Dispose can be called by:
+ /// 1. The user. AsyncUnaryCall.Dispose et al will call this Dispose
+ /// 2. will call dispose if errors fail validation
+ /// 3. will call dispose
+ ///
+ public void Dispose()
+ {
+ if (!Disposed)
+ {
+ Disposed = true;
+
+ if (!ResponseFinished)
+ {
+ // If the response is not finished then cancel any pending actions:
+ // 1. Call HttpClient.SendAsync
+ // 2. Response Stream.ReadAsync
+ // 3. Client stream
+ // - Getting the Stream from the Request.HttpContent
+ // - Holding the Request.HttpContent.SerializeToStream open
+ // - Writing to the client stream
+ _callCts.Cancel();
+ }
+
+ _ctsRegistration?.Dispose();
+ _writerCtsRegistration?.Dispose();
+ _deadlineTimer?.Dispose();
+ HttpResponse?.Dispose();
+ ClientStreamReader?.Dispose();
+ ClientStreamWriter?.Dispose();
+
+ // To avoid racing with Dispose, skip disposing the call CTS
+ // This avoid Dispose potentially calling cancel on a disposed CTS
+ // The call CTS is not exposed externally and all dependent registrations
+ // are cleaned up
+ }
+ }
+
+ public void EnsureNotDisposed()
+ {
+ if (Disposed)
+ {
+ throw new ObjectDisposedException(nameof(GrpcCall));
+ }
+ }
+
+ public void EnsureHeadersValid()
+ {
+ if (_headerValidationError != null)
+ {
+ throw new InvalidOperationException(_headerValidationError);
+ }
+ }
+
+ public Exception CreateCanceledStatusException()
+ {
+ if (_headerValidationError != null)
+ {
+ return new InvalidOperationException(_headerValidationError);
+ }
+
+ var statusCode = DeadlineReached ? StatusCode.DeadlineExceeded : StatusCode.Cancelled;
+ return new RpcException(new Status(statusCode, string.Empty));
+ }
+
+ ///
+ /// Marks the response as finished, i.e. all response content has been read and trailers are available.
+ /// Can be called by for unary and client streaming calls, or
+ ///
+ /// for server streaming and duplex streaming calls.
+ ///
+ public void FinishResponse()
+ {
+ ResponseFinished = true;
+
+ try
+ {
+ // Get status from response before dispose
+ // This may throw an error if the grpc-status is missing or malformed
+ var status = GetStatusCore(HttpResponse);
+
+ if (status.StatusCode != StatusCode.OK)
+ {
+ throw new RpcException(status);
+ }
+ }
+ finally
+ {
+ // Clean up call resources once this call is finished
+ // Call may not be explicitly disposed when used with unary methods
+ // e.g. var reply = await client.SayHelloAsync(new HelloRequest());
+ Dispose();
+ }
+ }
+
+ public async Task GetResponseHeadersAsync()
+ {
+ try
+ {
+ await SendTask.ConfigureAwait(false);
+
+ // The task of this method is cached so there is no need to cache the headers here
+ return GrpcProtocolHelpers.BuildMetadata(HttpResponse.Headers);
+ }
+ catch (OperationCanceledException)
+ {
+ EnsureNotDisposed();
+ throw CreateCanceledStatusException();
+ }
+ }
+
+ public Status GetStatus()
+ {
+ ValidateTrailersAvailable();
+
+ return GetStatusCore(HttpResponse);
+ }
+
+ public async Task GetResponseAsync()
+ {
+ try
+ {
+ await SendTask.ConfigureAwait(false);
+
+ // Trailers are only available once the response body had been read
+ var responseStream = await HttpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false);
+ FinishResponse();
+
+ if (message == null)
+ {
+ throw new InvalidOperationException("Call did not return a response message");
+ }
+
+ // The task of this method is cached so there is no need to cache the message here
+ return message;
+ }
+ catch (OperationCanceledException)
+ {
+ EnsureNotDisposed();
+ throw CreateCanceledStatusException();
+ }
+ }
+
+ private void ValidateHeaders()
+ {
+ if (HttpResponse.StatusCode != HttpStatusCode.OK)
+ {
+ _headerValidationError = "Bad gRPC response. Expected HTTP status code 200. Got status code: " + (int)HttpResponse.StatusCode;
+ }
+ else if (HttpResponse.Content.Headers.ContentType == null)
+ {
+ _headerValidationError = "Bad gRPC response. Response did not have a content-type header.";
+ }
+ else
+ {
+ var grpcEncoding = HttpResponse.Content.Headers.ContentType.ToString();
+ if (!GrpcProtocolHelpers.IsGrpcContentType(grpcEncoding))
+ {
+ _headerValidationError = "Bad gRPC response. Invalid content-type value: " + grpcEncoding;
+ }
+ }
+
+ if (_headerValidationError != null)
+ {
+ // Response is not valid gRPC
+ // Clean up/cancel any pending operations
+ Dispose();
+
+ throw new InvalidOperationException(_headerValidationError);
+ }
+
+ // Success!
+ }
+
+ public Metadata GetTrailers()
+ {
+ if (_trailers == null)
+ {
+ ValidateTrailersAvailable();
+
+ _trailers = GrpcProtocolHelpers.BuildMetadata(HttpResponse.TrailingHeaders);
+ }
+
+ return _trailers;
+ }
+
+ private void SetMessageContent(TRequest request, HttpRequestMessage message)
+ {
+ message.Content = new PushStreamContent(
+ (stream) =>
+ {
+ return SerializationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken);
+ },
+ GrpcProtocolConstants.GrpcContentTypeHeaderValue);
+ }
+
+ private void CancelCall()
+ {
+ _callCts.Cancel();
+ }
+
+ private void StartSend(System.Net.Http.HttpClient client, HttpRequestMessage message)
+ {
+ SendTask = SendAsync(client, message);
+ }
+
+ private async Task SendAsync(System.Net.Http.HttpClient client, HttpRequestMessage message)
+ {
+ HttpResponse = await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, _callCts.Token).ConfigureAwait(false);
+ ValidateHeaders();
+ }
+
+ private HttpContentClientStreamWriter CreateWriter(HttpRequestMessage message)
+ {
+ var writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ // Canceling call will cancel pending writes to the stream
+ _writerCtsRegistration = _callCts.Token.Register(() =>
+ {
+ completeTcs.TrySetCanceled();
+ writeStreamTcs.TrySetCanceled();
+ });
+
+ message.Content = new PushStreamContent(
+ (stream) =>
+ {
+ writeStreamTcs.TrySetResult(stream);
+ return completeTcs.Task;
+ },
+ GrpcProtocolConstants.GrpcContentTypeHeaderValue);
+
+ var writer = new HttpContentClientStreamWriter(this, writeStreamTcs.Task, completeTcs);
+ return writer;
+ }
+
+ private HttpRequestMessage CreateHttpRequestMessage()
+ {
+ var message = new HttpRequestMessage(HttpMethod.Post, Method.FullName);
+ message.Version = new Version(2, 0);
+ message.Headers.UserAgent.Add(GrpcProtocolConstants.UserAgentHeader);
+
+ if (Options.Headers != null && Options.Headers.Count > 0)
+ {
+ foreach (var entry in Options.Headers)
+ {
+ // Deadline is set via CallOptions.Deadline
+ if (entry.Key == GrpcProtocolConstants.TimeoutHeader)
+ {
+ continue;
+ }
+
+ var value = entry.IsBinary ? Convert.ToBase64String(entry.ValueBytes) : entry.Value;
+ message.Headers.Add(entry.Key, value);
+ }
+ }
+
+ if (_timeout != null)
+ {
+ message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, GrpcProtocolHelpers.EncodeTimeout(Convert.ToInt64(_timeout.Value.TotalMilliseconds)));
+ }
+
+ return message;
+ }
+
+ private void DeadlineExceeded(object state)
+ {
+ // Deadline is only exceeded if the timeout has passed and
+ // the response has not been finished or canceled
+ if (!_callCts.IsCancellationRequested && !ResponseFinished)
+ {
+ // Flag is used to determine status code when generating exceptions
+ DeadlineReached = true;
+
+ _callCts.Cancel();
+ }
+ }
+
+ private static Status GetStatusCore(HttpResponseMessage httpResponseMessage)
+ {
+ string grpcStatus = GetHeaderValue(httpResponseMessage.TrailingHeaders, GrpcProtocolConstants.StatusTrailer);
+ // grpc-status is a required trailer
+ if (grpcStatus == null)
+ {
+ throw new InvalidOperationException("Response did not have a grpc-status trailer.");
+ }
+
+ int statusValue;
+ if (!int.TryParse(grpcStatus, out statusValue))
+ {
+ throw new InvalidOperationException("Unexpected grpc-status value: " + grpcStatus);
+ }
+
+ // grpc-message is optional
+ string grpcMessage = GetHeaderValue(httpResponseMessage.TrailingHeaders, GrpcProtocolConstants.MessageTrailer);
+ if (!string.IsNullOrEmpty(grpcMessage))
+ {
+ // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses
+ // The value portion of Status-Message is conceptually a Unicode string description of the error,
+ // physically encoded as UTF-8 followed by percent-encoding.
+ grpcMessage = Uri.UnescapeDataString(grpcMessage);
+ }
+
+ return new Status((StatusCode)statusValue, grpcMessage);
+ }
+
+ private static string GetHeaderValue(HttpHeaders headers, string name)
+ {
+ if (!headers.TryGetValues(name, out var values))
+ {
+ return null;
+ }
+
+ // HttpHeaders appears to always return an array, but fallback to converting values to one just in case
+ var valuesArray = values as string[] ?? values.ToArray();
+
+ switch (valuesArray.Length)
+ {
+ case 0:
+ return null;
+ case 1:
+ return valuesArray[0];
+ default:
+ throw new InvalidOperationException($"Multiple {name} headers.");
+ }
+ }
+
+ private void ValidateTrailersAvailable()
+ {
+ // Response headers have been returned and are not a valid grpc response
+ EnsureHeadersValid();
+
+ // Response is finished
+ if (ResponseFinished)
+ {
+ return;
+ }
+
+ // Async call could have been disposed
+ EnsureNotDisposed();
+
+ // Call could have been canceled or deadline exceeded
+ if (_callCts.IsCancellationRequested)
+ {
+ throw CreateCanceledStatusException();
+ }
+
+ // HttpClient.SendAsync could have failed
+ if (SendTask.IsFaulted)
+ {
+ throw new InvalidOperationException("Can't get the call trailers because an error occured when making the request.", SendTask.Exception);
+ }
+
+ throw new InvalidOperationException("Can't get the call trailers because the call is not complete.");
+ }
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs
new file mode 100644
index 000000000..0fa4ffcc8
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs
@@ -0,0 +1,62 @@
+#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
+
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Reflection;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal static class GrpcProtocolConstants
+ {
+ internal const string GrpcContentType = "application/grpc";
+ internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc");
+
+ internal const string TimeoutHeader = "grpc-timeout";
+ internal const string MessageEncodingHeader = "grpc-encoding";
+
+ internal const string StatusTrailer = "grpc-status";
+ internal const string MessageTrailer = "grpc-message";
+
+ internal const string MessageAcceptEncodingHeader = "grpc-accept-encoding";
+
+ internal static readonly ProductInfoHeaderValue UserAgentHeader;
+
+ static GrpcProtocolConstants()
+ {
+ var userAgent = "grpc-dotnet";
+
+ var assemblyVersion = typeof(GrpcProtocolConstants)
+ .Assembly
+ .GetCustomAttributes()
+ .FirstOrDefault();
+
+ Debug.Assert(assemblyVersion != null);
+
+ // assembly version attribute should always be present
+ // but in case it isn't then don't include version in user-agent
+ if (assemblyVersion != null)
+ {
+ userAgent += "/" + assemblyVersion.InformationalVersion;
+ }
+
+ UserAgentHeader = ProductInfoHeaderValue.Parse(userAgent);
+ }
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs
new file mode 100644
index 000000000..df354592f
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs
@@ -0,0 +1,188 @@
+#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
+
+using System;
+using System.Net.Http.Headers;
+using Grpc.Core;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal static class GrpcProtocolHelpers
+ {
+ public static bool IsGrpcContentType(string contentType)
+ {
+ if (contentType == null)
+ {
+ return false;
+ }
+
+ if (!contentType.StartsWith(GrpcProtocolConstants.GrpcContentType, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (contentType.Length == GrpcProtocolConstants.GrpcContentType.Length)
+ {
+ // Exact match
+ return true;
+ }
+
+ // Support variations on the content-type (e.g. +proto, +json)
+ char nextChar = contentType[GrpcProtocolConstants.GrpcContentType.Length];
+ if (nextChar == ';')
+ {
+ return true;
+ }
+ if (nextChar == '+')
+ {
+ // Accept any message format. Marshaller could be set to support third-party formats
+ return true;
+ }
+
+ return false;
+ }
+
+ public static byte[] ParseBinaryHeader(string base64)
+ {
+ string decodable;
+ switch (base64.Length % 4)
+ {
+ case 0:
+ // base64 has the required padding
+ decodable = base64;
+ break;
+ case 2:
+ // 2 chars padding
+ decodable = base64 + "==";
+ break;
+ case 3:
+ // 3 chars padding
+ decodable = base64 + "=";
+ break;
+ default:
+ // length%4 == 1 should be illegal
+ throw new FormatException("Invalid base64 header value");
+ }
+
+ return Convert.FromBase64String(decodable);
+ }
+
+ public static Metadata BuildMetadata(HttpResponseHeaders responseHeaders)
+ {
+ var headers = new Metadata();
+
+ foreach (var header in responseHeaders)
+ {
+ // ASP.NET Core includes pseudo headers in the set of request headers
+ // whereas, they are not in gRPC implementations. We will filter them
+ // out when we construct the list of headers on the context.
+ if (header.Key.StartsWith(":", StringComparison.Ordinal))
+ {
+ continue;
+ }
+ else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase))
+ {
+ headers.Add(header.Key, GrpcProtocolHelpers.ParseBinaryHeader(string.Join(",", header.Value)));
+ }
+ else
+ {
+ headers.Add(header.Key, string.Join(",", header.Value));
+ }
+ }
+
+ return headers;
+ }
+
+ private const int MillisecondsPerSecond = 1000;
+
+ /* round an integer up to the next value with three significant figures */
+ private static long TimeoutRoundUpToThreeSignificantFigures(long x)
+ {
+ if (x < 1000) return x;
+ if (x < 10000) return RoundUp(x, 10);
+ if (x < 100000) return RoundUp(x, 100);
+ if (x < 1000000) return RoundUp(x, 1000);
+ if (x < 10000000) return RoundUp(x, 10000);
+ if (x < 100000000) return RoundUp(x, 100000);
+ if (x < 1000000000) return RoundUp(x, 1000000);
+ return RoundUp(x, 10000000);
+
+ static long RoundUp(long x, long divisor)
+ {
+ return (x / divisor + Convert.ToInt32(x % divisor != 0)) * divisor;
+ }
+ }
+
+ private static string FormatTimeout(long value, char ext)
+ {
+ return value.ToString() + ext;
+ }
+
+ private static string EncodeTimeoutSeconds(long sec)
+ {
+ if (sec % 3600 == 0)
+ {
+ return FormatTimeout(sec / 3600, 'H');
+ }
+ else if (sec % 60 == 0)
+ {
+ return FormatTimeout(sec / 60, 'M');
+ }
+ else
+ {
+ return FormatTimeout(sec, 'S');
+ }
+ }
+
+ private static string EncodeTimeoutMilliseconds(long x)
+ {
+ x = TimeoutRoundUpToThreeSignificantFigures(x);
+ if (x < MillisecondsPerSecond)
+ {
+ return FormatTimeout(x, 'm');
+ }
+ else
+ {
+ if (x % MillisecondsPerSecond == 0)
+ {
+ return EncodeTimeoutSeconds(x / MillisecondsPerSecond);
+ }
+ else
+ {
+ return FormatTimeout(x, 'm');
+ }
+ }
+ }
+
+ public static string EncodeTimeout(long timeout)
+ {
+ if (timeout <= 0)
+ {
+ return "1n";
+ }
+ else if (timeout < 1000 * MillisecondsPerSecond)
+ {
+ return EncodeTimeoutMilliseconds(timeout);
+ }
+ else
+ {
+ return EncodeTimeoutSeconds(timeout / MillisecondsPerSecond + Convert.ToInt32(timeout % MillisecondsPerSecond != 0));
+ }
+ }
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs
new file mode 100644
index 000000000..2e1d5b463
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs
@@ -0,0 +1,140 @@
+#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
+
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Grpc.Core;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal class HttpContentClientStreamReader : IAsyncStreamReader
+ {
+ private static readonly Task FinishedTask = Task.FromResult(false);
+
+ private readonly GrpcCall _call;
+ private readonly object _moveNextLock;
+
+ private HttpResponseMessage _httpResponse;
+ private Stream _responseStream;
+ private Task _moveNextTask;
+
+ public HttpContentClientStreamReader(GrpcCall call)
+ {
+ _call = call;
+ _moveNextLock = new object();
+ }
+
+ public TResponse Current { get; private set; }
+
+ public void Dispose()
+ {
+ }
+
+ public Task MoveNext(CancellationToken cancellationToken)
+ {
+ // HTTP response has finished
+ if (_call.ResponseFinished)
+ {
+ return FinishedTask;
+ }
+
+ if (_call.IsCancellationRequested)
+ {
+ throw _call.CreateCanceledStatusException();
+ }
+
+ lock (_moveNextLock)
+ {
+ // Pending move next need to be awaited first
+ if (IsMoveNextInProgressUnsynchronized)
+ {
+ return Task.FromException(new InvalidOperationException("Cannot read next message because the previous read is in progress."));
+ }
+
+ // Save move next task to track whether it is complete
+ _moveNextTask = MoveNextCore(cancellationToken);
+ }
+
+ return _moveNextTask;
+ }
+
+ private async Task MoveNextCore(CancellationToken cancellationToken)
+ {
+ CancellationTokenSource cts = null;
+ try
+ {
+ // Linking tokens is expensive. Only create a linked token if the token passed in requires it
+ if (cancellationToken.CanBeCanceled)
+ {
+ cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _call.CancellationToken);
+ cancellationToken = cts.Token;
+ }
+ else
+ {
+ cancellationToken = _call.CancellationToken;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (_httpResponse == null)
+ {
+ await _call.SendTask.ConfigureAwait(false);
+ _httpResponse = _call.HttpResponse;
+ }
+ if (_responseStream == null)
+ {
+ _responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ }
+
+ Current = await _responseStream.ReadStreamedMessageAsync(_call.Method.ResponseMarshaller.Deserializer, cancellationToken).ConfigureAwait(false);
+ if (Current == null)
+ {
+ // No more content in response so mark as finished
+ _call.FinishResponse();
+ return false;
+ }
+
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ throw _call.CreateCanceledStatusException();
+ }
+ finally
+ {
+ cts?.Dispose();
+ }
+ }
+
+ ///
+ /// A value indicating whether there is an async move next already in progress.
+ /// Should only check this property when holding the move next lock.
+ ///
+ private bool IsMoveNextInProgressUnsynchronized
+ {
+ get
+ {
+ var moveNextTask = _moveNextTask;
+ return moveNextTask != null && !moveNextTask.IsCompleted;
+ }
+ }
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs
new file mode 100644
index 000000000..49847f351
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs
@@ -0,0 +1,127 @@
+#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
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Grpc.Core;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal class HttpContentClientStreamWriter : IClientStreamWriter
+ {
+ private readonly GrpcCall _call;
+ private readonly Task _writeStreamTask;
+ private readonly TaskCompletionSource _completeTcs;
+ private readonly object _writeLock;
+ private Task _writeTask;
+
+ public HttpContentClientStreamWriter(GrpcCall call, Task writeStreamTask, TaskCompletionSource completeTcs)
+ {
+ _call = call;
+ _writeStreamTask = writeStreamTask;
+ _completeTcs = completeTcs;
+ _writeLock = new object();
+ WriteOptions = _call.Options.WriteOptions;
+ }
+
+ public WriteOptions WriteOptions { get; set; }
+
+ public Task CompleteAsync()
+ {
+ lock (_writeLock)
+ {
+ // Pending writes need to be awaited first
+ if (IsWriteInProgressUnsynchronized)
+ {
+ return Task.FromException(new InvalidOperationException("Cannot complete client stream writer because the previous write is in progress."));
+ }
+
+ // Notify that the client stream is complete
+ _completeTcs.TrySetResult(true);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task WriteAsync(TRequest message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ lock (_writeLock)
+ {
+ // CompleteAsync has already been called
+ if (_completeTcs.Task.IsCompletedSuccessfully)
+ {
+ return Task.FromException(new InvalidOperationException("Cannot write message because the client stream writer is complete."));
+ }
+ else if (_completeTcs.Task.IsCanceled)
+ {
+ throw _call.CreateCanceledStatusException();
+ }
+
+ // Pending writes need to be awaited first
+ if (IsWriteInProgressUnsynchronized)
+ {
+ return Task.FromException(new InvalidOperationException("Cannot write message because the previous write is in progress."));
+ }
+
+ // Save write task to track whether it is complete
+ _writeTask = WriteAsyncCore(message);
+ }
+
+ return _writeTask;
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private async Task WriteAsyncCore(TRequest message)
+ {
+ try
+ {
+ // Wait until the client stream has started
+ var writeStream = await _writeStreamTask.ConfigureAwait(false);
+
+ await SerializationHelpers.WriteMessage(writeStream, message, _call.Method.RequestMarshaller.Serializer, _call.CancellationToken).ConfigureAwait(false);
+ }
+ catch (TaskCanceledException)
+ {
+ throw _call.CreateCanceledStatusException();
+ }
+ }
+
+ ///
+ /// A value indicating whether there is an async write already in progress.
+ /// Should only check this property when holding the write lock.
+ ///
+ private bool IsWriteInProgressUnsynchronized
+ {
+ get
+ {
+ var writeTask = _writeTask;
+ return writeTask != null && !writeTask.IsCompleted;
+ }
+ }
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs b/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs
new file mode 100644
index 000000000..016c52ee0
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs
@@ -0,0 +1,27 @@
+#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
+
+using System;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal interface ISystemClock
+ {
+ DateTime UtcNow { get; }
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs b/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs
new file mode 100644
index 000000000..51e28f87e
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs
@@ -0,0 +1,53 @@
+#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
+
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal class PushStreamContent : HttpContent
+ {
+ private readonly Func _onStreamAvailable;
+
+ public PushStreamContent(Func onStreamAvailable, MediaTypeHeaderValue mediaType)
+ {
+ _onStreamAvailable = onStreamAvailable;
+ Headers.ContentType = mediaType;
+ }
+
+ protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ return _onStreamAvailable(stream);
+ }
+
+ protected override bool TryComputeLength(out long length)
+ {
+ // We can't know the length of the content being pushed to the output stream.
+ length = -1;
+ return false;
+ }
+
+ // Hacky. ReadAsStreamAsync does not complete until SerializeToStreamAsync finishes
+ internal Task PushComplete => ReadAsStreamAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs
new file mode 100644
index 000000000..3715cbc54
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs
@@ -0,0 +1,61 @@
+#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
+
+using System;
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal static class SerializationHelpers
+ {
+ public static async Task WriteMessage(Stream stream, TMessage message, Func serializer, CancellationToken cancellationToken)
+ {
+ var data = serializer(message);
+
+ await WriteHeaderAsync(stream, data.Length, false, cancellationToken).ConfigureAwait(false);
+ await stream.WriteAsync(data, cancellationToken).ConfigureAwait(false);
+ }
+
+ private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length"
+ private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag
+
+ public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken)
+ {
+ var headerData = new byte[HeaderSize];
+
+ // Compression flag
+ headerData[0] = compress ? (byte)1 : (byte)0;
+
+ // Message length
+ EncodeMessageLength(length, headerData.AsSpan(1));
+
+ return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken);
+ }
+
+ private static void EncodeMessageLength(int messageLength, Span destination)
+ {
+ Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length.");
+
+ BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength);
+ }
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs
new file mode 100644
index 000000000..28f066ecb
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs
@@ -0,0 +1,111 @@
+#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
+
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Grpc.NetCore.HttpClient
+{
+ internal static class StreamExtensions
+ {
+ public static Task ReadSingleMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken)
+ {
+ return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true, true);
+ }
+
+ public static Task ReadStreamedMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken)
+ {
+ return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true, false);
+ }
+
+ private static async Task ReadMessageCoreAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken, bool canBeEmpty, bool singleMessage)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var header = new byte[5];
+
+ int read;
+ var received = 0;
+ while ((read = await responseStream.ReadAsync(header, received, header.Length - received, cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ received += read;
+
+ if (received == header.Length)
+ {
+ break;
+ }
+ }
+
+ if (received < header.Length)
+ {
+ if (received == 0 && canBeEmpty)
+ {
+ return default;
+ }
+
+ throw new InvalidDataException("Unexpected end of content while reading the message header.");
+ }
+
+ var length = BinaryPrimitives.ReadUInt32BigEndian(header.AsSpan(1));
+ if (length > int.MaxValue)
+ {
+ throw new InvalidDataException("Message too large.");
+ }
+
+ byte[] messageData;
+ if (length > 0)
+ {
+ received = 0;
+ messageData = new byte[length];
+ while ((read = await responseStream.ReadAsync(messageData, received, messageData.Length - received, cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ received += read;
+
+ if (received == messageData.Length)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ messageData = Array.Empty();
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var message = deserializer(messageData);
+
+ if (singleMessage)
+ {
+ // Check that there is no additional content in the stream for a single message
+ // There is no ReadByteAsync on stream. Reuse header array with ReadAsync, we don't need it anymore
+ if (await responseStream.ReadAsync(header, 0, 1).ConfigureAwait(false) > 0)
+ {
+ throw new InvalidDataException("Unexpected data after finished reading message.");
+ }
+ }
+
+ return message;
+ }
+
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs b/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs
new file mode 100644
index 000000000..87321c263
--- /dev/null
+++ b/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs
@@ -0,0 +1,29 @@
+#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
+
+using System;
+
+namespace Grpc.NetCore.HttpClient.Internal
+{
+ internal class SystemClock : ISystemClock
+ {
+ public static readonly SystemClock Instance = new SystemClock();
+
+ public DateTime UtcNow => DateTime.UtcNow;
+ }
+}
diff --git a/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs
deleted file mode 100644
index 18605f46e..000000000
--- a/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-#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
-
-using System;
-using System.IO.Pipelines;
-using System.Threading.Tasks;
-using Grpc.Core;
-
-namespace Grpc.NetCore.HttpClient
-{
- internal class PipeClientStreamWriter : IClientStreamWriter
- {
- private readonly PipeWriter _writer;
- private readonly Func _serializer;
-
- public PipeClientStreamWriter(PipeWriter Writer, Func serializer, WriteOptions options)
- {
- _writer = Writer;
- _serializer = serializer;
- WriteOptions = options;
- }
-
- public WriteOptions WriteOptions { get; set; }
-
- public Task CompleteAsync()
- {
- _writer.Complete();
- return Task.CompletedTask;
- }
-
- public Task WriteAsync(TRequest message)
- {
- if (message == null)
- {
- throw new ArgumentNullException(nameof(message));
- }
-
- return _writer.WriteMessageCoreAsync(_serializer(message), flush: true);
- }
- }
-}
diff --git a/src/Grpc.NetCore.HttpClient/PipeContent.cs b/src/Grpc.NetCore.HttpClient/PipeContent.cs
deleted file mode 100644
index 6982828f0..000000000
--- a/src/Grpc.NetCore.HttpClient/PipeContent.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-#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
-
-using System.Buffers;
-using System.IO;
-using System.IO.Pipelines;
-using System.Net;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading.Tasks;
-
-namespace Grpc.NetCore.HttpClient
-{
- internal class PipeContent : HttpContent
- {
- private Pipe _pipe = new Pipe();
-
- public PipeWriter PipeWriter => _pipe.Writer;
- private PipeReader PipeReader => _pipe.Reader;
-
- public PipeContent()
- {
- Headers.ContentType = new MediaTypeHeaderValue("application/grpc");
- }
-
- protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
- {
- while (true)
- {
- var result = await PipeReader.ReadAsync();
- var buffer = result.Buffer;
-
- try
- {
- if (result.IsCanceled)
- {
- throw new TaskCanceledException();
- }
-
- if (!buffer.IsEmpty)
- {
- var data = buffer.ToArray();
- stream.Write(data, 0, data.Length);
- }
-
- if (result.IsCompleted)
- {
- break;
- }
- }
- finally
- {
- PipeReader.AdvanceTo(buffer.End);
- }
- }
- }
-
- protected override bool TryComputeLength(out long length)
- {
- length = 0;
- return false;
- }
- }
-}
diff --git a/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs b/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs
deleted file mode 100644
index 64867f326..000000000
--- a/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-#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
-
-using System.Buffers;
-using System.Buffers.Binary;
-using System.IO.Pipelines;
-using System.Threading.Tasks;
-
-namespace Grpc.NetCore.HttpClient
-{
- internal static class PipeWriterExtensions
- {
- private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length"
- private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag
-
- public static Task WriteMessageCoreAsync(this PipeWriter pipeWriter, byte[] messageData, bool flush)
- {
- WriteHeader(pipeWriter, messageData.Length);
- pipeWriter.Write(messageData);
-
- if (flush)
- {
- var valueTask = pipeWriter.FlushAsync();
-
- if (valueTask.IsCompletedSuccessfully)
- {
- // We do this to reset the underlying value task (which happens in GetResult())
- valueTask.GetAwaiter().GetResult();
- return Task.CompletedTask;
- }
-
- return valueTask.AsTask();
- }
-
- return Task.CompletedTask;
- }
-
- private static void WriteHeader(PipeWriter pipeWriter, int length)
- {
- var headerData = pipeWriter.GetSpan(HeaderSize);
- // Messages are currently always uncompressed
- headerData[0] = 0;
- BinaryPrimitives.WriteUInt32BigEndian(headerData.Slice(1), (uint)length);
-
- pipeWriter.Advance(HeaderSize);
- }
- }
-}
diff --git a/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs b/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs
index 9e89fd1e0..c81c37157 100644
--- a/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs
+++ b/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs
@@ -24,6 +24,12 @@
"27fc95aff3dc604a6971417453f9483c7b5e836756d5b271bf8f2403fe186e31956148c03d804487cf642f8cc0" +
"71394ee9672dfe5b55ea0f95dfd5a7f77d22c962ccf51320d3")]
+[assembly: InternalsVisibleTo("Grpc.NetCore.HttpClient.Tests,PublicKey=" +
+ "00240000048000009400000006020000002400005253413100040000010001002f5797a92c6fcde81bd4098f43" +
+ "0442bb8e12768722de0b0cb1b15e955b32a11352740ee59f2c94c48edc8e177d1052536b8ac651bce11ce5da3a" +
+ "27fc95aff3dc604a6971417453f9483c7b5e836756d5b271bf8f2403fe186e31956148c03d804487cf642f8cc0" +
+ "71394ee9672dfe5b55ea0f95dfd5a7f77d22c962ccf51320d3")]
+
// For Moq. This assembly needs access to internal types via InternalVisibleTo to be able to mock them
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602" +
"000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02ba" +
diff --git a/src/Grpc.NetCore.HttpClient/StreamExtensions.cs b/src/Grpc.NetCore.HttpClient/StreamExtensions.cs
deleted file mode 100644
index e6f35daef..000000000
--- a/src/Grpc.NetCore.HttpClient/StreamExtensions.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-#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
-
-using System;
-using System.Buffers.Binary;
-using System.IO;
-
-namespace Grpc.NetCore.HttpClient
-{
- internal static class StreamExtensions
- {
- public static TResponse ReadSingleMessage(this Stream responseStream, Func deserializer)
- {
- if (responseStream.ReadByte() != 0)
- {
- throw new InvalidOperationException("Compressed response not yet supported");
- }
-
- var lengthBytes = new byte[4];
- responseStream.Read(lengthBytes, 0, 4);
- var length = BinaryPrimitives.ReadUInt32BigEndian(lengthBytes);
- if (length > int.MaxValue)
- {
- throw new InvalidOperationException("message too large");
- }
-
- var responseBytes = new byte[length];
- responseStream.Read(responseBytes, 0, (int)length);
-
- return deserializer(responseBytes);
- }
- }
-}
diff --git a/test/FunctionalTests/AuthorizationTests.cs b/test/FunctionalTests/AuthorizationTests.cs
index be65d5423..f44cc3cde 100644
--- a/test/FunctionalTests/AuthorizationTests.cs
+++ b/test/FunctionalTests/AuthorizationTests.cs
@@ -25,6 +25,7 @@
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
using Grpc.Core;
+using Grpc.Tests.Shared;
using NUnit.Framework;
namespace Grpc.AspNetCore.FunctionalTests
diff --git a/test/FunctionalTests/ClientStreamingMethodTests.cs b/test/FunctionalTests/ClientStreamingMethodTests.cs
index a505967a9..053220cc3 100644
--- a/test/FunctionalTests/ClientStreamingMethodTests.cs
+++ b/test/FunctionalTests/ClientStreamingMethodTests.cs
@@ -27,9 +27,9 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
-using Grpc.AspNetCore.Server.Tests;
using Grpc.Core;
using NUnit.Framework;
+using Grpc.Tests.Shared;
namespace Grpc.AspNetCore.FunctionalTests
{
@@ -157,7 +157,7 @@ static async Task AccumulateCount(IAsyncStreamReader AccumulateCount(IAsyncStreamReader();
+ var reply = await response.GetSuccessfulGrpcMessageAsync().DefaultTimeout();
Assert.AreEqual(3, reply.Count);
Assert.AreEqual(StatusCode.OK.ToTrailerString(), Fixture.TrailersContainer.Trailers[GrpcProtocolConstants.StatusTrailer].Single());
diff --git a/test/FunctionalTests/CompressionTests.cs b/test/FunctionalTests/CompressionTests.cs
index c1e479a43..16c802d21 100644
--- a/test/FunctionalTests/CompressionTests.cs
+++ b/test/FunctionalTests/CompressionTests.cs
@@ -29,7 +29,7 @@
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Compression;
using Grpc.AspNetCore.Server.Internal;
-using Grpc.AspNetCore.Server.Tests;
+using Grpc.Tests.Shared;
using Grpc.Core;
using NUnit.Framework;
diff --git a/test/FunctionalTests/DeadlineTests.cs b/test/FunctionalTests/DeadlineTests.cs
index 5ad8e8780..6e6edbc61 100644
--- a/test/FunctionalTests/DeadlineTests.cs
+++ b/test/FunctionalTests/DeadlineTests.cs
@@ -26,6 +26,7 @@
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
using Grpc.Core;
+using Grpc.Tests.Shared;
using NUnit.Framework;
namespace Grpc.AspNetCore.FunctionalTests
@@ -49,7 +50,7 @@ public Task WriteUntilDeadline_SuccessResponsesStreamed_Deadline() =>
}
// Ensure deadline timer has run
- var tcs = new TaskCompletionSource
diff --git a/test/FunctionalTests/HttpContextTests.cs b/test/FunctionalTests/HttpContextTests.cs
index 18ae0b944..331fe1aaa 100644
--- a/test/FunctionalTests/HttpContextTests.cs
+++ b/test/FunctionalTests/HttpContextTests.cs
@@ -22,7 +22,7 @@
using Greet;
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
-using Grpc.AspNetCore.Server.Tests;
+using Grpc.Tests.Shared;
using Grpc.Core;
using NUnit.Framework;
diff --git a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs
index 3660d4778..7dad7ed19 100644
--- a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs
+++ b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs
@@ -16,11 +16,11 @@
#endregion
-using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Google.Protobuf;
+using Grpc.Tests.Shared;
using NUnit.Framework;
namespace Grpc.AspNetCore.FunctionalTests.Infrastructure
diff --git a/test/FunctionalTests/LifetimeTests.cs b/test/FunctionalTests/LifetimeTests.cs
index afa949125..339db2a7a 100644
--- a/test/FunctionalTests/LifetimeTests.cs
+++ b/test/FunctionalTests/LifetimeTests.cs
@@ -25,7 +25,7 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
-using Grpc.AspNetCore.Server.Tests;
+using Grpc.Tests.Shared;
using Grpc.Core;
using Lifetime;
using NUnit.Framework;
diff --git a/test/FunctionalTests/MaxMessageSizeTests.cs b/test/FunctionalTests/MaxMessageSizeTests.cs
index b32a95254..ad53ff799 100644
--- a/test/FunctionalTests/MaxMessageSizeTests.cs
+++ b/test/FunctionalTests/MaxMessageSizeTests.cs
@@ -23,7 +23,7 @@
using Greet;
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
-using Grpc.AspNetCore.Server.Tests;
+using Grpc.Tests.Shared;
using Grpc.Core;
using NUnit.Framework;
diff --git a/test/FunctionalTests/NestedTests.cs b/test/FunctionalTests/NestedTests.cs
index 0c3601b5a..dafa43abe 100644
--- a/test/FunctionalTests/NestedTests.cs
+++ b/test/FunctionalTests/NestedTests.cs
@@ -24,7 +24,7 @@
using System.Threading.Tasks;
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
-using Grpc.AspNetCore.Server.Tests;
+using Grpc.Tests.Shared;
using Grpc.Core;
using Nested;
using NUnit.Framework;
@@ -35,6 +35,7 @@ namespace Grpc.AspNetCore.FunctionalTests
public class NestedTests : FunctionalTestBase
{
[Test]
+ [Ignore("Failing because TestHost does not return trailers. Blocked on https://github.com/aspnet/AspNetCore/issues/6880")]
public async Task CallNestedService_SuccessResponse()
{
// Arrange
diff --git a/test/FunctionalTests/ServerStreamingMethodTests.cs b/test/FunctionalTests/ServerStreamingMethodTests.cs
index a6d0ce687..f9cad3381 100644
--- a/test/FunctionalTests/ServerStreamingMethodTests.cs
+++ b/test/FunctionalTests/ServerStreamingMethodTests.cs
@@ -26,6 +26,7 @@
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
using Grpc.Core;
+using Grpc.Tests.Shared;
using NUnit.Framework;
namespace Grpc.AspNetCore.FunctionalTests
diff --git a/test/FunctionalTests/UnaryMethodTests.cs b/test/FunctionalTests/UnaryMethodTests.cs
index 035078caa..f10e81ebe 100644
--- a/test/FunctionalTests/UnaryMethodTests.cs
+++ b/test/FunctionalTests/UnaryMethodTests.cs
@@ -28,8 +28,8 @@
using Greet;
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
-using Grpc.AspNetCore.Server.Tests;
using Grpc.Core;
+using Grpc.Tests.Shared;
using NUnit.Framework;
namespace Grpc.AspNetCore.FunctionalTests
diff --git a/test/FunctionalTests/UnimplementedTests.cs b/test/FunctionalTests/UnimplementedTests.cs
index d22594344..e9eb31579 100644
--- a/test/FunctionalTests/UnimplementedTests.cs
+++ b/test/FunctionalTests/UnimplementedTests.cs
@@ -25,6 +25,7 @@
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
using Grpc.Core;
+using Grpc.Tests.Shared;
using NUnit.Framework;
namespace Grpc.AspNetCore.FunctionalTests
diff --git a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj
index 7a262e836..2e66bacdd 100644
--- a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj
+++ b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj
@@ -3,22 +3,17 @@
netcoreapp3.0
false
- latest
-
+
-
-
-
-
diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs
index a58613e67..9c00f7849 100644
--- a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs
+++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs
@@ -23,6 +23,7 @@
using Greet;
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
+using Grpc.Tests.Shared;
using Microsoft.AspNetCore.Http;
using NUnit.Framework;
diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs
index 91360240c..cfb54575d 100644
--- a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs
+++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs
@@ -24,6 +24,7 @@
using Grpc.AspNetCore.FunctionalTests.Infrastructure;
using Grpc.AspNetCore.Server.Internal;
using Grpc.Core;
+using Grpc.Tests.Shared;
using Microsoft.AspNetCore.Http;
using NUnit.Framework;
diff --git a/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs b/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs
index 26b71e420..32be8bd8a 100644
--- a/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs
+++ b/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs
@@ -27,6 +27,7 @@
using Grpc.AspNetCore.Server.Compression;
using Grpc.AspNetCore.Server.Internal;
using Grpc.Core;
+using Grpc.Tests.Shared;
using Microsoft.AspNetCore.Http;
using NUnit.Framework;
diff --git a/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs b/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs
index 1b26d3652..f1b187caa 100644
--- a/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs
+++ b/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs
@@ -26,6 +26,7 @@
using Grpc.AspNetCore.Server.Reflection.Internal;
using Grpc.Core;
using Grpc.Reflection.V1Alpha;
+using Grpc.Tests.Shared;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs
new file mode 100644
index 000000000..0d7623199
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs
@@ -0,0 +1,212 @@
+#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
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class AsyncClientStreamingCallTests
+ {
+ [Test]
+ public async Task AsyncClientStreamingCall_Success_HttpRequestMessagePopulated()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ await call.RequestStream.CompleteAsync().DefaultTimeout();
+
+ var response = await call;
+
+ // Assert
+ Assert.AreEqual("Hello world", response.Message);
+
+ Assert.IsNotNull(httpRequestMessage);
+ Assert.AreEqual(new Version(2, 0), httpRequestMessage.Version);
+ Assert.AreEqual(HttpMethod.Post, httpRequestMessage.Method);
+ Assert.AreEqual(new Uri("https://localhost/ServiceName/MethodName"), httpRequestMessage.RequestUri);
+ Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content.Headers.ContentType);
+ }
+
+ [Test]
+ public async Task AsyncClientStreamingCall_Success_RequestContentSent()
+ {
+ // Arrange
+ PushStreamContent content = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ content = (PushStreamContent)request.Content;
+ await content.PushComplete.DefaultTimeout();
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ // Assert
+ Assert.IsNotNull(call);
+ Assert.IsNotNull(content);
+
+ var responseTask = call.ResponseAsync;
+ Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete.");
+
+ var streamTask = content.ReadAsStreamAsync();
+
+ await call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout();
+ await call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }).DefaultTimeout();
+
+ await call.RequestStream.CompleteAsync().DefaultTimeout();
+
+ var requestContent = await streamTask.DefaultTimeout();
+ var requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout();
+ Assert.AreEqual("1", requestMessage.Name);
+ requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout();
+ Assert.AreEqual("2", requestMessage.Name);
+
+ var responseMessage = await responseTask.DefaultTimeout();
+ Assert.AreEqual("Hello world", responseMessage.Message);
+ }
+
+ [Test]
+ public void ClientStreamWriter_WriteWhilePendingWrite_ErrorThrown()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var streamContent = new StreamContent(new SyncPointMemoryStream());
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ // Assert
+ var writeTask1 = call.RequestStream.WriteAsync(new HelloRequest { Name = "1" });
+ Assert.IsFalse(writeTask1.IsCompleted);
+
+ var writeTask2 = call.RequestStream.WriteAsync(new HelloRequest { Name = "2" });
+ var ex = Assert.ThrowsAsync(() => writeTask2.DefaultTimeout());
+
+ Assert.AreEqual("Cannot write message because the previous write is in progress.", ex.Message);
+ }
+
+ [Test]
+ public void ClientStreamWriter_CompleteWhilePendingWrite_ErrorThrown()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var streamContent = new StreamContent(new SyncPointMemoryStream());
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ // Assert
+ var writeTask1 = call.RequestStream.WriteAsync(new HelloRequest { Name = "1" });
+ Assert.IsFalse(writeTask1.IsCompleted);
+
+ var completeTask = call.RequestStream.CompleteAsync();
+ var ex = Assert.ThrowsAsync(() => completeTask.DefaultTimeout());
+
+ Assert.AreEqual("Cannot complete client stream writer because the previous write is in progress.", ex.Message);
+ }
+
+ [Test]
+ public async Task ClientStreamWriter_WriteWhileComplete_ErrorThrown()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var streamContent = new StreamContent(new SyncPointMemoryStream());
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ await call.RequestStream.CompleteAsync();
+
+ // Assert
+ var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout());
+
+ Assert.AreEqual("Cannot write message because the client stream writer is complete.", ex.Message);
+ }
+
+ [Test]
+ public void ClientStreamWriter_WriteWithInvalidHttpStatus_ErrorThrown()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var streamContent = new StreamContent(new SyncPointMemoryStream());
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NotFound, streamContent));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ // Assert
+ var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout());
+
+ Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs
new file mode 100644
index 000000000..b806d8568
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs
@@ -0,0 +1,169 @@
+#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
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class AsyncDuplexStreamingCallTests
+ {
+ [Test]
+ public async Task AsyncDuplexStreamingCall_NoContent_NoMessagesReturned()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty())));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ var responseStream = call.ResponseStream;
+
+ // Assert
+ Assert.IsNull(responseStream.Current);
+ Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ Assert.IsNull(responseStream.Current);
+ }
+
+ [Test]
+ public async Task AsyncServerStreamingCall_MessagesReturnedTogether_MessagesReceived()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty())));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ var responseStream = call.ResponseStream;
+
+ // Assert
+ Assert.IsNull(responseStream.Current);
+ Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ Assert.IsNull(responseStream.Current);
+ }
+
+ [Test]
+ public async Task AsyncDuplexStreamingCall_MessagesStreamed_MessagesReceived()
+ {
+ // Arrange
+ var streamContent = new SyncPointMemoryStream();
+
+ PushStreamContent content = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ content = (PushStreamContent)request.Content;
+ await content.PushComplete.DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(streamContent));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+
+ var requestStream = call.RequestStream;
+ var responseStream = call.ResponseStream;
+
+ // Assert
+ await call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout();
+ await call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }).DefaultTimeout();
+
+ await call.RequestStream.CompleteAsync().DefaultTimeout();
+
+ var requestContent = await content.ReadAsStreamAsync().DefaultTimeout();
+ var requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout();
+ Assert.AreEqual("1", requestMessage.Name);
+ requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout();
+ Assert.AreEqual("2", requestMessage.Name);
+
+ Assert.IsNull(responseStream.Current);
+
+ var moveNextTask1 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsFalse(moveNextTask1.IsCompleted);
+
+ await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply
+ {
+ Message = "Hello world 1"
+ }).DefaultTimeout()).DefaultTimeout();
+
+ Assert.IsTrue(await moveNextTask1.DefaultTimeout());
+ Assert.IsNotNull(responseStream.Current);
+ Assert.AreEqual("Hello world 1", responseStream.Current.Message);
+
+ var moveNextTask2 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsFalse(moveNextTask2.IsCompleted);
+
+ await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply
+ {
+ Message = "Hello world 2"
+ }).DefaultTimeout()).DefaultTimeout();
+
+ Assert.IsTrue(await moveNextTask2.DefaultTimeout());
+ Assert.IsNotNull(responseStream.Current);
+ Assert.AreEqual("Hello world 2", responseStream.Current.Message);
+
+ var moveNextTask3 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsFalse(moveNextTask3.IsCompleted);
+
+ await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout();
+
+ Assert.IsFalse(await moveNextTask3.DefaultTimeout());
+
+ var moveNextTask4 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsTrue(moveNextTask4.IsCompleted);
+ Assert.IsFalse(await moveNextTask3.DefaultTimeout());
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs
new file mode 100644
index 000000000..9d71fc2be
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs
@@ -0,0 +1,180 @@
+#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
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class AsyncServerStreamingCallTests
+ {
+ [Test]
+ public async Task AsyncServerStreamingCall_NoContent_NoMessagesReturned()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty())));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ var responseStream = call.ResponseStream;
+
+ // Assert
+ Assert.IsNull(responseStream.Current);
+ Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ Assert.IsNull(responseStream.Current);
+ }
+
+ [Test]
+ public async Task AsyncServerStreamingCall_MessagesReturnedTogether_MessagesReceived()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(
+ new HelloReply
+ {
+ Message = "Hello world 1"
+ },
+ new HelloReply
+ {
+ Message = "Hello world 2"
+ }).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ var responseStream = call.ResponseStream;
+
+ // Assert
+ Assert.IsNull(responseStream.Current);
+
+ Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ Assert.IsNotNull(responseStream.Current);
+ Assert.AreEqual("Hello world 1", responseStream.Current.Message);
+
+ Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ Assert.IsNotNull(responseStream.Current);
+ Assert.AreEqual("Hello world 2", responseStream.Current.Message);
+
+ Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ }
+
+ [Test]
+ public async Task AsyncServerStreamingCall_MessagesStreamed_MessagesReceived()
+ {
+ // Arrange
+ var streamContent = new SyncPointMemoryStream();
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(streamContent)));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ var responseStream = call.ResponseStream;
+
+ // Assert
+ Assert.IsNull(responseStream.Current);
+
+ var moveNextTask1 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsFalse(moveNextTask1.IsCompleted);
+
+ await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply
+ {
+ Message = "Hello world 1"
+ }).DefaultTimeout()).DefaultTimeout();
+
+ Assert.IsTrue(await moveNextTask1.DefaultTimeout());
+ Assert.IsNotNull(responseStream.Current);
+ Assert.AreEqual("Hello world 1", responseStream.Current.Message);
+
+ var moveNextTask2 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsFalse(moveNextTask2.IsCompleted);
+
+ await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply
+ {
+ Message = "Hello world 2"
+ }).DefaultTimeout()).DefaultTimeout();
+
+ Assert.IsTrue(await moveNextTask2.DefaultTimeout());
+ Assert.IsNotNull(responseStream.Current);
+ Assert.AreEqual("Hello world 2", responseStream.Current.Message);
+
+ var moveNextTask3 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsFalse(moveNextTask3.IsCompleted);
+
+ await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout();
+
+ Assert.IsFalse(await moveNextTask3.DefaultTimeout());
+
+ var moveNextTask4 = responseStream.MoveNext(CancellationToken.None);
+ Assert.IsTrue(moveNextTask4.IsCompleted);
+ Assert.IsFalse(await moveNextTask3.DefaultTimeout());
+ }
+
+ [Test]
+ public void ClientStreamReader_WriteWithInvalidHttpStatus_ErrorThrown()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var streamContent = new StreamContent(new SyncPointMemoryStream());
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NotFound, streamContent));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ // Assert
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+
+ Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs
new file mode 100644
index 000000000..079bdb5f2
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs
@@ -0,0 +1,130 @@
+#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
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class AsyncUnaryCallTests
+ {
+ [Test]
+ public async Task AsyncUnaryCall_Success_HttpRequestMessagePopulated()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ // Assert
+ Assert.AreEqual("Hello world", rs.Message);
+
+ Assert.IsNotNull(httpRequestMessage);
+ Assert.AreEqual(new Version(2, 0), httpRequestMessage.Version);
+ Assert.AreEqual(HttpMethod.Post, httpRequestMessage.Method);
+ Assert.AreEqual(new Uri("https://localhost/ServiceName/MethodName"), httpRequestMessage.RequestUri);
+ Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content.Headers.ContentType);
+
+ var userAgent = httpRequestMessage.Headers.UserAgent.Single();
+ Assert.AreEqual(GrpcProtocolConstants.UserAgentHeader, userAgent);
+ Assert.AreEqual("grpc-dotnet", userAgent.Product.Name);
+ Assert.IsTrue(!string.IsNullOrEmpty(userAgent.Product.Version));
+ }
+
+ [Test]
+ public async Task AsyncUnaryCall_Success_RequestContentSent()
+ {
+ // Arrange
+ HttpContent content = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ content = request.Content;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" });
+
+ // Assert
+ Assert.AreEqual("Hello world", rs.Message);
+
+ Assert.IsNotNull(content);
+
+ var requestContent = await content.ReadAsStreamAsync().DefaultTimeout();
+ var requestMessage = await requestContent.ReadSingleMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout();
+
+ Assert.AreEqual("World", requestMessage.Name);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_NonOkStatusTrailer_ThrowRpcError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()), StatusCode.Unimplemented);
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var ex = Assert.ThrowsAsync(async () => await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()));
+
+ // Assert
+ Assert.AreEqual(StatusCode.Unimplemented, ex.StatusCode);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs
new file mode 100644
index 000000000..833b60e80
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs
@@ -0,0 +1,115 @@
+#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
+
+using NUnit.Framework;
+using Greet;
+using static Greet.Greeter;
+using Grpc.Core;
+using Google.Protobuf;
+using System.Net.Http;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using System.Net;
+using System.Threading.Tasks;
+using System;
+using System.IO;
+using System.Threading;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Linq;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.Tests.Shared;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class CancellationTests
+ {
+ [Test]
+ public void AsyncClientStreamingCall_CancellationDuringSend_ResponseThrowsCancelledStatus()
+ {
+ // Arrange
+ var cts = new CancellationTokenSource();
+ var invoker = CreateTimedoutCallInvoker();
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token));
+
+ // Assert
+ var responseTask = call.ResponseAsync;
+ Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete.");
+
+ cts.Cancel();
+
+ var ex = Assert.ThrowsAsync(async () => await responseTask.DefaultTimeout());
+ Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_CancellationDuringSend_ResponseHeadersThrowsCancelledStatus()
+ {
+ // Arrange
+ var cts = new CancellationTokenSource();
+ var invoker = CreateTimedoutCallInvoker();
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token));
+
+ // Assert
+ var responseHeadersTask = call.ResponseHeadersAsync;
+ Assert.IsFalse(responseHeadersTask.IsCompleted, "Headers not returned until client stream is complete.");
+
+ cts.Cancel();
+
+ var ex = Assert.ThrowsAsync(async () => await responseHeadersTask.DefaultTimeout());
+ Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_CancellationDuringSend_TrailersThrowsCancelledStatus()
+ {
+ // Arrange
+ var cts = new CancellationTokenSource();
+ var invoker = CreateTimedoutCallInvoker();
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token));
+
+ // Assert
+ cts.Cancel();
+
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode);
+ }
+
+ private static HttpClientCallInvoker CreateTimedoutCallInvoker()
+ {
+ PushStreamContent content = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ content = (PushStreamContent)request.Content;
+ await content.PushComplete.DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+ return invoker;
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs
new file mode 100644
index 000000000..d0e651c16
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs
@@ -0,0 +1,270 @@
+#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
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class DeadlineTests
+ {
+ [Test]
+ public async Task AsyncUnaryCall_SetSecondDeadline_RequestMessageContainsDeadlineHeader()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+ invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc));
+
+ // Act
+ await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: invoker.Clock.UtcNow.AddSeconds(1)), new HelloRequest());
+
+ // Assert
+ Assert.IsNotNull(httpRequestMessage);
+ Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.TimeoutHeader).Single());
+ }
+
+ [Test]
+ public async Task AsyncUnaryCall_SetMaxValueDeadline_RequestMessageHasNoDeadlineHeader()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.MaxValue), new HelloRequest());
+
+ // Assert
+ Assert.IsNotNull(httpRequestMessage);
+ Assert.AreEqual(0, httpRequestMessage.Headers.Count(h => string.Equals(h.Key, GrpcProtocolConstants.TimeoutHeader, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ [Test]
+ public async Task AsyncUnaryCall_SendDeadlineHeaderAndDeadlineValue_DeadlineValueIsUsed()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+ invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc));
+
+ var headers = new Metadata();
+ headers.Add("grpc-timeout", "1D");
+
+ // Act
+ var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers, deadline: invoker.Clock.UtcNow.AddSeconds(1)), new HelloRequest());
+
+ // Assert
+ Assert.AreEqual("Hello world", rs.Message);
+
+ Assert.IsNotNull(httpRequestMessage);
+ Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.TimeoutHeader).Single());
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_DeadlineDuringSend_ResponseThrowsDeadlineExceededStatus()
+ {
+ // Arrange
+ PushStreamContent content = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ content = (PushStreamContent)request.Content;
+ await content.PushComplete.DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5)));
+
+ // Assert
+ var responseTask = call.ResponseAsync;
+ Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete.");
+
+ var ex = Assert.ThrowsAsync(async () => await responseTask.DefaultTimeout());
+ Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_DeadlineBeforeWrite_ResponseThrowsDeadlineExceededStatus()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow));
+
+ // Assert
+ var ex = Assert.ThrowsAsync(async () => await call.RequestStream.WriteAsync(new HelloRequest()).DefaultTimeout());
+ Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineExceededStatus()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var stream = new SyncPointMemoryStream();
+ var content = new StreamContent(stream);
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content, grpcStatusCode: null));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5)));
+
+ // Assert
+ var ex = Assert.ThrowsAsync(async () => await call.RequestStream.WriteAsync(new HelloRequest()).DefaultTimeout());
+ Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode);
+ }
+
+ [Test]
+ public void AsyncServerStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineExceededStatus()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var stream = new SyncPointMemoryStream();
+ var content = new StreamContent(stream);
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5)), new HelloRequest());
+
+ // Assert
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext(CancellationToken.None));
+ Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode);
+ }
+
+ [Test]
+ public async Task AsyncUnaryCall_SuccessAndReadValuesAfterDeadline_ValuesReturned()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+ invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc));
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: invoker.Clock.UtcNow.AddSeconds(0.5)), new HelloRequest());
+
+ // Assert
+ var result = await call;
+ Assert.IsNotNull(result);
+
+ // Wait for deadline to trigger
+ await Task.Delay(1000);
+
+ Assert.IsNotNull(await call.ResponseHeadersAsync);
+
+ Assert.IsNotNull(call.GetTrailers());
+
+ Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_SetNonUtcDeadline_ThrowError()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var ex = Assert.ThrowsAsync(async () => await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: new DateTime(2000, DateTimeKind.Local)), new HelloRequest()));
+
+ // Assert
+ Assert.AreEqual("Deadline must have a kind DateTimeKind.Utc or be equal to DateTime.MaxValue or DateTime.MinValue.", ex.Message);
+ }
+
+ private class TestSystemClock : ISystemClock
+ {
+ public TestSystemClock(DateTime utcNow)
+ {
+ UtcNow = utcNow;
+ }
+
+ public DateTime UtcNow { get; }
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs
new file mode 100644
index 000000000..ad64cfd23
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs
@@ -0,0 +1,159 @@
+#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
+
+using NUnit.Framework;
+using Greet;
+using static Greet.Greeter;
+using Grpc.Core;
+using Google.Protobuf;
+using System.Net.Http;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using System.Net;
+using System.Threading.Tasks;
+using System;
+using System.IO;
+using System.Threading;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Linq;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.Tests.Shared;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class GetStatusTests
+ {
+ [Test]
+ public void AsyncUnaryCall_ValidStatusReturned_ReturnsStatus()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted);
+ response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "value");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ // Assert
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+ Assert.AreEqual(StatusCode.Aborted, ex.StatusCode);
+
+ var status = call.GetStatus();
+ Assert.AreEqual(StatusCode.Aborted, status.StatusCode);
+ Assert.AreEqual("value", status.Detail);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_PercentEncodedMessage_MessageDecoded()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted);
+ response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "%C2%A3");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ // Assert
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+ Assert.AreEqual(StatusCode.Aborted, ex.StatusCode);
+
+ var status = call.GetStatus();
+ Assert.AreEqual(StatusCode.Aborted, status.StatusCode);
+ Assert.AreEqual("£", status.Detail);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_MultipleStatusHeaders_ThrowError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted);
+ response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "one");
+ response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "two");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ // Assert
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+ Assert.AreEqual("Multiple grpc-message headers.", ex.Message);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_MissingStatus_ThrowError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: null);
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ // Assert
+ Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+
+ var ex = Assert.Throws(() => call.GetStatus());
+ Assert.AreEqual("Response did not have a grpc-status trailer.", ex.Message);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_InvalidStatus_ThrowError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: null);
+ response.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, "value");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+
+ // Assert
+ Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+
+ var ex = Assert.Throws(() => call.GetStatus());
+ Assert.AreEqual("Unexpected grpc-status value: value", ex.Message);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs
new file mode 100644
index 000000000..7bd144e71
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs
@@ -0,0 +1,340 @@
+#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
+
+using NUnit.Framework;
+using Greet;
+using static Greet.Greeter;
+using Grpc.Core;
+using Google.Protobuf;
+using System.Net.Http;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using System.Net;
+using System.Threading.Tasks;
+using System;
+using System.IO;
+using System.Threading;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Linq;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.Tests.Shared;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class GetTrailersTests
+ {
+ [Test]
+ public async Task AsyncUnaryCall_MessageReturned_ReturnsTrailers()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.Headers.Add("custom", "ABC");
+ response.TrailingHeaders.Add("custom-header", "value");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var message = await call;
+ var trailers1 = call.GetTrailers();
+ var trailers2 = call.GetTrailers();
+
+ // Assert
+ Assert.AreSame(trailers1, trailers2);
+ Assert.AreEqual("value", trailers1.Single(t => t.Key == "custom-header").Value);
+ }
+
+ [Test]
+ public async Task AsyncUnaryCall_HeadersReturned_ReturnsTrailers()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.Headers.Add("custom", "ABC");
+ response.TrailingHeaders.Add("custom-header", "value");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout();
+ var trailers = call.GetTrailers();
+
+ // Assert
+ Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_UnfinishedCall_ThrowsError()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ await tcs.Task.DefaultTimeout();
+ return null;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_ErrorCall_ThrowsError()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ return Task.FromException(new Exception("An error!"));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Can't get the call trailers because an error occured when making the request.", ex.Message);
+ Assert.AreEqual("An error!", ex.InnerException.InnerException.Message);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_UnfinishedCall_ThrowsError()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ await tcs.Task.DefaultTimeout();
+ return null;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message);
+ }
+
+ [Test]
+ public void AsyncServerStreamingCall_UnfinishedCall_ThrowsError()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ await tcs.Task.DefaultTimeout();
+ return null;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message);
+ }
+
+ [Test]
+ public async Task AsyncServerStreamingCall_UnfinishedReader_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(
+ new HelloReply
+ {
+ Message = "Hello world 1"
+ },
+ new HelloReply
+ {
+ Message = "Hello world 2"
+ }).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var responseStream = call.ResponseStream;
+
+ Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message);
+ }
+
+ [Test]
+ public async Task AsyncServerStreamingCall_FinishedReader_ReturnsTrailers()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(
+ new HelloReply
+ {
+ Message = "Hello world 1"
+ },
+ new HelloReply
+ {
+ Message = "Hello world 2"
+ }).DefaultTimeout();
+
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.TrailingHeaders.Add("custom-header", "value");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var responseStream = call.ResponseStream;
+
+ Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout());
+ var trailers = call.GetTrailers();
+
+ // Assert
+ Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value);
+ }
+
+ [Test]
+ public async Task AsyncClientStreamingCall_CompleteWriter_ReturnsTrailers()
+ {
+ // Arrange
+ var trailingHeadersWrittenTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var content = (PushStreamContent)request.Content;
+ var stream = new SyncPointMemoryStream();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream));
+
+ _ = Task.Run(async () =>
+ {
+ // Add a response message after the client has completed
+ await content.PushComplete.DefaultTimeout();
+
+ var messageData = await TestHelpers.GetResponseDataAsync(new HelloReply { Message = "Hello world" }).DefaultTimeout();
+ await stream.AddDataAndWait(messageData).DefaultTimeout();
+ await stream.AddDataAndWait(Array.Empty()).DefaultTimeout();
+
+ response.TrailingHeaders.Add("custom-header", "value");
+ trailingHeadersWrittenTcs.SetResult(true);
+ });
+
+ return Task.FromResult(response);
+ });
+
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ await call.RequestStream.CompleteAsync().DefaultTimeout();
+ await Task.WhenAll(call.ResponseAsync, trailingHeadersWrittenTcs.Task).DefaultTimeout();
+ var trailers = call.GetTrailers();
+
+ // Assert
+ Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_UncompleteWriter_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var stream = new SyncPointMemoryStream();
+
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream), grpcStatusCode: null);
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound);
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_InvalidContentType_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK);
+ response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.Throws(() => call.GetTrailers());
+
+ // Assert
+ Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj
new file mode 100644
index 000000000..c2dfb4bba
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ netcoreapp3.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs
new file mode 100644
index 000000000..1e5a4b518
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs
@@ -0,0 +1,38 @@
+#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
+
+using NUnit.Framework;
+using Greet;
+using static Greet.Greeter;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class GrpcClientFactoryTests
+ {
+ [Test]
+ public void Create_WithBaseAddress_ReturnInstance()
+ {
+ // Arrange & Act
+ var client = GrpcClientFactory.Create("http://localhost");
+
+ // Assert
+ Assert.IsNotNull(client);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs
new file mode 100644
index 000000000..1ca736199
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs
@@ -0,0 +1,59 @@
+#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
+
+using NUnit.Framework;
+using Greet;
+using static Greet.Greeter;
+using System;
+using Grpc.NetCore.HttpClient.Internal;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class GrpcProtocolHelpersTests
+ {
+ private const int MillisecondsPerSecond = 1000;
+
+ [TestCase(-1, "1n")]
+ [TestCase(-10, "1n")]
+ [TestCase(1, "1m")]
+ [TestCase(10, "10m")]
+ [TestCase(100, "100m")]
+ [TestCase(890, "890m")]
+ [TestCase(900, "900m")]
+ [TestCase(901, "901m")]
+ [TestCase(1000, "1S")]
+ [TestCase(2000, "2S")]
+ [TestCase(2500, "2500m")]
+ [TestCase(59900, "59900m")]
+ [TestCase(50000, "50S")]
+ [TestCase(59000, "59S")]
+ [TestCase(60000, "1M")]
+ [TestCase(80000, "80S")]
+ [TestCase(90000, "90S")]
+ [TestCase(120000, "2M")]
+ [TestCase(20 * 60 * MillisecondsPerSecond, "20M")]
+ [TestCase(60 * 60 * MillisecondsPerSecond, "1H")]
+ [TestCase(10 * 60 * 60 * MillisecondsPerSecond, "10H")]
+ public void EncodeTimeout(int milliseconds, string expected)
+ {
+ var encoded = GrpcProtocolHelpers.EncodeTimeout(milliseconds);
+ Assert.AreEqual(expected, encoded);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs
new file mode 100644
index 000000000..651f62675
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs
@@ -0,0 +1,110 @@
+#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
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using Microsoft.Net.Http.Headers;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class HeadersTests
+ {
+ [Test]
+ public async Task AsyncUnaryCall_SendHeaders_RequestMessageContainsHeaders()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ var headers = new Metadata();
+ headers.Add("custom", "ascii");
+ headers.Add("custom-bin", Encoding.UTF8.GetBytes("Hello world"));
+
+ // Act
+ var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers), new HelloRequest());
+
+ // Assert
+ Assert.AreEqual("Hello world", rs.Message);
+
+ Assert.IsNotNull(httpRequestMessage);
+ Assert.AreEqual("ascii", httpRequestMessage.Headers.GetValues("custom").Single());
+ Assert.AreEqual("Hello world", Encoding.UTF8.GetString(Convert.FromBase64String(httpRequestMessage.Headers.GetValues("custom-bin").Single())));
+ }
+
+ [Test]
+ public async Task AsyncUnaryCall_NoHeaders_RequestMessageHasNoHeaders()
+ {
+ // Arrange
+ HttpRequestMessage httpRequestMessage = null;
+
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ httpRequestMessage = request;
+
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ var headers = new Metadata();
+
+ // Act
+ var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers), new HelloRequest());
+
+ // Assert
+ Assert.AreEqual("Hello world", rs.Message);
+
+ Assert.IsNotNull(httpRequestMessage);
+
+ // User-Agent is always sent
+ Assert.AreEqual(0, httpRequestMessage.Headers.Count(h => !string.Equals(h.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)));
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs
new file mode 100644
index 000000000..d7579454a
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs
@@ -0,0 +1,115 @@
+#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
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class HttpContentClientStreamReaderTests
+ {
+ [Test]
+ public void MoveNext_TokenCanceledBeforeCall_ThrowError()
+ {
+ // Arrange
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var stream = new SyncPointMemoryStream();
+ var content = new StreamContent(stream);
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content));
+ });
+
+ var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance);
+ call.StartServerStreaming(httpClient, new HelloRequest());
+
+ // Act
+ var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token);
+
+ // Assert
+ Assert.IsTrue(moveNextTask1.IsCompleted);
+ var ex = Assert.ThrowsAsync(async () => await moveNextTask1.DefaultTimeout());
+ Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode);
+ }
+
+ [Test]
+ public void MoveNext_TokenCanceledDuringCall_ThrowError()
+ {
+ // Arrange
+ var cts = new CancellationTokenSource();
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var stream = new SyncPointMemoryStream();
+ var content = new StreamContent(stream);
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content));
+ });
+
+ var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance);
+ call.StartServerStreaming(httpClient, new HelloRequest());
+
+ // Act
+ var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token);
+
+ // Assert
+ Assert.IsFalse(moveNextTask1.IsCompleted);
+
+ cts.Cancel();
+
+ var ex = Assert.ThrowsAsync(async () => await moveNextTask1.DefaultTimeout());
+ Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode);
+ }
+
+ [Test]
+ public void MoveNext_MultipleCallsWithoutAwait_ThrowError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var stream = new SyncPointMemoryStream();
+ var content = new StreamContent(stream);
+ return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content));
+ });
+
+ var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance);
+ call.StartServerStreaming(httpClient, new HelloRequest());
+
+ // Act
+ var moveNextTask1 = call.ClientStreamReader.MoveNext(CancellationToken.None);
+ var moveNextTask2 = call.ClientStreamReader.MoveNext(CancellationToken.None);
+
+ // Assert
+ Assert.IsFalse(moveNextTask1.IsCompleted);
+
+ var ex = Assert.ThrowsAsync(async () => await moveNextTask2.DefaultTimeout());
+ Assert.AreEqual("Cannot read next message because the previous read is in progress.", ex.Message);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs
new file mode 100644
index 000000000..87a21705c
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs
@@ -0,0 +1,80 @@
+#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
+
+using System.Net.Http;
+using System;
+using System.Net;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Diagnostics;
+using System.Buffers.Binary;
+using Grpc.NetCore.HttpClient.Internal;
+using Grpc.Core;
+
+namespace Grpc.NetCore.HttpClient.Tests.Infrastructure
+{
+ internal static class ResponseUtils
+ {
+ public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode) =>
+ CreateResponse(statusCode, string.Empty);
+
+ public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string payload) =>
+ CreateResponse(statusCode, new StringContent(payload));
+
+ public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload, StatusCode? grpcStatusCode = StatusCode.OK)
+ {
+ payload.Headers.ContentType = GrpcProtocolConstants.GrpcContentTypeHeaderValue;
+
+ var message = new HttpResponseMessage(statusCode)
+ {
+ Content = payload
+ };
+
+ if (grpcStatusCode != null)
+ {
+ message.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, grpcStatusCode.Value.ToString("D"));
+ }
+
+ return message;
+ }
+
+ private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length"
+ private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag
+
+ public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken)
+ {
+ var headerData = new byte[HeaderSize];
+
+ // Compression flag
+ headerData[0] = compress ? (byte)1 : (byte)0;
+
+ // Message length
+ EncodeMessageLength(length, headerData.AsSpan(1));
+
+ return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken);
+ }
+
+ private static void EncodeMessageLength(int messageLength, Span destination)
+ {
+ Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length.");
+
+ BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs
new file mode 100644
index 000000000..261622d2d
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs
@@ -0,0 +1,82 @@
+#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
+
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Greet;
+using Grpc.Core;
+
+namespace Grpc.NetCore.HttpClient.Tests.Infrastructure
+{
+ public static class TestHelpers
+ {
+ public static readonly Marshaller HelloRequestMarshaller = Marshallers.Create(r => r.ToByteArray(), data => HelloRequest.Parser.ParseFrom(data));
+ public static readonly Marshaller HelloReplyMarshaller = Marshallers.Create(r => r.ToByteArray(), data => HelloReply.Parser.ParseFrom(data));
+
+ public static readonly Method ServiceMethod = new Method(MethodType.Unary, "ServiceName", "MethodName", HelloRequestMarshaller, HelloReplyMarshaller);
+
+ public static System.Net.Http.HttpClient CreateTestClient(Func> sendAsync)
+ {
+ var handler = TestHttpMessageHandler.Create(sendAsync);
+ var httpClient = new System.Net.Http.HttpClient(handler);
+ httpClient.BaseAddress = new Uri("https://localhost");
+
+ return httpClient;
+ }
+
+ public static System.Net.Http.HttpClient CreateTestClient(Func> sendAsync)
+ {
+ var handler = TestHttpMessageHandler.Create(sendAsync);
+ var httpClient = new System.Net.Http.HttpClient(handler);
+ httpClient.BaseAddress = new Uri("https://localhost");
+
+ return httpClient;
+ }
+
+ public static async Task CreateResponseContent(params TResponse[] responses) where TResponse : IMessage
+ {
+ var ms = new MemoryStream();
+ foreach (var response in responses)
+ {
+ await WriteResponseAsync(ms, response);
+ }
+ ms.Seek(0, SeekOrigin.Begin);
+ var streamContent = new StreamContent(ms);
+ streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc");
+ return streamContent;
+ }
+
+ public static async Task WriteResponseAsync(Stream ms, TResponse response) where TResponse : IMessage
+ {
+ await ResponseUtils.WriteHeaderAsync(ms, response.CalculateSize(), false, CancellationToken.None);
+ await ms.WriteAsync(response.ToByteArray());
+ }
+
+ public static async Task GetResponseDataAsync(TResponse response) where TResponse : IMessage
+ {
+ var ms = new MemoryStream();
+ await WriteResponseAsync(ms, response);
+ return ms.ToArray();
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs
new file mode 100644
index 000000000..895cb42e4
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs
@@ -0,0 +1,50 @@
+#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
+
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System;
+
+namespace Grpc.NetCore.HttpClient.Tests.Infrastructure
+{
+ public class TestHttpMessageHandler : HttpMessageHandler
+ {
+ private readonly Func> _sendAsync;
+
+ public TestHttpMessageHandler(Func> sendAsync)
+ {
+ _sendAsync = sendAsync;
+ }
+
+ public static TestHttpMessageHandler Create(Func> sendAsync)
+ {
+ return new TestHttpMessageHandler((request, cancellationToken) => sendAsync(request));
+ }
+
+ public static TestHttpMessageHandler Create(Func> sendAsync)
+ {
+ return new TestHttpMessageHandler(sendAsync);
+ }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _sendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto b/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto
new file mode 100644
index 000000000..85fb59318
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto
@@ -0,0 +1,35 @@
+// 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.
+
+syntax = "proto3";
+
+package Greet;
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
+ rpc SayHellos (HelloRequest) returns (stream HelloReply) {}
+}
+
+service SecondGreeter {
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
+ rpc SayHellos (HelloRequest) returns (stream HelloReply) {}
+}
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string message = 1;
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs
new file mode 100644
index 000000000..0f3fd9dd2
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs
@@ -0,0 +1,152 @@
+#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
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class ResponseAsyncTests
+ {
+ [Test]
+ public async Task AsyncUnaryCall_AwaitMultipleTimes_SameMessageReturned()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+
+ return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" });
+
+ var response1 = await call;
+ var response2 = await call;
+ var response3 = await call.ResponseAsync.DefaultTimeout();
+ var response4 = await call.ResponseAsync.DefaultTimeout();
+
+ // Assert
+ Assert.AreEqual("Hello world", response1.Message);
+
+ Assert.AreEqual(response1, response2);
+ Assert.AreEqual(response1, response3);
+ Assert.AreEqual(response1, response4);
+ }
+
+ [Test]
+ public async Task AsyncUnaryCall_DisposeAfterHeadersAndBeforeMessage_ThrowsError()
+ {
+ // Arrange
+ var stream = new SyncPointMemoryStream();
+
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream));
+ response.Headers.Add("custom", "value!");
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" });
+ var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout();
+ call.Dispose();
+
+ // Assert
+ Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+
+ var header = responseHeaders.Single(h => h.Key == "custom");
+ Assert.AreEqual("value!", header.Value);
+ }
+
+ [Test]
+ public void AsyncUnaryCall_ErrorSendingRequest_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ return Task.FromException(new Exception("An error!"));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var ex = Assert.CatchAsync(() => call.ResponseAsync);
+
+ // Assert
+ Assert.AreEqual("An error!", ex.Message);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound);
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+
+ // Assert
+ Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_InvalidContentType_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK);
+ response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout());
+
+ // Assert
+ Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message);
+ }
+ }
+}
diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs
new file mode 100644
index 000000000..4fcb0bb4b
--- /dev/null
+++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs
@@ -0,0 +1,225 @@
+#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
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+using Greet;
+using Grpc.Core;
+using Grpc.NetCore.HttpClient.Tests.Infrastructure;
+using Grpc.Tests.Shared;
+using NUnit.Framework;
+
+namespace Grpc.NetCore.HttpClient.Tests
+{
+ [TestFixture]
+ public class ResponseHeadersAsyncTests
+ {
+ [Test]
+ public async Task AsyncUnaryCall_Success_ResponseHeadersPopulated()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ HelloReply reply = new HelloReply
+ {
+ Message = "Hello world"
+ };
+
+ var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.Headers.Server.Add(new ProductInfoHeaderValue("TestName", "1.0"));
+ response.Headers.Add("custom", "ABC");
+ response.Headers.Add("binary-bin", Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello world")));
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var responseHeaders1 = await call.ResponseHeadersAsync.DefaultTimeout();
+ var responseHeaders2 = await call.ResponseHeadersAsync.DefaultTimeout();
+
+ // Assert
+ Assert.AreSame(responseHeaders1, responseHeaders2);
+
+ var header = responseHeaders1.Single(h => h.Key == "server");
+ Assert.AreEqual("TestName/1.0", header.Value);
+
+ header = responseHeaders1.Single(h => h.Key == "custom");
+ Assert.AreEqual("ABC", header.Value);
+
+ header = responseHeaders1.Single(h => h.Key == "binary-bin");
+ Assert.AreEqual(true, header.IsBinary);
+ CollectionAssert.AreEqual(Encoding.UTF8.GetBytes("Hello world"), header.ValueBytes);
+ }
+
+ [Test]
+ public async Task AsyncClientStreamingCall_Success_ResponseHeadersPopulated()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.Headers.Add("custom", "ABC");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout();
+
+ // Assert
+ var header = responseHeaders.Single(h => h.Key == "custom");
+ Assert.AreEqual("ABC", header.Value);
+ }
+
+ [Test]
+ public async Task AsyncDuplexStreamingCall_Success_ResponseHeadersPopulated()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.Headers.Add("custom", "ABC");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout();
+
+ // Assert
+ var header = responseHeaders.Single(h => h.Key == "custom");
+ Assert.AreEqual("ABC", header.Value);
+ }
+
+ [Test]
+ public async Task AsyncServerStreamingCall_Success_ResponseHeadersPopulated()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(async request =>
+ {
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.Headers.Add("custom", "ABC");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout();
+
+ // Assert
+ var header = responseHeaders.Single(h => h.Key == "custom");
+ Assert.AreEqual("ABC", header.Value);
+ }
+
+ [Test]
+ public void AsyncServerStreamingCall_ErrorSendingRequest_ReturnsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ return Task.FromException(new Exception("An error!"));
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ var ex = Assert.CatchAsync(() => call.ResponseHeadersAsync);
+
+ // Assert
+ Assert.AreEqual("An error!", ex.Message);
+ }
+
+ [Test]
+ public void AsyncServerStreamingCall_DisposeBeforeHeadersReceived_ReturnsError()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var httpClient = TestHelpers.CreateTestClient(async (request, ct) =>
+ {
+ await tcs.Task.DefaultTimeout();
+ ct.ThrowIfCancellationRequested();
+ var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
+ response.Headers.Add("custom", "ABC");
+ return response;
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest());
+ call.Dispose();
+ tcs.TrySetResult(true);
+
+ // Assert
+ Assert.ThrowsAsync(() => call.ResponseHeadersAsync);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound);
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseHeadersAsync.DefaultTimeout());
+
+ // Assert
+ Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message);
+ }
+
+ [Test]
+ public void AsyncClientStreamingCall_InvalidContentType_ThrowsError()
+ {
+ // Arrange
+ var httpClient = TestHelpers.CreateTestClient(request =>
+ {
+ var response = ResponseUtils.CreateResponse(HttpStatusCode.OK);
+ response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
+ return Task.FromResult(response);
+ });
+ var invoker = new HttpClientCallInvoker(httpClient);
+
+ // Act
+ var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions());
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseHeadersAsync.DefaultTimeout());
+
+ // Assert
+ Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message);
+ }
+ }
+}
diff --git a/test/Shared/HttpContextServerCallContextHelpers.cs b/test/Shared/HttpContextServerCallContextHelpers.cs
index c9b5e8369..7e7439328 100644
--- a/test/Shared/HttpContextServerCallContextHelpers.cs
+++ b/test/Shared/HttpContextServerCallContextHelpers.cs
@@ -22,7 +22,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-namespace Grpc.AspNetCore.FunctionalTests.Infrastructure
+namespace Grpc.Tests.Shared
{
internal static class HttpContextServerCallContextHelper
{
diff --git a/test/Shared/MessageHelpers.cs b/test/Shared/MessageHelpers.cs
index 7b0b51b0d..fdc51558d 100644
--- a/test/Shared/MessageHelpers.cs
+++ b/test/Shared/MessageHelpers.cs
@@ -27,7 +27,7 @@
using Grpc.AspNetCore.Server.Internal;
using Microsoft.AspNetCore.Http;
-namespace Grpc.AspNetCore.FunctionalTests.Infrastructure
+namespace Grpc.Tests.Shared
{
internal static class MessageHelpers
{
diff --git a/test/Shared/SyncPoint.cs b/test/Shared/SyncPoint.cs
index 1d722a50e..0269812dc 100644
--- a/test/Shared/SyncPoint.cs
+++ b/test/Shared/SyncPoint.cs
@@ -20,7 +20,7 @@
using System.Threading;
using System.Threading.Tasks;
-namespace Grpc.AspNetCore.Server.Tests
+namespace Grpc.Tests.Shared
{
public class SyncPoint
{
diff --git a/test/Shared/SyncPointMemoryStream.cs b/test/Shared/SyncPointMemoryStream.cs
index c9e368f3f..cfd81d1cf 100644
--- a/test/Shared/SyncPointMemoryStream.cs
+++ b/test/Shared/SyncPointMemoryStream.cs
@@ -22,7 +22,7 @@
using System.Threading;
using System.Threading.Tasks;
-namespace Grpc.AspNetCore.Server.Tests
+namespace Grpc.Tests.Shared
{
///
/// A memory stream that waits for data when reading and allows the sender of data to wait for it to be read.
diff --git a/test/FunctionalTests/Infrastructure/TaskExtensions.cs b/test/Shared/TaskExtensions.cs
similarity index 98%
rename from test/FunctionalTests/Infrastructure/TaskExtensions.cs
rename to test/Shared/TaskExtensions.cs
index 894c82a84..ea708499e 100644
--- a/test/FunctionalTests/Infrastructure/TaskExtensions.cs
+++ b/test/Shared/TaskExtensions.cs
@@ -22,7 +22,7 @@
using System.Threading;
using System.Threading.Tasks;
-namespace Grpc.AspNetCore.FunctionalTests.Infrastructure
+namespace Grpc.Tests.Shared
{
internal static class TaskExtensions
{
diff --git a/testassets/FunctionalTestsWebsite/Startup.cs b/testassets/FunctionalTestsWebsite/Startup.cs
index 8abffa8e3..b260a20e6 100644
--- a/testassets/FunctionalTestsWebsite/Startup.cs
+++ b/testassets/FunctionalTestsWebsite/Startup.cs
@@ -67,7 +67,6 @@ public void ConfigureServices(IServiceCollection services)
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
- services.AddAuthorizationPolicyEvaluator();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
diff --git a/testassets/InteropTestsClient/Certs/README.md b/testassets/InteropTestsClient/Certs/README.md
new file mode 100644
index 000000000..79206a486
--- /dev/null
+++ b/testassets/InteropTestsClient/Certs/README.md
@@ -0,0 +1,8 @@
+Keys taken from https://github.com/grpc/grpc/tree/master/src/core/tsi/test_creds
+so that interop server in this project is compatible with interop clients
+implemented in other gRPC languages.
+
+The server1.pem and server1.key were combined into server1.pfx. The password is 1111. These certs are not secure, do not use in production.
+```
+openssl pkcs12 -export -out server1.pfx -inkey server1.key -in server1.pem -certfile ca.pem
+```
diff --git a/testassets/InteropTestsClient/Certs/ca.pem b/testassets/InteropTestsClient/Certs/ca.pem
new file mode 100644
index 000000000..6c8511a73
--- /dev/null
+++ b/testassets/InteropTestsClient/Certs/ca.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
+Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
+YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
+BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
+g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
+Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
+sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
+oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
+Dfcog5wrJytaQ6UA0wE=
+-----END CERTIFICATE-----
diff --git a/testassets/InteropTestsClient/Certs/server1.key b/testassets/InteropTestsClient/Certs/server1.key
new file mode 100644
index 000000000..143a5b876
--- /dev/null
+++ b/testassets/InteropTestsClient/Certs/server1.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
+M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
+3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
+AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
+V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
+tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
+dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
+K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
+81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
+DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
+aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
+ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
+XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
+F98XJ7tIFfJq
+-----END PRIVATE KEY-----
diff --git a/testassets/InteropTestsClient/Certs/server1.pem b/testassets/InteropTestsClient/Certs/server1.pem
new file mode 100644
index 000000000..f3d43fcc5
--- /dev/null
+++ b/testassets/InteropTestsClient/Certs/server1.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
+MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
+dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
+MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
+BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
+ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
+LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
+zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
+9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
+CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
+em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
+CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
+hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
+y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
+-----END CERTIFICATE-----
diff --git a/testassets/InteropTestsClient/Certs/server1.pfx b/testassets/InteropTestsClient/Certs/server1.pfx
new file mode 100644
index 000000000..252acc126
Binary files /dev/null and b/testassets/InteropTestsClient/Certs/server1.pfx differ
diff --git a/testassets/InteropTestsClient/IChannel.cs b/testassets/InteropTestsClient/IChannel.cs
new file mode 100644
index 000000000..47a2c949b
--- /dev/null
+++ b/testassets/InteropTestsClient/IChannel.cs
@@ -0,0 +1,61 @@
+#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
+
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Grpc.Core;
+
+namespace InteropTestsClient
+{
+ public interface IChannel
+ {
+ Task ShutdownAsync();
+ }
+
+ public class HttpClientChannel : IChannel
+ {
+ public HttpClientHandler HttpClientHandler { get; }
+
+ public HttpClientChannel(HttpClientHandler httpClient)
+ {
+ HttpClientHandler = httpClient;
+ }
+
+ public Task ShutdownAsync()
+ {
+ HttpClientHandler.Dispose();
+ return Task.CompletedTask;
+ }
+ }
+
+ public class CoreChannel : IChannel
+ {
+ public Channel Channel { get; }
+
+ public CoreChannel(Channel channel)
+ {
+ Channel = channel;
+ }
+
+ public Task ShutdownAsync()
+ {
+ return Channel.ShutdownAsync();
+ }
+ }
+}
diff --git a/testassets/InteropTestsClient/InteropClient.cs b/testassets/InteropTestsClient/InteropClient.cs
new file mode 100644
index 000000000..2702f1960
--- /dev/null
+++ b/testassets/InteropTestsClient/InteropClient.cs
@@ -0,0 +1,772 @@
+#region Copyright notice and license
+
+// Copyright 2015-2016 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
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using CommandLine;
+using Google.Apis.Auth.OAuth2;
+using Google.Protobuf;
+using Grpc.Auth;
+using Grpc.Core;
+using Grpc.Core.Logging;
+using Grpc.Core.Utils;
+using Grpc.NetCore.HttpClient;
+using Grpc.Testing;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+
+namespace InteropTestsClient
+{
+ public class InteropClient
+ {
+ internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request";
+
+ private class ClientOptions
+ {
+ [Option("client_type"
+#if DEBUG
+ , Default = "httpclient"
+#endif
+ )]
+ public string ClientType { get; set; }
+
+ [Option("server_host", Default = "localhost")]
+ public string ServerHost { get; set; }
+
+ [Option("server_host_override")]
+ public string ServerHostOverride { get; set; }
+
+ [Option("server_port"
+#if DEBUG
+ , Default = 50052
+#endif
+ )]
+ public int ServerPort { get; set; }
+
+ [Option("test_case"
+#if DEBUG
+ , Default = "large_unary"
+#endif
+ )]
+ public string TestCase { get; set; }
+
+ // Deliberately using nullable bool type to allow --use_tls=true syntax (as opposed to --use_tls)
+ [Option("use_tls", Default = false)]
+ public bool? UseTls { get; set; }
+
+ // Deliberately using nullable bool type to allow --use_test_ca=true syntax (as opposed to --use_test_ca)
+ [Option("use_test_ca", Default = false)]
+ public bool? UseTestCa { get; set; }
+
+ [Option("default_service_account", Required = false)]
+ public string DefaultServiceAccount { get; set; }
+
+ [Option("oauth_scope", Required = false)]
+ public string OAuthScope { get; set; }
+
+ [Option("service_account_key_file", Required = false)]
+ public string ServiceAccountKeyFile { get; set; }
+ }
+
+ ClientOptions options;
+
+ private InteropClient(ClientOptions options)
+ {
+ this.options = options;
+ }
+
+ public static void Run(string[] args)
+ {
+ GrpcEnvironment.SetLogger(new ConsoleLogger());
+ var parserResult = Parser.Default.ParseArguments(args)
+ .WithNotParsed(errors => Environment.Exit(1))
+ .WithParsed(options =>
+ {
+ Console.WriteLine("Use TLS: " + options.UseTls);
+ Console.WriteLine("Server host: " + options.ServerHost);
+ Console.WriteLine("Server port: " + options.ServerPort);
+
+ var interopClient = new InteropClient(options);
+ interopClient.Run().Wait();
+ });
+ }
+
+ private async Task Run()
+ {
+ IChannel channel = IsHttpClient() ? await HttpClientCreateChannel() : await CoreCreateChannel();
+ await RunTestCaseAsync(channel, options);
+ await channel.ShutdownAsync();
+ }
+
+ private Task HttpClientCreateChannel()
+ {
+ AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
+ AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
+
+ var httpClientHandler = new HttpClientHandler();
+ httpClientHandler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true;
+
+ return Task.FromResult(new HttpClientChannel(httpClientHandler));
+ }
+
+ private async Task CoreCreateChannel()
+ {
+ var credentials = await CreateCredentialsAsync();
+
+ List channelOptions = null;
+ if (!string.IsNullOrEmpty(options.ServerHostOverride))
+ {
+ channelOptions = new List
+ {
+ new ChannelOption(ChannelOptions.SslTargetNameOverride, options.ServerHostOverride)
+ };
+ }
+ var channel = new Channel(options.ServerHost, options.ServerPort, credentials, channelOptions);
+ await channel.ConnectAsync();
+ return new CoreChannel(channel);
+ }
+
+ private bool IsHttpClient() => string.Equals(options.ClientType, "httpclient", StringComparison.OrdinalIgnoreCase);
+
+ private async Task CreateCredentialsAsync()
+ {
+ var credentials = ChannelCredentials.Insecure;
+ if (options.UseTls.Value)
+ {
+ credentials = options.UseTestCa.Value ? TestCredentials.CreateSslCredentials() : new SslCredentials();
+ }
+
+ if (options.TestCase == "jwt_token_creds")
+ {
+ var googleCredential = await GoogleCredential.GetApplicationDefaultAsync();
+ Assert.IsTrue(googleCredential.IsCreateScopedRequired);
+ credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials());
+ }
+
+ if (options.TestCase == "compute_engine_creds")
+ {
+ var googleCredential = await GoogleCredential.GetApplicationDefaultAsync();
+ Assert.IsFalse(googleCredential.IsCreateScopedRequired);
+ credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials());
+ }
+ return credentials;
+ }
+
+ private TClient CreateClient(IChannel channel) where TClient : ClientBase
+ {
+ if (channel is CoreChannel coreChannel)
+ {
+ return (TClient)Activator.CreateInstance(typeof(TClient), coreChannel.Channel);
+ }
+ else if (channel is HttpClientChannel httpClientChannel)
+ {
+ return GrpcClientFactory.Create($"http://{options.ServerHost}:{options.ServerPort}", certificate: null);
+ }
+ else
+ {
+ throw new Exception("Unexpected channel type.");
+ }
+ }
+
+ private async Task RunTestCaseAsync(IChannel channel, ClientOptions options)
+ {
+ var client = CreateClient(channel);
+ switch (options.TestCase)
+ {
+ case "empty_unary":
+ RunEmptyUnary(client);
+ break;
+ case "large_unary":
+ RunLargeUnary(client);
+ break;
+ case "client_streaming":
+ await RunClientStreamingAsync(client);
+ break;
+ case "server_streaming":
+ await RunServerStreamingAsync(client);
+ break;
+ case "ping_pong":
+ await RunPingPongAsync(client);
+ break;
+ case "empty_stream":
+ await RunEmptyStreamAsync(client);
+ break;
+ case "compute_engine_creds":
+ RunComputeEngineCreds(client, options.DefaultServiceAccount, options.OAuthScope);
+ break;
+ case "jwt_token_creds":
+ RunJwtTokenCreds(client);
+ break;
+ case "oauth2_auth_token":
+ await RunOAuth2AuthTokenAsync(client, options.OAuthScope);
+ break;
+ case "per_rpc_creds":
+ await RunPerRpcCredsAsync(client, options.OAuthScope);
+ break;
+ case "cancel_after_begin":
+ await RunCancelAfterBeginAsync(client);
+ break;
+ case "cancel_after_first_response":
+ await RunCancelAfterFirstResponseAsync(client);
+ break;
+ case "timeout_on_sleeping_server":
+ await RunTimeoutOnSleepingServerAsync(client);
+ break;
+ case "custom_metadata":
+ await RunCustomMetadataAsync(client);
+ break;
+ case "status_code_and_message":
+ await RunStatusCodeAndMessageAsync(client);
+ break;
+ case "unimplemented_service":
+ RunUnimplementedService(CreateClient(channel));
+ break;
+ case "unimplemented_method":
+ RunUnimplementedMethod(client);
+ break;
+ case "client_compressed_unary":
+ RunClientCompressedUnary(client);
+ break;
+ case "client_compressed_streaming":
+ await RunClientCompressedStreamingAsync(client);
+ break;
+ default:
+ throw new ArgumentException("Unknown test case " + options.TestCase);
+ }
+ }
+
+ public static void RunEmptyUnary(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running empty_unary");
+ var response = client.EmptyCall(new Empty());
+ Assert.IsNotNull(response);
+ Console.WriteLine("Passed!");
+ }
+
+ public static void RunLargeUnary(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running large_unary");
+ var request = new SimpleRequest
+ {
+ ResponseSize = 314159,
+ Payload = CreateZerosPayload(271828)
+ };
+ var response = client.UnaryCall(request);
+
+ Assert.AreEqual(314159, response.Payload.Body.Length);
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunClientStreamingAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running client_streaming");
+
+ var bodySizes = new List { 27182, 8, 1828, 45904 }.Select((size) => new StreamingInputCallRequest { Payload = CreateZerosPayload(size) });
+
+ using (var call = client.StreamingInputCall())
+ {
+ await call.RequestStream.WriteAllAsync(bodySizes);
+
+ var response = await call.ResponseAsync;
+ Assert.AreEqual(74922, response.AggregatedPayloadSize);
+ }
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunServerStreamingAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running server_streaming");
+
+ var bodySizes = new List { 31415, 9, 2653, 58979 };
+
+ var request = new StreamingOutputCallRequest
+ {
+ ResponseParameters = { bodySizes.Select((size) => new ResponseParameters { Size = size }) }
+ };
+
+ using (var call = client.StreamingOutputCall(request))
+ {
+ var responseList = await call.ResponseStream.ToListAsync();
+ CollectionAssert.AreEqual(bodySizes, responseList.Select((item) => item.Payload.Body.Length));
+ }
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunPingPongAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running ping_pong");
+
+ using (var call = client.FullDuplexCall())
+ {
+ await call.RequestStream.WriteAsync(new StreamingOutputCallRequest
+ {
+ ResponseParameters = { new ResponseParameters { Size = 31415 } },
+ Payload = CreateZerosPayload(27182)
+ });
+
+ Assert.IsTrue(await call.ResponseStream.MoveNext());
+ Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length);
+
+ await call.RequestStream.WriteAsync(new StreamingOutputCallRequest
+ {
+ ResponseParameters = { new ResponseParameters { Size = 9 } },
+ Payload = CreateZerosPayload(8)
+ });
+
+ Assert.IsTrue(await call.ResponseStream.MoveNext());
+ Assert.AreEqual(9, call.ResponseStream.Current.Payload.Body.Length);
+
+ await call.RequestStream.WriteAsync(new StreamingOutputCallRequest
+ {
+ ResponseParameters = { new ResponseParameters { Size = 2653 } },
+ Payload = CreateZerosPayload(1828)
+ });
+
+ Assert.IsTrue(await call.ResponseStream.MoveNext());
+ Assert.AreEqual(2653, call.ResponseStream.Current.Payload.Body.Length);
+
+ await call.RequestStream.WriteAsync(new StreamingOutputCallRequest
+ {
+ ResponseParameters = { new ResponseParameters { Size = 58979 } },
+ Payload = CreateZerosPayload(45904)
+ });
+
+ Assert.IsTrue(await call.ResponseStream.MoveNext());
+ Assert.AreEqual(58979, call.ResponseStream.Current.Payload.Body.Length);
+
+ await call.RequestStream.CompleteAsync();
+
+ Assert.IsFalse(await call.ResponseStream.MoveNext());
+ }
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunEmptyStreamAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running empty_stream");
+ using (var call = client.FullDuplexCall())
+ {
+ await call.RequestStream.CompleteAsync();
+
+ var responseList = await call.ResponseStream.ToListAsync();
+ Assert.AreEqual(0, responseList.Count);
+ }
+ Console.WriteLine("Passed!");
+ }
+
+ public static void RunComputeEngineCreds(TestService.TestServiceClient client, string defaultServiceAccount, string oauthScope)
+ {
+ Console.WriteLine("running compute_engine_creds");
+
+ var request = new SimpleRequest
+ {
+ ResponseSize = 314159,
+ Payload = CreateZerosPayload(271828),
+ FillUsername = true,
+ FillOauthScope = true
+ };
+
+ // not setting credentials here because they were set on channel already
+ var response = client.UnaryCall(request);
+
+ Assert.AreEqual(314159, response.Payload.Body.Length);
+ Assert.False(string.IsNullOrEmpty(response.OauthScope));
+ Assert.True(oauthScope.Contains(response.OauthScope));
+ Assert.AreEqual(defaultServiceAccount, response.Username);
+ Console.WriteLine("Passed!");
+ }
+
+ public static void RunJwtTokenCreds(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running jwt_token_creds");
+
+ var request = new SimpleRequest
+ {
+ ResponseSize = 314159,
+ Payload = CreateZerosPayload(271828),
+ FillUsername = true,
+ };
+
+ // not setting credentials here because they were set on channel already
+ var response = client.UnaryCall(request);
+
+ Assert.AreEqual(314159, response.Payload.Body.Length);
+ Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username);
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunOAuth2AuthTokenAsync(TestService.TestServiceClient client, string oauthScope)
+ {
+ Console.WriteLine("running oauth2_auth_token");
+ ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { oauthScope });
+ string oauth2Token = await credential.GetAccessTokenForRequestAsync();
+
+ var credentials = GoogleGrpcCredentials.FromAccessToken(oauth2Token);
+ var request = new SimpleRequest
+ {
+ FillUsername = true,
+ FillOauthScope = true
+ };
+
+ var response = client.UnaryCall(request, new CallOptions(credentials: credentials));
+
+ Assert.False(string.IsNullOrEmpty(response.OauthScope));
+ Assert.True(oauthScope.Contains(response.OauthScope));
+ Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username);
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunPerRpcCredsAsync(TestService.TestServiceClient client, string oauthScope)
+ {
+ Console.WriteLine("running per_rpc_creds");
+ ITokenAccess googleCredential = await GoogleCredential.GetApplicationDefaultAsync();
+
+ var credentials = googleCredential.ToCallCredentials();
+ var request = new SimpleRequest
+ {
+ FillUsername = true,
+ };
+
+ var response = client.UnaryCall(request, new CallOptions(credentials: credentials));
+
+ Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username);
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunCancelAfterBeginAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running cancel_after_begin");
+
+ var cts = new CancellationTokenSource();
+ using (var call = client.StreamingInputCall(cancellationToken: cts.Token))
+ {
+ // TODO(jtattermusch): we need this to ensure call has been initiated once we cancel it.
+ await Task.Delay(1000);
+ cts.Cancel();
+
+ var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync);
+ Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode);
+ }
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunCancelAfterFirstResponseAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running cancel_after_first_response");
+
+ var cts = new CancellationTokenSource();
+ using (var call = client.FullDuplexCall(cancellationToken: cts.Token))
+ {
+ await call.RequestStream.WriteAsync(new StreamingOutputCallRequest
+ {
+ ResponseParameters = { new ResponseParameters { Size = 31415 } },
+ Payload = CreateZerosPayload(27182)
+ });
+
+ Assert.IsTrue(await call.ResponseStream.MoveNext());
+ Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length);
+
+ cts.Cancel();
+
+ try
+ {
+ // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock.
+ await call.ResponseStream.MoveNext();
+ Assert.Fail();
+ }
+ catch (RpcException ex)
+ {
+ Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode);
+ }
+ }
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunTimeoutOnSleepingServerAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running timeout_on_sleeping_server");
+
+ var deadline = DateTime.UtcNow.AddMilliseconds(1);
+ using (var call = client.FullDuplexCall(deadline: deadline))
+ {
+ try
+ {
+ await call.RequestStream.WriteAsync(new StreamingOutputCallRequest { Payload = CreateZerosPayload(27182) });
+ }
+ catch (InvalidOperationException)
+ {
+ // Deadline was reached before write has started. Eat the exception and continue.
+ }
+ catch (RpcException)
+ {
+ // Deadline was reached before write has started. Eat the exception and continue.
+ }
+
+ try
+ {
+ await call.ResponseStream.MoveNext();
+ Assert.Fail();
+ }
+ catch (RpcException ex)
+ {
+ // We can't guarantee the status code always DeadlineExceeded. See issue #2685.
+ Assert.Contains(ex.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal });
+ }
+ }
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunCustomMetadataAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running custom_metadata");
+ {
+ // step 1: test unary call
+ var request = new SimpleRequest
+ {
+ ResponseSize = 314159,
+ Payload = CreateZerosPayload(271828)
+ };
+
+ var call = client.UnaryCallAsync(request, headers: CreateTestMetadata());
+ await call.ResponseAsync;
+
+ var responseHeaders = await call.ResponseHeadersAsync;
+ var responseTrailers = call.GetTrailers();
+
+ Assert.AreEqual("test_initial_metadata_value", responseHeaders.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value);
+ CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes);
+ }
+
+ {
+ // step 2: test full duplex call
+ var request = new StreamingOutputCallRequest
+ {
+ ResponseParameters = { new ResponseParameters { Size = 31415 } },
+ Payload = CreateZerosPayload(27182)
+ };
+
+ var call = client.FullDuplexCall(headers: CreateTestMetadata());
+
+ await call.RequestStream.WriteAsync(request);
+ await call.RequestStream.CompleteAsync();
+ await call.ResponseStream.ToListAsync();
+
+ var responseHeaders = await call.ResponseHeadersAsync;
+ var responseTrailers = call.GetTrailers();
+
+ Assert.AreEqual("test_initial_metadata_value", responseHeaders.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value);
+ CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes);
+ }
+
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunStatusCodeAndMessageAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running status_code_and_message");
+ var echoStatus = new EchoStatus
+ {
+ Code = 2,
+ Message = "test status message"
+ };
+
+ {
+ // step 1: test unary call
+ var request = new SimpleRequest { ResponseStatus = echoStatus };
+
+ var e = Assert.Throws(() => client.UnaryCall(request));
+ Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode);
+ Assert.AreEqual(echoStatus.Message, e.Status.Detail);
+ }
+
+ {
+ // step 2: test full duplex call
+ var request = new StreamingOutputCallRequest { ResponseStatus = echoStatus };
+
+ var call = client.FullDuplexCall();
+ await call.RequestStream.WriteAsync(request);
+ await call.RequestStream.CompleteAsync();
+
+ try
+ {
+ // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock.
+ await call.ResponseStream.ToListAsync();
+ Assert.Fail();
+ }
+ catch (RpcException e)
+ {
+ Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode);
+ Assert.AreEqual(echoStatus.Message, e.Status.Detail);
+ }
+ }
+
+ Console.WriteLine("Passed!");
+ }
+
+ public static void RunUnimplementedService(UnimplementedService.UnimplementedServiceClient client)
+ {
+ Console.WriteLine("running unimplemented_service");
+ var e = Assert.Throws(() => client.UnimplementedCall(new Empty()));
+
+ Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode);
+ Console.WriteLine("Passed!");
+ }
+
+ public static void RunUnimplementedMethod(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running unimplemented_method");
+ var e = Assert.Throws(() => client.UnimplementedCall(new Empty()));
+
+ Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode);
+ Console.WriteLine("Passed!");
+ }
+
+ public static void RunClientCompressedUnary(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running client_compressed_unary");
+ var probeRequest = new SimpleRequest
+ {
+ ExpectCompressed = new BoolValue
+ {
+ Value = true // lie about compression
+ },
+ ResponseSize = 314159,
+ Payload = CreateZerosPayload(271828)
+ };
+ var e = Assert.Throws(() => client.UnaryCall(probeRequest, CreateClientCompressionMetadata(false)));
+ Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode);
+
+ var compressedRequest = new SimpleRequest
+ {
+ ExpectCompressed = new BoolValue
+ {
+ Value = true
+ },
+ ResponseSize = 314159,
+ Payload = CreateZerosPayload(271828)
+ };
+ var response1 = client.UnaryCall(compressedRequest, CreateClientCompressionMetadata(true));
+ Assert.AreEqual(314159, response1.Payload.Body.Length);
+
+ var uncompressedRequest = new SimpleRequest
+ {
+ ExpectCompressed = new BoolValue
+ {
+ Value = false
+ },
+ ResponseSize = 314159,
+ Payload = CreateZerosPayload(271828)
+ };
+ var response2 = client.UnaryCall(uncompressedRequest, CreateClientCompressionMetadata(false));
+ Assert.AreEqual(314159, response2.Payload.Body.Length);
+
+ Console.WriteLine("Passed!");
+ }
+
+ public static async Task RunClientCompressedStreamingAsync(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running client_compressed_streaming");
+ try
+ {
+ var probeCall = client.StreamingInputCall(CreateClientCompressionMetadata(false));
+ await probeCall.RequestStream.WriteAsync(new StreamingInputCallRequest
+ {
+ ExpectCompressed = new BoolValue
+ {
+ Value = true
+ },
+ Payload = CreateZerosPayload(27182)
+ });
+
+ // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock.
+ await probeCall;
+ Assert.Fail();
+ }
+ catch (RpcException e)
+ {
+ Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode);
+ }
+
+ var call = client.StreamingInputCall(CreateClientCompressionMetadata(true));
+ await call.RequestStream.WriteAsync(new StreamingInputCallRequest
+ {
+ ExpectCompressed = new BoolValue
+ {
+ Value = true
+ },
+ Payload = CreateZerosPayload(27182)
+ });
+
+ call.RequestStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress);
+ await call.RequestStream.WriteAsync(new StreamingInputCallRequest
+ {
+ ExpectCompressed = new BoolValue
+ {
+ Value = false
+ },
+ Payload = CreateZerosPayload(45904)
+ });
+ await call.RequestStream.CompleteAsync();
+
+ var response = await call.ResponseAsync;
+ Assert.AreEqual(73086, response.AggregatedPayloadSize);
+
+ Console.WriteLine("Passed!");
+ }
+
+ private static Payload CreateZerosPayload(int size)
+ {
+ return new Payload { Body = ByteString.CopyFrom(new byte[size]) };
+ }
+
+ private static Metadata CreateClientCompressionMetadata(bool compressed)
+ {
+ var algorithmName = compressed ? "gzip" : "identity";
+ return new Metadata
+ {
+ { new Metadata.Entry(CompressionRequestAlgorithmMetadataKey, algorithmName) }
+ };
+ }
+
+ // extracts the client_email field from service account file used for auth test cases
+ private static string GetEmailFromServiceAccountFile()
+ {
+ string keyFile = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS");
+ Assert.IsNotNull(keyFile);
+ var jobject = JObject.Parse(File.ReadAllText(keyFile));
+ string email = jobject.GetValue("client_email").Value();
+ Assert.IsTrue(email.Length > 0); // spec requires nonempty client email.
+ return email;
+ }
+
+ private static Metadata CreateTestMetadata()
+ {
+ return new Metadata
+ {
+ {"x-grpc-test-echo-initial", "test_initial_metadata_value"},
+ {"x-grpc-test-echo-trailing-bin", new byte[] {0xab, 0xab, 0xab}}
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/testassets/InteropTestsClient/InteropTestsClient.csproj b/testassets/InteropTestsClient/InteropTestsClient.csproj
new file mode 100644
index 000000000..55d7f44be
--- /dev/null
+++ b/testassets/InteropTestsClient/InteropTestsClient.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ netcoreapp3.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testassets/InteropTestsClient/Program.cs b/testassets/InteropTestsClient/Program.cs
new file mode 100644
index 000000000..3e204d853
--- /dev/null
+++ b/testassets/InteropTestsClient/Program.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 InteropTestsClient
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ InteropClient.Run(args);
+ }
+ }
+}
diff --git a/testassets/InteropTestsClient/RunTests.ps1 b/testassets/InteropTestsClient/RunTests.ps1
new file mode 100644
index 000000000..9ef6fd741
--- /dev/null
+++ b/testassets/InteropTestsClient/RunTests.ps1
@@ -0,0 +1,35 @@
+$allTests =
+ "empty_unary",
+ "large_unary",
+ "client_streaming",
+ "server_streaming",
+ #"ping_pong",
+ "empty_stream",
+
+ #"compute_engine_creds",
+ #"jwt_token_creds",
+ #"oauth2_auth_token",
+ #"per_rpc_creds",
+
+ "cancel_after_begin",
+ #"cancel_after_first_response",
+ "timeout_on_sleeping_server",
+ "custom_metadata",
+ "status_code_and_message",
+ "unimplemented_service",
+ "unimplemented_method"
+ #,
+ #"client_compressed_unary",
+ #"client_compressed_streaming"
+
+Write-Host "Running $($allTests.Count) tests" -ForegroundColor Cyan
+Write-Host
+
+foreach ($test in $allTests)
+{
+ Write-Host "Running $test" -ForegroundColor Cyan
+ dotnet run --use_tls false --server_port 50052 --client_type httpclient --test_case $test
+ Write-Host
+}
+
+Write-Host "Done" -ForegroundColor Cyan
\ No newline at end of file
diff --git a/testassets/InteropTestsClient/TestCredentials.cs b/testassets/InteropTestsClient/TestCredentials.cs
new file mode 100644
index 000000000..088fcd89b
--- /dev/null
+++ b/testassets/InteropTestsClient/TestCredentials.cs
@@ -0,0 +1,75 @@
+#region Copyright notice and license
+
+// Copyright 2015 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
+
+using System.IO;
+using System.Reflection;
+using Grpc.Core;
+
+namespace InteropTestsClient
+{
+ ///
+ /// SSL Credentials for testing.
+ ///
+ public static class TestCredentials
+ {
+ public const string DefaultHostOverride = "foo.test.google.fr";
+
+ public static string ClientCertAuthorityPath
+ {
+ get
+ {
+ return GetPath("data/ca.pem");
+ }
+ }
+
+ public static string ServerCertChainPath
+ {
+ get
+ {
+ return GetPath("data/server1.pem");
+ }
+ }
+
+ public static string ServerPrivateKeyPath
+ {
+ get
+ {
+ return GetPath("data/server1.key");
+ }
+ }
+
+ public static SslCredentials CreateSslCredentials()
+ {
+ return new SslCredentials(File.ReadAllText(ClientCertAuthorityPath));
+ }
+
+ public static SslServerCredentials CreateSslServerCredentials()
+ {
+ var keyCertPair = new KeyCertificatePair(
+ File.ReadAllText(ServerCertChainPath),
+ File.ReadAllText(ServerPrivateKeyPath));
+ return new SslServerCredentials(new[] { keyCertPair });
+ }
+
+ private static string GetPath(string relativePath)
+ {
+ var assemblyDir = Path.GetDirectoryName(typeof(TestCredentials).GetTypeInfo().Assembly.Location);
+ return Path.Combine(assemblyDir, relativePath);
+ }
+ }
+}
diff --git a/testassets/InteropTestsWebsite/Program.cs b/testassets/InteropTestsWebsite/Program.cs
index a53520ead..7c627785b 100644
--- a/testassets/InteropTestsWebsite/Program.cs
+++ b/testassets/InteropTestsWebsite/Program.cs
@@ -16,6 +16,7 @@
#endregion
+using System;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
@@ -43,6 +44,8 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
options.Limits.MinRequestBodyDataRate = null;
options.ListenAnyIP(port, listenOptions =>
{
+ Console.WriteLine($"Enabling connection encryption: {useTls}");
+
if (useTls)
{
listenOptions.UseHttps(Resources.ServerPFXPath, "1111");
diff --git a/testassets/InteropTestsWebsite/TestServiceImpl.cs b/testassets/InteropTestsWebsite/TestServiceImpl.cs
index d9c46cb93..cfc5ab706 100644
--- a/testassets/InteropTestsWebsite/TestServiceImpl.cs
+++ b/testassets/InteropTestsWebsite/TestServiceImpl.cs
@@ -39,6 +39,7 @@ public override async Task UnaryCall(SimpleRequest request, Serv
{
await EnsureEchoMetadataAsync(context);
EnsureEchoStatus(request.ResponseStatus, context);
+ EnsureCompression(request.ExpectCompressed, context);
var response = new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) };
return response;
@@ -63,6 +64,8 @@ public override async Task StreamingInputCall(IAsync
int sum = 0;
await requestStream.ForEachAsync(request =>
{
+ EnsureCompression(request.ExpectCompressed, context);
+
sum += request.Payload.Body.Length;
return Task.CompletedTask;
});
@@ -116,5 +119,20 @@ private static void EnsureEchoStatus(EchoStatus responseStatus, ServerCallContex
context.Status = new Status(statusCode, responseStatus.Message);
}
}
+
+ private static void EnsureCompression(BoolValue expectCompressed, ServerCallContext context)
+ {
+ if (expectCompressed != null)
+ {
+ string encoding = context.RequestHeaders.SingleOrDefault(h => h.Key == "grpc-encoding")?.Value;
+ if (expectCompressed.Value)
+ {
+ if (encoding == null || encoding == "identity")
+ {
+ throw new RpcException(new Status(StatusCode.InvalidArgument, string.Empty));
+ }
+ }
+ }
+ }
}
}
diff --git a/testassets/Proto/empty.proto b/testassets/Proto/empty.proto
new file mode 100644
index 000000000..6a0aa88df
--- /dev/null
+++ b/testassets/Proto/empty.proto
@@ -0,0 +1,28 @@
+
+// Copyright 2015 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.
+
+syntax = "proto3";
+
+package grpc.testing;
+
+// An empty message that you can re-use to avoid defining duplicated empty
+// messages in your project. A typical example is to use it as argument or the
+// return value of a service API. For instance:
+//
+// service Foo {
+// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { };
+// };
+//
+message Empty {}
diff --git a/testassets/Proto/messages.proto b/testassets/Proto/messages.proto
new file mode 100644
index 000000000..7b1b7286d
--- /dev/null
+++ b/testassets/Proto/messages.proto
@@ -0,0 +1,165 @@
+
+// Copyright 2015-2016 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.
+
+// Message definitions to be used by integration test service definitions.
+
+syntax = "proto3";
+
+package grpc.testing;
+
+// TODO(dgq): Go back to using well-known types once
+// https://github.com/grpc/grpc/issues/6980 has been fixed.
+// import "google/protobuf/wrappers.proto";
+message BoolValue {
+ // The bool value.
+ bool value = 1;
+}
+
+// The type of payload that should be returned.
+enum PayloadType {
+ // Compressable text format.
+ COMPRESSABLE = 0;
+}
+
+// A block of data, to simply increase gRPC message size.
+message Payload {
+ // The type of data in body.
+ PayloadType type = 1;
+ // Primary contents of payload.
+ bytes body = 2;
+}
+
+// A protobuf representation for grpc status. This is used by test
+// clients to specify a status that the server should attempt to return.
+message EchoStatus {
+ int32 code = 1;
+ string message = 2;
+}
+
+// Unary request.
+message SimpleRequest {
+ // Desired payload type in the response from the server.
+ // If response_type is RANDOM, server randomly chooses one from other formats.
+ PayloadType response_type = 1;
+
+ // Desired payload size in the response from the server.
+ int32 response_size = 2;
+
+ // Optional input payload sent along with the request.
+ Payload payload = 3;
+
+ // Whether SimpleResponse should include username.
+ bool fill_username = 4;
+
+ // Whether SimpleResponse should include OAuth scope.
+ bool fill_oauth_scope = 5;
+
+ // Whether to request the server to compress the response. This field is
+ // "nullable" in order to interoperate seamlessly with clients not able to
+ // implement the full compression tests by introspecting the call to verify
+ // the response's compression status.
+ BoolValue response_compressed = 6;
+
+ // Whether server should return a given status
+ EchoStatus response_status = 7;
+
+ // Whether the server should expect this request to be compressed.
+ BoolValue expect_compressed = 8;
+}
+
+// Unary response, as configured by the request.
+message SimpleResponse {
+ // Payload to increase message size.
+ Payload payload = 1;
+ // The user the request came from, for verifying authentication was
+ // successful when the client expected it.
+ string username = 2;
+ // OAuth scope.
+ string oauth_scope = 3;
+}
+
+// Client-streaming request.
+message StreamingInputCallRequest {
+ // Optional input payload sent along with the request.
+ Payload payload = 1;
+
+ // Whether the server should expect this request to be compressed. This field
+ // is "nullable" in order to interoperate seamlessly with servers not able to
+ // implement the full compression tests by introspecting the call to verify
+ // the request's compression status.
+ BoolValue expect_compressed = 2;
+
+ // Not expecting any payload from the response.
+}
+
+// Client-streaming response.
+message StreamingInputCallResponse {
+ // Aggregated size of payloads received from the client.
+ int32 aggregated_payload_size = 1;
+}
+
+// Configuration for a particular response.
+message ResponseParameters {
+ // Desired payload sizes in responses from the server.
+ int32 size = 1;
+
+ // Desired interval between consecutive responses in the response stream in
+ // microseconds.
+ int32 interval_us = 2;
+
+ // Whether to request the server to compress the response. This field is
+ // "nullable" in order to interoperate seamlessly with clients not able to
+ // implement the full compression tests by introspecting the call to verify
+ // the response's compression status.
+ BoolValue compressed = 3;
+}
+
+// Server-streaming request.
+message StreamingOutputCallRequest {
+ // Desired payload type in the response from the server.
+ // If response_type is RANDOM, the payload from each response in the stream
+ // might be of different types. This is to simulate a mixed type of payload
+ // stream.
+ PayloadType response_type = 1;
+
+ // Configuration for each expected response message.
+ repeated ResponseParameters response_parameters = 2;
+
+ // Optional input payload sent along with the request.
+ Payload payload = 3;
+
+ // Whether server should return a given status
+ EchoStatus response_status = 7;
+}
+
+// Server-streaming response, as configured by the request and parameters.
+message StreamingOutputCallResponse {
+ // Payload to increase response size.
+ Payload payload = 1;
+}
+
+// For reconnect interop test only.
+// Client tells server what reconnection parameters it used.
+message ReconnectParams {
+ int32 max_reconnect_backoff_ms = 1;
+}
+
+// For reconnect interop test only.
+// Server tells client whether its reconnects are following the spec and the
+// reconnect backoffs it saw.
+message ReconnectInfo {
+ bool passed = 1;
+ repeated int32 backoff_ms = 2;
+}
diff --git a/testassets/Proto/test.proto b/testassets/Proto/test.proto
new file mode 100644
index 000000000..86d6ab605
--- /dev/null
+++ b/testassets/Proto/test.proto
@@ -0,0 +1,79 @@
+
+// Copyright 2015-2016 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.
+
+// An integration test service that covers all the method signature permutations
+// of unary/streaming requests/responses.
+
+syntax = "proto3";
+
+import "empty.proto";
+import "messages.proto";
+
+package grpc.testing;
+
+// A simple service to test the various types of RPCs and experiment with
+// performance with various types of payload.
+service TestService {
+ // One empty request followed by one empty response.
+ rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty);
+
+ // One request followed by one response.
+ rpc UnaryCall(SimpleRequest) returns (SimpleResponse);
+
+ // One request followed by one response. Response has cache control
+ // headers set such that a caching HTTP proxy (such as GFE) can
+ // satisfy subsequent requests.
+ rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse);
+
+ // One request followed by a sequence of responses (streamed download).
+ // The server returns the payload with client desired type and sizes.
+ rpc StreamingOutputCall(StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+
+ // A sequence of requests followed by one response (streamed upload).
+ // The server returns the aggregated size of client payload as the result.
+ rpc StreamingInputCall(stream StreamingInputCallRequest)
+ returns (StreamingInputCallResponse);
+
+ // A sequence of requests with each request served by the server immediately.
+ // As one request could lead to multiple responses, this interface
+ // demonstrates the idea of full duplexing.
+ rpc FullDuplexCall(stream StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+
+ // A sequence of requests followed by a sequence of responses.
+ // The server buffers all the client requests and then serves them in order. A
+ // stream of responses are returned to the client when the server starts with
+ // first request.
+ rpc HalfDuplexCall(stream StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+
+ // The test server will not implement this method. It will be used
+ // to test the behavior when clients call unimplemented methods.
+ rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty);
+}
+
+// A simple service NOT implemented at servers so clients can test for
+// that case.
+service UnimplementedService {
+ // A call that no server should implement
+ rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty);
+}
+
+// A service used to control reconnect server.
+service ReconnectService {
+ rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty);
+ rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo);
+}