From 8afb78fb9573dd6bc62bd2bed329b4c68a00c6a8 Mon Sep 17 00:00:00 2001
From: James Newton-King <james@newtonking.com>
Date: Sat, 21 Mar 2020 15:32:45 +1300
Subject: [PATCH] Add gRPC interop tests (#17040)

* WIP add interop tests

* Clean up

* Move test project into build infrastructure

* Clean up

* Remove hardcoded paths

* Clean up

* Fix build

* Add copyright notices

* Update azure template

* Fix build

* Fix build?

* Fix build?

* Add gRPC interop tests to CI

- Convert to using references managed by build infrastructure
- Use produced AspNetCore.App shared framework
- Save server logs
- Dynamically bind to ports
- Ensure InteropWebsite is built in the same configuration as the test project

* Cleanup

* Rebase fix

* Include tests assets in build directory for Helix

* Incorporate changes in ProcessEx

* Include Grpc test in regular build

* Fixup

* Test

* exe doesn't always exist

* Capture logs on helix

* Maybe this will work

* There are two application started messages

* Derp

* Cleanup

* Update directory

* Add interop tests to more pipelines

* mkdir

Co-authored-by: John Luo <johluo@microsoft.com>
---
 .azure/pipelines/ci.yml                       |   6 +-
 .azure/pipelines/quarantined-tests.yml        |   2 +-
 Directory.Build.targets                       |   2 +
 eng/Build.props                               |   7 +
 eng/Dependencies.props                        |   7 +-
 eng/Versions.props                            |   6 +-
 eng/helix/content/runtests.sh                 |   2 +-
 eng/targets/FunctionalTestAsset.targets       |   9 +
 eng/targets/FunctionalTestWithAssets.targets  |  27 +
 src/Grpc/Grpc.sln                             |  46 +
 src/Grpc/THIRD-PARTY-NOTICES                  |  26 +
 src/Grpc/build.cmd                            |   3 +
 src/Grpc/build.sh                             |   7 +
 src/Grpc/startvs.cmd                          |   3 +
 .../InteropTests/Helpers/ClientProcess.cs     |  62 ++
 .../Helpers/InteropTestsFixture.cs            |  44 +
 .../InteropTests/Helpers/WebsiteProcess.cs    |  93 ++
 src/Grpc/test/InteropTests/InteropTests.cs    |  80 ++
 .../test/InteropTests/InteropTests.csproj     |  21 +
 .../test/testassets/InteropClient/Assert.cs   | 131 +++
 .../InteropClient/AsyncStreamExtensions.cs    |  86 ++
 .../InteropClient/IChannelWrapper.cs          |  46 +
 .../testassets/InteropClient/InteropClient.cs | 899 ++++++++++++++++++
 .../InteropClient/InteropClient.csproj        |  24 +
 .../test/testassets/InteropClient/Program.cs  |  32 +
 .../testassets/InteropClient/RunTests.ps1     |  53 ++
 .../InteropWebsite/AsyncStreamExtensions.cs   |  41 +
 .../InteropWebsite/Directory.Build.targets    |  24 +
 .../InteropWebsite/InteropWebsite.csproj      |  19 +
 .../test/testassets/InteropWebsite/Program.cs |  63 ++
 .../test/testassets/InteropWebsite/Startup.cs |  49 +
 .../InteropWebsite/TestServiceImpl.cs         | 149 +++
 .../testassets/Proto/grpc/testing/empty.proto |  28 +
 .../Proto/grpc/testing/messages.proto         | 165 ++++
 .../testassets/Proto/grpc/testing/test.proto  |  79 ++
 src/Grpc/test/testassets/README.md            |   3 +
 .../test/Helpers/AspNetProcess.cs             |   1 +
 .../test/Helpers/ErrorMessages.cs             |   2 +
 src/ProjectTemplates/test/Helpers/Project.cs  |   1 +
 .../test/Helpers/TemplatePackageInstaller.cs  |   1 +
 .../SpaTemplateTest/SpaTemplateTestBase.cs    |   1 +
 .../Helpers => Shared/Process}/ProcessEx.cs   |   6 +-
 42 files changed, 2346 insertions(+), 10 deletions(-)
 create mode 100644 eng/targets/FunctionalTestAsset.targets
 create mode 100644 eng/targets/FunctionalTestWithAssets.targets
 create mode 100644 src/Grpc/Grpc.sln
 create mode 100644 src/Grpc/THIRD-PARTY-NOTICES
 create mode 100644 src/Grpc/build.cmd
 create mode 100644 src/Grpc/build.sh
 create mode 100644 src/Grpc/startvs.cmd
 create mode 100644 src/Grpc/test/InteropTests/Helpers/ClientProcess.cs
 create mode 100644 src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs
 create mode 100644 src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs
 create mode 100644 src/Grpc/test/InteropTests/InteropTests.cs
 create mode 100644 src/Grpc/test/InteropTests/InteropTests.csproj
 create mode 100644 src/Grpc/test/testassets/InteropClient/Assert.cs
 create mode 100644 src/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs
 create mode 100644 src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs
 create mode 100644 src/Grpc/test/testassets/InteropClient/InteropClient.cs
 create mode 100644 src/Grpc/test/testassets/InteropClient/InteropClient.csproj
 create mode 100644 src/Grpc/test/testassets/InteropClient/Program.cs
 create mode 100644 src/Grpc/test/testassets/InteropClient/RunTests.ps1
 create mode 100644 src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs
 create mode 100644 src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets
 create mode 100644 src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj
 create mode 100644 src/Grpc/test/testassets/InteropWebsite/Program.cs
 create mode 100644 src/Grpc/test/testassets/InteropWebsite/Startup.cs
 create mode 100644 src/Grpc/test/testassets/InteropWebsite/TestServiceImpl.cs
 create mode 100644 src/Grpc/test/testassets/Proto/grpc/testing/empty.proto
 create mode 100644 src/Grpc/test/testassets/Proto/grpc/testing/messages.proto
 create mode 100644 src/Grpc/test/testassets/Proto/grpc/testing/test.proto
 create mode 100644 src/Grpc/test/testassets/README.md
 rename src/{ProjectTemplates/test/Helpers => Shared/Process}/ProcessEx.cs (99%)

diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml
index 9b491337a97..6c4e6adbcde 100644
--- a/.azure/pipelines/ci.yml
+++ b/.azure/pipelines/ci.yml
@@ -674,9 +674,9 @@ stages:
       # Build the shared framework
       - script: ./build.cmd -ci -all -pack -arch x64 -buildNative /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log /bl:artifacts/log/helix.build.x64.binlog
         displayName: Build shared fx
-      - script: .\restore.cmd -ci
+      - script: .\restore.cmd -ci /p:BuildInteropProjects=true
         displayName: Restore
-      - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl
+      - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildInteropProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl
         displayName: Run build.cmd helix target
         env:
           HelixApiAccessToken: $(HelixApiAccessToken) # Needed for internal queues
@@ -701,7 +701,7 @@ stages:
       # Build the x86 shared framework
       - script: .\restore.cmd -ci
         displayName: Restore
-      - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsHelixJob=true /p:IsHelixDaily=true /p:BuildAllProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl
+      - script: .\build.cmd -ci -NoRestore -test -projects eng\helix\helix.proj /p:IsHelixJob=true /p:IsHelixDaily=true /p:BuildAllProjects=true /p:BuildInteropProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl
         displayName: Run build.cmd helix target
         env:
           HelixApiAccessToken: $(HelixApiAccessToken) # Needed for internal queues
diff --git a/.azure/pipelines/quarantined-tests.yml b/.azure/pipelines/quarantined-tests.yml
index 415853ed8e1..791283efd6f 100644
--- a/.azure/pipelines/quarantined-tests.yml
+++ b/.azure/pipelines/quarantined-tests.yml
@@ -35,7 +35,7 @@ jobs:
       displayName: Build shared fx
     - script: .\restore.cmd -ci
       displayName: Restore
-    - script: .\build.cmd -ci -NoRestore -test -noBuildJava -projects eng\helix\helix.proj /p:RunQuarantinedTests=true /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl
+    - script: .\build.cmd -ci -NoRestore -test -noBuildJava -projects eng\helix\helix.proj /p:RunQuarantinedTests=true /p:IsRequiredCheck=true /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildInteropProjects=true /p:BuildNative=true /p:RunTemplateTests=true /p:ASPNETCORE_TEST_LOG_DIR=artifacts/log -bl
       displayName: Run build.cmd helix target
       env:
         HelixApiAccessToken: $(HelixApiAccessToken) # Needed for internal queues
diff --git a/Directory.Build.targets b/Directory.Build.targets
index a62e3ed6a48..9a30afbb8e0 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -167,5 +167,7 @@
   <Import Project="eng\targets\Npm.Common.targets"  Condition="'$(MSBuildProjectExtension)' == '.npmproj'" />
   <Import Project="eng\targets\ReferenceAssembly.targets" Condition=" $(HasReferenceAssembly) " />
   <Import Project="eng\targets\Helix.targets" Condition="'$(IsTestProject)' == 'true'" />
+  <Import Project="eng\targets\FunctionalTestAsset.targets" Condition="'$(IsTestAssetProject)' == 'true'" />
+  <Import Project="eng\targets\FunctionalTestWithAssets.targets" Condition="'$(ContainsFunctionalTestAssets)' == 'true'" />
 
 </Project>
diff --git a/eng/Build.props b/eng/Build.props
index 4c0542db15d..6f1b3f19089 100644
--- a/eng/Build.props
+++ b/eng/Build.props
@@ -123,6 +123,13 @@
         <ProjectToBuild Condition=" '$(BuildJava)' == 'true'" Include="@(JavaProjects)" Exclude="@(ProjectToExclude)" />
         <ProjectToExclude Condition=" '$(BuildJava)' != 'true'" Include="@(JavaProjects)" />
 
+        <!-- These interop test projects can only be restored and built after the shared framework is built -->
+        <InteropProjects Include="$(RepoRoot)src\Grpc\**\*.csproj"
+                      Exclude="@(ProjectToExclude)" />
+
+        <ProjectToBuild Condition=" '$(BuildInteropProjects)' == 'true'" Include="@(InteropProjects)" Exclude="@(ProjectToExclude)" />
+        <ProjectToExclude Condition=" '$(BuildInteropProjects)' != 'true'" Include="@(InteropProjects)" />
+
         <!--
           Use caution to avoid deep recursion. If the globbing pattern picks up something which exceeds MAX_PATH,
           the entire pattern will silently fail to evaluate correctly.
diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index 37c0b6a4717..d02e5158ae1 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -154,8 +154,13 @@ and are generated based on the last package release.
   <ItemGroup Label="External dependencies" Condition="'$(DotNetBuildFromSource)' != 'true'">
     <LatestPackageReference Include="AngleSharp" Version="$(AngleSharpPackageVersion)" />
     <LatestPackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" />
+    <LatestPackageReference Include="CommandLineParser" Version="$(CommandLineParserPackageVersion)" />
     <LatestPackageReference Include="FSharp.Core" Version="$(FSharpCorePackageVersion)" />
-    <LatestPackageReference Include="Google.ProtoBuf" Version="$(GoogleProtoBufPackageVersion)" />
+    <LatestPackageReference Include="Google.Protobuf" Version="$(GoogleProtobufPackageVersion)" />
+    <LatestPackageReference Include="Grpc.AspNetCore" Version="$(GrpcAspNetCorePackageVersion)" />
+    <LatestPackageReference Include="Grpc.Auth" Version="$(GrpcAuthPackageVersion)" />
+    <LatestPackageReference Include="Grpc.Net.Client" Version="$(GrpcNetClientPackageVersion)" />
+    <LatestPackageReference Include="Grpc.Tools" Version="$(GrpcToolsPackageVersion)" />
     <LatestPackageReference Include="IdentityServer4" Version="$(IdentityServer4PackageVersion)" />
     <LatestPackageReference Include="IdentityServer4.AspNetIdentity" Version="$(IdentityServer4AspNetIdentityPackageVersion)" />
     <LatestPackageReference Include="IdentityServer4.EntityFramework" Version="$(IdentityServer4EntityFrameworkPackageVersion)" />
diff --git a/eng/Versions.props b/eng/Versions.props
index 63bf24ae901..a4b0b0a4840 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -217,9 +217,13 @@
     <AngleSharpPackageVersion>0.9.9</AngleSharpPackageVersion>
     <BenchmarkDotNetPackageVersion>0.12.0</BenchmarkDotNetPackageVersion>
     <CastleCorePackageVersion>4.2.1</CastleCorePackageVersion>
+    <CommandLineParserPackageVersion>2.3.0</CommandLineParserPackageVersion>
     <FSharpCorePackageVersion>4.2.1</FSharpCorePackageVersion>
-    <GoogleProtobufPackageVersion>3.8.0</GoogleProtobufPackageVersion>
+    <GoogleProtobufPackageVersion>3.10.0</GoogleProtobufPackageVersion>
     <GrpcAspNetCorePackageVersion>2.27.0</GrpcAspNetCorePackageVersion>
+    <GrpcAuthPackageVersion>2.27.0</GrpcAuthPackageVersion>
+    <GrpcNetClientPackageVersion>2.27.0</GrpcNetClientPackageVersion>
+    <GrpcToolsPackageVersion>2.27.0</GrpcToolsPackageVersion>
     <IdentityServer4AspNetIdentityPackageVersion>3.0.0</IdentityServer4AspNetIdentityPackageVersion>
     <IdentityServer4EntityFrameworkPackageVersion>3.0.0</IdentityServer4EntityFrameworkPackageVersion>
     <IdentityServer4PackageVersion>3.0.0</IdentityServer4PackageVersion>
diff --git a/eng/helix/content/runtests.sh b/eng/helix/content/runtests.sh
index 9ff0133e34f..7788f800f9c 100644
--- a/eng/helix/content/runtests.sh
+++ b/eng/helix/content/runtests.sh
@@ -41,7 +41,7 @@ echo "Creating nugetRestore directory: $NUGET_RESTORE"
 mkdir $NUGET_RESTORE
 mkdir logs
 
-ls -la
+ls -laR
 
 RESET="\033[0m"
 RED="\033[0;31m"
diff --git a/eng/targets/FunctionalTestAsset.targets b/eng/targets/FunctionalTestAsset.targets
new file mode 100644
index 00000000000..04b35133412
--- /dev/null
+++ b/eng/targets/FunctionalTestAsset.targets
@@ -0,0 +1,9 @@
+<Project>
+  <Target Name="CollectFunctionalTestPayload" DependsOnTargets="Publish" Returns="@(DependencyPayload)" >
+    <ItemGroup>
+      <DependencyPayload Include="@(ResolvedFileToPublish)">
+        <RelativePath>$(MSBuildProjectName)\%(ResolvedFileToPublish.RelativePath)</RelativePath>
+      </DependencyPayload>
+    </ItemGroup>
+  </Target>
+</Project>
\ No newline at end of file
diff --git a/eng/targets/FunctionalTestWithAssets.targets b/eng/targets/FunctionalTestWithAssets.targets
new file mode 100644
index 00000000000..b6194607cb4
--- /dev/null
+++ b/eng/targets/FunctionalTestWithAssets.targets
@@ -0,0 +1,27 @@
+<Project>
+
+  <ItemGroup>
+    <ProjectReference Include="@(FunctionalTestAssetProjectReference)" ReferenceOutputAssembly="false" />
+  </ItemGroup>
+
+  <Target Name="CollectTestAssetPayload" BeforeTargets="GetCopyToOutputDirectoryItems" >
+    <MSBuild Targets="CollectFunctionalTestPayload"
+             BuildInParallel="true"
+             SkipNonexistentTargets="true"
+             Projects="%(FunctionalTestAssetProjectReference.Identity)"
+             RebaseOutputs="True">
+      <Output TaskParameter="TargetOutputs" ItemName="DependencyPayload" />
+    </MSBuild>
+
+    <ItemGroup>
+      <ContentWithTargetPath
+        Include="@(DependencyPayload)"
+        Condition="'@(DependencyPayload->Count())' != '0'">
+        <TargetPath>%(DependencyPayload.RelativePath)</TargetPath>
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+      </ContentWithTargetPath>
+    </ItemGroup>
+  </Target>
+
+</Project>
\ No newline at end of file
diff --git a/src/Grpc/Grpc.sln b/src/Grpc/Grpc.sln
new file mode 100644
index 00000000000..d1491ed3e35
--- /dev/null
+++ b/src/Grpc/Grpc.sln
@@ -0,0 +1,46 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29505.145
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0FFB3605-0203-450F-80C8-F82CA2E8269F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{F5841B0A-901A-448F-9CC5-4CB393CE86AF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropTests", "test\InteropTests\InteropTests.csproj", "{90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropWebsite", "test\testassets\InteropWebsite\InteropWebsite.csproj", "{3AB7E8E4-BA36-44CE-844E-39DB66E46D45}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InteropClient", "test\testassets\InteropClient\InteropClient.csproj", "{66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{90BF37E6-B3F1-4EFC-A233-8288D8B32DD2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3AB7E8E4-BA36-44CE-844E-39DB66E46D45}.Release|Any CPU.Build.0 = Release|Any CPU
+		{66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{90BF37E6-B3F1-4EFC-A233-8288D8B32DD2} = {0FFB3605-0203-450F-80C8-F82CA2E8269F}
+		{3AB7E8E4-BA36-44CE-844E-39DB66E46D45} = {F5841B0A-901A-448F-9CC5-4CB393CE86AF}
+		{66E6C55E-E4E3-4F4B-834A-BB34BFE00D2F} = {F5841B0A-901A-448F-9CC5-4CB393CE86AF}
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {3CAE66FD-9A59-49C2-B133-1D599225259A}
+	EndGlobalSection
+EndGlobal
diff --git a/src/Grpc/THIRD-PARTY-NOTICES b/src/Grpc/THIRD-PARTY-NOTICES
new file mode 100644
index 00000000000..6b4a863467d
--- /dev/null
+++ b/src/Grpc/THIRD-PARTY-NOTICES
@@ -0,0 +1,26 @@
+.NET Core uses third-party libraries or other resources that may be
+distributed under licenses different than the .NET Core software.
+
+In the event that we accidentally failed to list a required notice, please
+bring it to our attention. Post an issue or email us:
+
+           dotnet@microsoft.com
+
+The attached notices are provided for information only.
+
+License notice for gRPC interop tests
+-------------------------------------
+
+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.
diff --git a/src/Grpc/build.cmd b/src/Grpc/build.cmd
new file mode 100644
index 00000000000..2406296662e
--- /dev/null
+++ b/src/Grpc/build.cmd
@@ -0,0 +1,3 @@
+@ECHO OFF
+SET RepoRoot=%~dp0..\..
+%RepoRoot%\build.cmd -projects %~dp0**\*.*proj %*
diff --git a/src/Grpc/build.sh b/src/Grpc/build.sh
new file mode 100644
index 00000000000..7046bb98a0f
--- /dev/null
+++ b/src/Grpc/build.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+repo_root="$DIR/../.."
+"$repo_root/build.sh" --projects "$DIR/**/*.*proj" "$@"
diff --git a/src/Grpc/startvs.cmd b/src/Grpc/startvs.cmd
new file mode 100644
index 00000000000..a4a22788e6f
--- /dev/null
+++ b/src/Grpc/startvs.cmd
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+%~dp0..\..\startvs.cmd %~dp0Grpc.sln
diff --git a/src/Grpc/test/InteropTests/Helpers/ClientProcess.cs b/src/Grpc/test/InteropTests/Helpers/ClientProcess.cs
new file mode 100644
index 00000000000..449342b28de
--- /dev/null
+++ b/src/Grpc/test/InteropTests/Helpers/ClientProcess.cs
@@ -0,0 +1,62 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Xunit.Abstractions;
+
+namespace InteropTests.Helpers
+{
+    public class ClientProcess : IDisposable
+    {
+        private readonly Process _process;
+        private readonly ProcessEx _processEx;
+        private readonly TaskCompletionSource<object> _startTcs;
+
+        public ClientProcess(ITestOutputHelper output, string path, string serverPort, string testCase)
+        {
+            _process = new Process();
+            _process.StartInfo = new ProcessStartInfo
+            {
+                RedirectStandardOutput = true,
+                RedirectStandardError = true,
+                FileName = "dotnet",
+                Arguments = @$"{path} --use_tls false --server_port {serverPort} --client_type httpclient --test_case {testCase}"
+            };
+            _process.EnableRaisingEvents = true;
+            _process.OutputDataReceived += Process_OutputDataReceived;
+            _process.Start();
+
+            _processEx = new ProcessEx(output, _process);
+
+            _startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
+        }
+
+        public Task WaitForReady()
+        {
+            return _startTcs.Task;
+        }
+
+        public int ExitCode => _process.ExitCode;
+        public Task Exited => _processEx.Exited;
+
+        private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
+        {
+            var data = e.Data;
+            if (data != null)
+            {
+                if (data.Contains("Application started."))
+                {
+                    _startTcs.TrySetResult(null);
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+            _processEx.Dispose();
+        }
+    }
+}
diff --git a/src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs b/src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs
new file mode 100644
index 00000000000..86ffff4a079
--- /dev/null
+++ b/src/Grpc/test/InteropTests/Helpers/InteropTestsFixture.cs
@@ -0,0 +1,44 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace InteropTests.Helpers
+{
+    public class InteropTestsFixture : IDisposable
+    {
+        private WebsiteProcess _process;
+
+        public string Path { get; set; }
+
+
+        public string ServerPort { get; private set; }
+
+
+        public async Task EnsureStarted(ITestOutputHelper output)
+        {
+            if (_process != null)
+            {
+                return;
+            }
+
+            if (string.IsNullOrEmpty(Path))
+            {
+                throw new InvalidOperationException("Path has not been set.");
+            }
+
+            _process = new WebsiteProcess(Path, output);
+
+            await _process.WaitForReady();
+
+            ServerPort = _process.ServerPort;
+        }
+
+        public void Dispose()
+        {
+            _process.Dispose();
+        }
+    }
+}
diff --git a/src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs b/src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs
new file mode 100644
index 00000000000..77ac62242df
--- /dev/null
+++ b/src/Grpc/test/InteropTests/Helpers/WebsiteProcess.cs
@@ -0,0 +1,93 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Xunit.Abstractions;
+
+namespace InteropTests.Helpers
+{
+    public class WebsiteProcess : IDisposable
+    {
+        private readonly Process _process;
+        private readonly ProcessEx _processEx;
+        private readonly TaskCompletionSource<object> _startTcs;
+        private readonly StringBuilder _consoleOut = new StringBuilder();
+        private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: .*:(?<port>\d*)$");
+
+        public string ServerPort { get; private set; }
+
+        public WebsiteProcess(string path, ITestOutputHelper output)
+        {
+            _process = new Process();
+            _process.StartInfo = new ProcessStartInfo
+            {
+                RedirectStandardOutput = true,
+                RedirectStandardError = true,
+                FileName = "dotnet",
+                Arguments = path
+            };
+            _process.EnableRaisingEvents = true;
+            _process.OutputDataReceived += Process_OutputDataReceived;
+            _process.Start();
+
+            _processEx = new ProcessEx(output, _process);
+
+            _startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
+        }
+
+        public Task WaitForReady()
+        {
+            if (_processEx.HasExited)
+            {
+                return Task.FromException(new InvalidOperationException("Server is not running."));
+            }
+
+            return _startTcs.Task;
+        }
+
+        private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
+        {
+            var data = e.Data;
+            if (data != null)
+            {
+                _consoleOut.AppendLine(data);
+                var m = NowListeningRegex.Match(data);
+                if (m.Success)
+                {
+                    ServerPort = m.Groups["port"].Value;
+                }
+
+                if (data.Contains("Application started. Press Ctrl+C to shut down."))
+                {
+                    _startTcs.TrySetResult(null);
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+            var attributes = Assembly.GetExecutingAssembly()
+                .GetCustomAttributes<AssemblyMetadataAttribute>();
+            var serverLogPath = attributes.SingleOrDefault(a => a.Key == "ServerLogPath")?.Value;
+            if (!string.IsNullOrEmpty(serverLogPath))
+            {
+                File.WriteAllText(serverLogPath, _consoleOut.ToString());
+            }
+            else
+            {
+                var logDir = Path.Combine(Directory.GetCurrentDirectory(), "artifacts", "logs");
+                Directory.CreateDirectory(logDir);
+                File.WriteAllText(Path.Combine(logDir, "InteropServer.log"), _consoleOut.ToString());
+            }
+            _processEx.Dispose();
+        }
+    }
+}
diff --git a/src/Grpc/test/InteropTests/InteropTests.cs b/src/Grpc/test/InteropTests/InteropTests.cs
new file mode 100644
index 00000000000..b12ec316dc6
--- /dev/null
+++ b/src/Grpc/test/InteropTests/InteropTests.cs
@@ -0,0 +1,80 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using InteropTests.Helpers;
+using Microsoft.AspNetCore.Testing;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace InteropTests
+{
+    public class InteropTests : IClassFixture<InteropTestsFixture>
+    {
+        private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
+
+        private readonly string _clientPath = Path.Combine(Directory.GetCurrentDirectory(), "InteropClient", "InteropClient.dll");
+        private readonly InteropTestsFixture _fixture;
+        private readonly ITestOutputHelper _output;
+
+        public InteropTests(InteropTestsFixture fixture, ITestOutputHelper output)
+        {
+            var attributes = Assembly.GetExecutingAssembly()
+                .GetCustomAttributes<AssemblyMetadataAttribute>();
+
+            fixture.Path = Path.Combine(Directory.GetCurrentDirectory(), "InteropWebsite", "InteropWebsite.dll");
+
+            _fixture = fixture;
+            _output = output;
+        }
+
+        [Theory]
+        [MemberData(nameof(TestCaseData))]
+        public async Task InteropTestCase(string name)
+        {
+            await _fixture.EnsureStarted(_output).TimeoutAfter(DefaultTimeout);
+
+            using (var clientProcess = new ClientProcess(_output, _clientPath, _fixture.ServerPort, name))
+            {
+                await clientProcess.WaitForReady().TimeoutAfter(DefaultTimeout);
+
+                await clientProcess.Exited.TimeoutAfter(DefaultTimeout);
+
+                Assert.Equal(0, clientProcess.ExitCode);
+            }
+        }
+
+        #region TestData
+        // All interop test cases, minus GCE authentication specific tests
+        private static string[] AllTests = new string[]
+        {
+            "empty_unary",
+            "large_unary",
+            "client_streaming",
+            "server_streaming",
+            "ping_pong",
+            "empty_stream",
+
+            "cancel_after_begin",
+            "cancel_after_first_response",
+            "timeout_on_sleeping_server",
+            "custom_metadata",
+            "status_code_and_message",
+            "special_status_message",
+            "unimplemented_service",
+            "unimplemented_method",
+            "client_compressed_unary",
+            "client_compressed_streaming",
+            "server_compressed_unary",
+            "server_compressed_streaming"
+        };
+
+        public static IEnumerable<object[]> TestCaseData => AllTests.Select(t => new object[] { t });
+        #endregion
+    }
+}
diff --git a/src/Grpc/test/InteropTests/InteropTests.csproj b/src/Grpc/test/InteropTests/InteropTests.csproj
new file mode 100644
index 00000000000..bcbd1fc4b6a
--- /dev/null
+++ b/src/Grpc/test/InteropTests/InteropTests.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <ContainsFunctionalTestAssets>true</ContainsFunctionalTestAssets>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="..\..\..\Shared\Process\ProcessEx.cs" Link="Helpers\ProcessEx.cs" />
+    <Compile Include="..\..\..\Shared\Process\ProcessExtensions.cs" Link="Helpers\ProcessExtensions.cs" />
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(IsHelixJob)' != 'true'">
+      <_Parameter1>ServerLogPath</_Parameter1>
+      <_Parameter2>$(ArtifactsLogDir)InteropServer.log</_Parameter2>
+    </AssemblyAttribute>
+
+    <FunctionalTestAssetProjectReference Include="..\testassets\InteropClient\InteropClient.csproj" />
+    <FunctionalTestAssetProjectReference Include="..\testassets\InteropWebsite\InteropWebsite.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Grpc/test/testassets/InteropClient/Assert.cs b/src/Grpc/test/testassets/InteropClient/Assert.cs
new file mode 100644
index 00000000000..8ae666f4116
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropClient/Assert.cs
@@ -0,0 +1,131 @@
+#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;
+using System.Threading.Tasks;
+
+namespace InteropTestsClient
+{
+    internal static class Assert
+    {
+        public static void IsTrue(bool condition)
+        {
+            if (!condition)
+            {
+                throw new InvalidOperationException("Expected true but got false.");
+            }
+        }
+
+        public static void IsFalse(bool condition)
+        {
+            if (condition)
+            {
+                throw new InvalidOperationException("Expected false but got true.");
+            }
+        }
+
+        public static void AreEqual(object expected, object actual)
+        {
+            if (!Equals(expected, actual))
+            {
+                throw new InvalidOperationException($"Expected {expected} but got {actual}.");
+            }
+        }
+
+        public static void IsNotNull(object value)
+        {
+            if (value == null)
+            {
+                throw new InvalidOperationException("Expected not null but got null.");
+            }
+        }
+
+        public static void Fail()
+        {
+            throw new InvalidOperationException("Failure assert.");
+        }
+
+        public static async Task<TException> ThrowsAsync<TException>(Func<Task> action) where TException : Exception
+        {
+            try
+            {
+                await action();
+            }
+            catch (Exception ex)
+            {
+                if (ex.GetType() == typeof(TException))
+                {
+                    return (TException)ex;
+                }
+
+                throw new InvalidOperationException($"Expected ${typeof(TException)} but got ${ex.GetType()}.");
+            }
+
+            throw new InvalidOperationException("No exception thrown.");
+        }
+
+        public static TException Throws<TException>(Action action) where TException : Exception
+        {
+            try
+            {
+                action();
+            }
+            catch (Exception ex)
+            {
+                if (ex.GetType() == typeof(TException))
+                {
+                    return (TException)ex;
+                }
+
+                throw new InvalidOperationException($"Expected ${typeof(TException)} but got ${ex.GetType()}.");
+            }
+
+            throw new InvalidOperationException("No exception thrown.");
+        }
+
+        public static void Contains(object expected, ICollection actual)
+        {
+            foreach (var item in actual)
+            {
+                if (Equals(item, expected))
+                {
+                    return;
+                }
+            }
+
+            throw new InvalidOperationException($"Could not find {expected} in the collection.");
+        }
+    }
+
+    internal static class CollectionAssert
+    {
+        public static void AreEqual(IList expected, IList actual)
+        {
+            if (expected.Count != actual.Count)
+            {
+                throw new InvalidOperationException($"Collection lengths differ. {expected.Count} but got {actual.Count}.");
+            }
+
+            for (var i = 0; i < expected.Count; i++)
+            {
+                Assert.AreEqual(expected[i]!, actual[i]!);
+            }
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs b/src/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs
new file mode 100644
index 00000000000..00c1876fd13
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropClient/AsyncStreamExtensions.cs
@@ -0,0 +1,86 @@
+#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;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Grpc.Core;
+
+namespace InteropTestsClient
+{
+    /// <summary>
+    /// Extension methods that simplify work with gRPC streaming calls.
+    /// </summary>
+    public static class AsyncStreamExtensions
+    {
+        /// <summary>
+        /// Reads the entire stream and executes an async action for each element.
+        /// </summary>
+        public static async Task ForEachAsync<T>(this IAsyncStreamReader<T> streamReader, Func<T, Task> asyncAction)
+            where T : class
+        {
+            while (await streamReader.MoveNext().ConfigureAwait(false))
+            {
+                await asyncAction(streamReader.Current).ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Reads the entire stream and creates a list containing all the elements read.
+        /// </summary>
+        public static async Task<List<T>> ToListAsync<T>(this IAsyncStreamReader<T> streamReader)
+            where T : class
+        {
+            var result = new List<T>();
+            while (await streamReader.MoveNext().ConfigureAwait(false))
+            {
+                result.Add(streamReader.Current);
+            }
+            return result;
+        }
+
+        /// <summary>
+        /// Writes all elements from given enumerable to the stream.
+        /// Completes the stream afterwards unless close = false.
+        /// </summary>
+        public static async Task WriteAllAsync<T>(this IClientStreamWriter<T> streamWriter, IEnumerable<T> elements, bool complete = true)
+            where T : class
+        {
+            foreach (var element in elements)
+            {
+                await streamWriter.WriteAsync(element).ConfigureAwait(false);
+            }
+            if (complete)
+            {
+                await streamWriter.CompleteAsync().ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Writes all elements from given enumerable to the stream.
+        /// </summary>
+        public static async Task WriteAllAsync<T>(this IServerStreamWriter<T> streamWriter, IEnumerable<T> elements)
+            where T : class
+        {
+            foreach (var element in elements)
+            {
+                await streamWriter.WriteAsync(element).ConfigureAwait(false);
+            }
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs b/src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs
new file mode 100644
index 00000000000..4cbc82532b5
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropClient/IChannelWrapper.cs
@@ -0,0 +1,46 @@
+#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.Tasks;
+using Grpc.Core;
+using Grpc.Net.Client;
+
+namespace InteropTestsClient
+{
+    public interface IChannelWrapper
+    {
+        ChannelBase Channel { get; }
+        Task ShutdownAsync();
+    }
+
+    public class GrpcChannelWrapper : IChannelWrapper
+    {
+        public ChannelBase Channel { get; }
+
+        public GrpcChannelWrapper(GrpcChannel channel)
+        {
+            Channel = channel;
+        }
+
+        public Task ShutdownAsync()
+        {
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropClient/InteropClient.cs b/src/Grpc/test/testassets/InteropClient/InteropClient.cs
new file mode 100644
index 00000000000..27d51f69985
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropClient/InteropClient.cs
@@ -0,0 +1,899 @@
+#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.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+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.Utils;
+using Grpc.Net.Client;
+using Grpc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace InteropTestsClient
+{
+    public class InteropClient : IDisposable
+    {
+        internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request";
+
+        private class ClientOptions
+        {
+            [Option("client_type", Default = "httpclient")]
+            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; }
+        }
+
+        private ServiceProvider serviceProvider;
+        private ILoggerFactory loggerFactory;
+        private ClientOptions options;
+
+        private InteropClient(ClientOptions options)
+        {
+            this.options = options;
+
+            var services = new ServiceCollection();
+            services.AddLogging(configure =>
+            {
+                configure.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
+                configure.AddConsole(loggerOptions => loggerOptions.IncludeScopes = true);
+            });
+
+            serviceProvider = services.BuildServiceProvider();
+
+            loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
+        }
+
+        public void Dispose()
+        {
+            serviceProvider.Dispose();
+        }
+
+        public static void Run(string[] args)
+        {
+            var parserResult = Parser.Default.ParseArguments<ClientOptions>(args)
+                .WithNotParsed(errors => Environment.Exit(1))
+                .WithParsed(options =>
+                {
+                    Console.WriteLine("Use TLS: " + options.UseTls);
+                    Console.WriteLine("Use Test CA: " + options.UseTestCa);
+                    Console.WriteLine("Client type: " + options.ClientType);
+                    Console.WriteLine("Server host: " + options.ServerHost);
+                    Console.WriteLine("Server port: " + options.ServerPort);
+
+                    using (var interopClient = new InteropClient(options))
+                    {
+                        interopClient.Run().GetAwaiter().GetResult();
+                    }
+                });
+        }
+
+        private async Task Run()
+        {
+            var channel = await HttpClientCreateChannel();
+            await RunTestCaseAsync(channel, options);
+            await channel.ShutdownAsync();
+        }
+
+        private async Task<IChannelWrapper> HttpClientCreateChannel()
+        {
+            var credentials = await CreateCredentialsAsync(useTestCaOverride: false);
+
+            string scheme;
+            if (!(options.UseTls ?? false))
+            {
+                AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
+                scheme = "http";
+            }
+            else
+            {
+                scheme = "https";
+            }
+
+            var httpClientHandler = new HttpClientHandler();
+            httpClientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
+
+            if (options.UseTestCa ?? false)
+            {
+                var pem = File.ReadAllText("Certs/ca.pem");
+                var certData = GetBytesFromPem(pem, "CERTIFICATE");
+                var cert = new X509Certificate2(certData!);
+
+                httpClientHandler.ClientCertificates.Add(cert);
+            }
+
+            var httpClient = new HttpClient(httpClientHandler);
+
+            var channel = GrpcChannel.ForAddress($"{scheme}://{options.ServerHost}:{options.ServerPort}", new GrpcChannelOptions
+            {
+                Credentials = credentials,
+                HttpClient = httpClient,
+                LoggerFactory = loggerFactory
+            });
+
+            return new GrpcChannelWrapper(channel);
+        }
+
+        private bool IsHttpClient() => string.Equals(options.ClientType, "httpclient", StringComparison.OrdinalIgnoreCase);
+
+        private async Task<ChannelCredentials> CreateCredentialsAsync(bool? useTestCaOverride = null)
+        {
+            var credentials = ChannelCredentials.Insecure;
+            if (options.UseTls.GetValueOrDefault())
+            {
+                credentials = 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<TClient>(IChannelWrapper channel) where TClient : ClientBase
+        {
+            return (TClient)Activator.CreateInstance(typeof(TClient), channel.Channel)!;
+        }
+
+        private async Task RunTestCaseAsync(IChannelWrapper channel, ClientOptions options)
+        {
+            var client = CreateClient<TestService.TestServiceClient>(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<UnimplementedService.UnimplementedServiceClient>(channel));
+                    break;
+                case "special_status_message":
+                    await RunSpecialStatusMessageAsync(client);
+                    break;
+                case "unimplemented_method":
+                    RunUnimplementedMethod(client);
+                    break;
+                case "client_compressed_unary":
+                    RunClientCompressedUnary(client);
+                    break;
+                case "client_compressed_streaming":
+                    await RunClientCompressedStreamingAsync(client);
+                    break;
+                case "server_compressed_unary":
+                    await RunServerCompressedUnary(client);
+                    break;
+                case "server_compressed_streaming":
+                    await RunServerCompressedStreamingAsync(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<int> { 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<int> { 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).ToList());
+            }
+            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.IsFalse(string.IsNullOrEmpty(response.OauthScope));
+            Assert.IsTrue(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.IsFalse(string.IsNullOrEmpty(response.OauthScope));
+            Assert.IsTrue(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 = await Assert.ThrowsAsync<RpcException>(() => 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)
+                {
+                    Assert.AreEqual(StatusCode.DeadlineExceeded, ex.StatusCode);
+                }
+            }
+            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<RpcException>(() => 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!");
+        }
+
+        private static async Task RunSpecialStatusMessageAsync(TestService.TestServiceClient client)
+        {
+            Console.WriteLine("running special_status_message");
+
+            var echoStatus = new EchoStatus
+            {
+                Code = 2,
+                Message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n"
+            };
+
+            try
+            {
+                await client.UnaryCallAsync(new SimpleRequest
+                {
+                    ResponseStatus = echoStatus
+                });
+                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<RpcException>(() => 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<RpcException>(() => 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<RpcException>(() => 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!");
+        }
+
+        public static async Task RunServerCompressedUnary(TestService.TestServiceClient client)
+        {
+            Console.WriteLine("running server_compressed_unary");
+
+            var request = new SimpleRequest
+            {
+                ResponseSize = 314159,
+                Payload = CreateZerosPayload(271828),
+                ResponseCompressed = new BoolValue { Value = true }
+            };
+            var response = await client.UnaryCallAsync(request);
+
+            // Compression of response message is not verified because there is no API available
+            Assert.AreEqual(314159, response.Payload.Body.Length);
+
+            request = new SimpleRequest
+            {
+                ResponseSize = 314159,
+                Payload = CreateZerosPayload(271828),
+                ResponseCompressed = new BoolValue { Value = false }
+            };
+            response = await client.UnaryCallAsync(request);
+
+            // Compression of response message is not verified because there is no API available
+            Assert.AreEqual(314159, response.Payload.Body.Length);
+
+            Console.WriteLine("Passed!");
+        }
+
+        public static async Task RunServerCompressedStreamingAsync(TestService.TestServiceClient client)
+        {
+            Console.WriteLine("running server_compressed_streaming");
+
+            var bodySizes = new List<int> { 31415, 92653 };
+
+            var request = new StreamingOutputCallRequest
+            {
+                ResponseParameters = { bodySizes.Select((size) => new ResponseParameters { Size = size, Compressed = new BoolValue { Value = true } }) }
+            };
+
+            using (var call = client.StreamingOutputCall(request))
+            {
+                // Compression of response message is not verified because there is no API available
+                var responseList = await call.ResponseStream.ToListAsync();
+                CollectionAssert.AreEqual(bodySizes, responseList.Select((item) => item.Payload.Body.Length).ToList());
+            }
+
+            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<string>();
+            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}}
+            };
+        }
+
+        // TODO(JamesNK): PEM loading logic from https://stackoverflow.com/a/10498045/11829
+        // .NET does not have a built-in API for loading pem files
+        // Consider providing ca file in a different format and removing method
+        private byte[]? GetBytesFromPem(string pemString, string section)
+        {
+            var header = string.Format("-----BEGIN {0}-----", section);
+            var footer = string.Format("-----END {0}-----", section);
+
+            var start = pemString.IndexOf(header, StringComparison.Ordinal);
+            if (start == -1)
+            {
+                return null;
+            }
+
+            start += header.Length;
+            var end = pemString.IndexOf(footer, start, StringComparison.Ordinal) - start;
+
+            if (end == -1)
+            {
+                return null;
+            }
+
+            return Convert.FromBase64String(pemString.Substring(start, end));
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropClient/InteropClient.csproj b/src/Grpc/test/testassets/InteropClient/InteropClient.csproj
new file mode 100644
index 00000000000..274295c8386
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropClient/InteropClient.csproj
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <Nullable>enable</Nullable>
+    <LangVersion>8.0</LangVersion>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Protobuf Include="..\Proto\grpc\testing\test.proto" GrpcServices="Client" Link="Protos\test.proto" />
+    <Protobuf Include="..\Proto\grpc\testing\empty.proto" GrpcServices="None" Link="Protos\empty.proto" />
+    <Protobuf Include="..\Proto\grpc\testing\messages.proto" GrpcServices="None" Link="Protos\messages.proto" />
+
+    <Reference Include="CommandLineParser" />
+    <Reference Include="Google.Protobuf" />
+    <Reference Include="Grpc.Auth" />
+    <Reference Include="Grpc.Net.Client" />
+    <Reference Include="Grpc.Tools" PrivateAssets="All" />
+    <Reference Include="Microsoft.Extensions.DependencyInjection" />
+    <Reference Include="Microsoft.Extensions.Logging.Console" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Grpc/test/testassets/InteropClient/Program.cs b/src/Grpc/test/testassets/InteropClient/Program.cs
new file mode 100644
index 00000000000..3fc3d9d9757
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropClient/Program.cs
@@ -0,0 +1,32 @@
+#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 InteropTestsClient
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            Console.WriteLine("Application started.");
+
+            InteropClient.Run(args);
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropClient/RunTests.ps1 b/src/Grpc/test/testassets/InteropClient/RunTests.ps1
new file mode 100644
index 00000000000..faa46d98c1b
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropClient/RunTests.ps1
@@ -0,0 +1,53 @@
+Param
+(
+    [bool]$use_tls = $false
+)
+
+$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",
+  "special_status_message",
+  "unimplemented_service",
+  "unimplemented_method",
+  "client_compressed_unary",
+  "client_compressed_streaming",
+  "server_compressed_unary",
+  "server_compressed_streaming"
+
+Write-Host "Running $($allTests.Count) tests" -ForegroundColor Cyan
+Write-Host "Use TLS: $use_tls" -ForegroundColor Cyan
+Write-Host
+
+foreach ($test in $allTests)
+{
+  Write-Host "Running $test" -ForegroundColor Cyan
+
+  if (!$use_tls)
+  {
+    dotnet run --use_tls false --server_port 50052 --client_type httpclient --test_case $test
+  }
+  else
+  {
+    # Certificate is for test.google.com host. To run locally, setup the host file to point test.google.com to 127.0.0.1
+    dotnet run --use_tls true --server_port 50052 --client_type httpclient --test_case $test --server_host test.google.com
+  }
+
+  Write-Host
+}
+
+Write-Host "Done" -ForegroundColor Cyan
\ No newline at end of file
diff --git a/src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs b/src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs
new file mode 100644
index 00000000000..1f296cb7ba3
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropWebsite/AsyncStreamExtensions.cs
@@ -0,0 +1,41 @@
+#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;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Grpc.Core;
+
+namespace InteropTestsWebsite
+{
+    // Implementation copied from https://github.com/grpc/grpc/blob/master/src/csharp/Grpc.Core/Utils/AsyncStreamExtensions.cs
+    internal static class AsyncStreamExtensions
+    {
+        /// <summary>
+        /// Reads the entire stream and executes an async action for each element.
+        /// </summary>
+        public static async Task ForEachAsync<T>(this IAsyncStreamReader<T> streamReader, Func<T, Task> asyncAction)
+            where T : class
+        {
+            while (await streamReader.MoveNext().ConfigureAwait(false))
+            {
+                await asyncAction(streamReader.Current).ConfigureAwait(false);
+            }
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets b/src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets
new file mode 100644
index 00000000000..2f889b40eae
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropWebsite/Directory.Build.targets
@@ -0,0 +1,24 @@
+<Project>
+  <!-- Skip the parent folder to prevent getting test package references. -->
+  <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\..\, Directory.Build.targets))\Directory.Build.targets" />
+
+  <PropertyGroup>
+    <RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);$(ArtifactsShippingPackagesDir)</RestoreAdditionalProjectSources>
+    <MicrosoftAspNetCoreAppRefPackageVersion Condition="'$(IsTargetingPackBuilding)' != 'false'">$(TargetingPackVersion)</MicrosoftAspNetCoreAppRefPackageVersion>
+    <MicrosoftAspNetCoreAppRefPackageVersion Condition="'$(IsTargetingPackBuilding)' == 'false'">$(AspNetCoreBaselineVersion)</MicrosoftAspNetCoreAppRefPackageVersion>
+  </PropertyGroup>
+
+  <!-- Use the shared framework that was produced -->
+  <ItemGroup>
+    <KnownFrameworkReference
+      Include="Microsoft.AspNetCore.App"
+      TargetFramework="$(DefaultNetCoreTargetFramework)"
+      RuntimeFrameworkName="Microsoft.AspNetCore.App"
+      TargetingPackName="Microsoft.AspNetCore.App.Ref"
+      RuntimePackNamePatterns="Microsoft.AspNetCore.App.Runtime.**RID**"
+      DefaultRuntimeFrameworkVersion="$(SharedFxVersion)"
+      LatestRuntimeFrameworkVersion="$(SharedFxVersion)"
+      TargetingPackVersion="$(MicrosoftAspNetCoreAppRefPackageVersion)"
+      RuntimePackRuntimeIdentifiers="$(SupportedRuntimeIdentifiers)" />
+  </ItemGroup>
+</Project>
diff --git a/src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj b/src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj
new file mode 100644
index 00000000000..4b8d6d38509
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropWebsite/InteropWebsite.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
+    <GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
+    <Nullable>enable</Nullable>
+    <LangVersion>8.0</LangVersion>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Protobuf Include="..\Proto\grpc\testing\test.proto" GrpcServices="Server" Link="Protos\test.proto" />
+    <Protobuf Include="..\Proto\grpc\testing\empty.proto" GrpcServices="None" Link="Protos\empty.proto" />
+    <Protobuf Include="..\Proto\grpc\testing\messages.proto" GrpcServices="None" Link="Protos\messages.proto" />
+
+    <Reference Include="Grpc.AspNetCore" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Grpc/test/testassets/InteropWebsite/Program.cs b/src/Grpc/test/testassets/InteropWebsite/Program.cs
new file mode 100644
index 00000000000..fffaf75c559
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropWebsite/Program.cs
@@ -0,0 +1,63 @@
+#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 Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+
+namespace InteropTestsWebsite
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            CreateHostBuilder(args).Build().Run();
+        }
+
+        public static IHostBuilder CreateHostBuilder(string[] args) =>
+            Host.CreateDefaultBuilder(args)
+                .ConfigureWebHostDefaults(webBuilder =>
+                {
+                    webBuilder.ConfigureKestrel((context, options) =>
+                    {
+                        // Support --port and --use_tls cmdline arguments normally supported
+                        // by gRPC interop servers.
+                        var useTls = context.Configuration.GetValue("use_tls", false);
+
+                        options.Limits.MinRequestBodyDataRate = null;
+                        options.ListenAnyIP(0, listenOptions =>
+                        {
+                            Console.WriteLine($"Enabling connection encryption: {useTls}");
+
+                            if (useTls)
+                            {
+                                var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);
+                                var certPath = Path.Combine(basePath!, "Certs", "server1.pfx");
+
+                                listenOptions.UseHttps(certPath, "1111");
+                            }
+                            listenOptions.Protocols = HttpProtocols.Http2;
+                        });
+                    });
+                    webBuilder.UseStartup<Startup>();
+                });
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropWebsite/Startup.cs b/src/Grpc/test/testassets/InteropWebsite/Startup.cs
new file mode 100644
index 00000000000..4715d1fbbca
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropWebsite/Startup.cs
@@ -0,0 +1,49 @@
+#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 Grpc.Testing;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace InteropTestsWebsite
+{
+    public class Startup
+    {
+        // This method gets called by the runtime. Use this method to add services to the container.
+        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+        public void ConfigureServices(IServiceCollection services)
+        {
+            services.AddGrpc();
+        }
+
+        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+        public void Configure(IApplicationBuilder app, IHostApplicationLifetime applicationLifetime)
+        {
+            // Required to notify test infrastructure that it can begin tests
+            applicationLifetime.ApplicationStarted.Register(() => Console.WriteLine("Application started."));
+
+            app.UseRouting();
+            app.UseEndpoints(endpoints =>
+            {
+                endpoints.MapGrpcService<TestServiceImpl>();
+            });
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/InteropWebsite/TestServiceImpl.cs b/src/Grpc/test/testassets/InteropWebsite/TestServiceImpl.cs
new file mode 100644
index 00000000000..918500cd256
--- /dev/null
+++ b/src/Grpc/test/testassets/InteropWebsite/TestServiceImpl.cs
@@ -0,0 +1,149 @@
+#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.Linq;
+using System.Threading.Tasks;
+using Google.Protobuf;
+using Grpc.Core;
+using InteropTestsWebsite;
+
+namespace Grpc.Testing
+{
+    // Implementation copied from https://github.com/grpc/grpc/blob/master/src/csharp/Grpc.IntegrationTesting/TestServiceImpl.cs
+    public class TestServiceImpl : TestService.TestServiceBase
+    {
+        public override Task<Empty> EmptyCall(Empty request, ServerCallContext context)
+        {
+            return Task.FromResult(new Empty());
+        }
+
+        public override async Task<SimpleResponse> UnaryCall(SimpleRequest request, ServerCallContext context)
+        {
+            await EnsureEchoMetadataAsync(context, request.ResponseCompressed?.Value ?? false);
+            EnsureEchoStatus(request.ResponseStatus, context);
+            EnsureCompression(request.ExpectCompressed, context);
+
+            var response = new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) };
+            return response;
+        }
+
+        public override async Task StreamingOutputCall(StreamingOutputCallRequest request, IServerStreamWriter<StreamingOutputCallResponse> responseStream, ServerCallContext context)
+        {
+            await EnsureEchoMetadataAsync(context, request.ResponseParameters.Any(rp => rp.Compressed?.Value ?? false));
+            EnsureEchoStatus(request.ResponseStatus, context);
+
+            foreach (var responseParam in request.ResponseParameters)
+            {
+                responseStream.WriteOptions = !(responseParam.Compressed?.Value ?? false)
+                    ? new WriteOptions(WriteFlags.NoCompress)
+                    : null;
+
+                var response = new StreamingOutputCallResponse { Payload = CreateZerosPayload(responseParam.Size) };
+                await responseStream.WriteAsync(response);
+            }
+        }
+
+        public override async Task<StreamingInputCallResponse> StreamingInputCall(IAsyncStreamReader<StreamingInputCallRequest> requestStream, ServerCallContext context)
+        {
+            await EnsureEchoMetadataAsync(context);
+
+            int sum = 0;
+            await requestStream.ForEachAsync(request =>
+            {
+                EnsureCompression(request.ExpectCompressed, context);
+
+                sum += request.Payload.Body.Length;
+                return Task.CompletedTask;
+            });
+            return new StreamingInputCallResponse { AggregatedPayloadSize = sum };
+        }
+
+        public override async Task FullDuplexCall(IAsyncStreamReader<StreamingOutputCallRequest> requestStream, IServerStreamWriter<StreamingOutputCallResponse> responseStream, ServerCallContext context)
+        {
+            await EnsureEchoMetadataAsync(context);
+
+            await requestStream.ForEachAsync(async request =>
+            {
+                EnsureEchoStatus(request.ResponseStatus, context);
+                foreach (var responseParam in request.ResponseParameters)
+                {
+                    var response = new StreamingOutputCallResponse { Payload = CreateZerosPayload(responseParam.Size) };
+                    await responseStream.WriteAsync(response);
+                }
+            });
+        }
+
+        public override Task HalfDuplexCall(IAsyncStreamReader<StreamingOutputCallRequest> requestStream, IServerStreamWriter<StreamingOutputCallResponse> responseStream, ServerCallContext context)
+        {
+            throw new NotImplementedException();
+        }
+
+        private static Payload CreateZerosPayload(int size)
+        {
+            return new Payload { Body = ByteString.CopyFrom(new byte[size]) };
+        }
+
+        private static async Task EnsureEchoMetadataAsync(ServerCallContext context, bool enableCompression = false)
+        {
+            var echoInitialList = context.RequestHeaders.Where((entry) => entry.Key == "x-grpc-test-echo-initial").ToList();
+
+            // Append grpc internal compression header if compression is requested by the client
+            if (enableCompression)
+            {
+                echoInitialList.Add(new Metadata.Entry("grpc-internal-encoding-request", "gzip"));
+            }
+
+            if (echoInitialList.Any()) {
+                var entry = echoInitialList.Single();
+                await context.WriteResponseHeadersAsync(new Metadata { entry });
+            }
+
+            var echoTrailingList = context.RequestHeaders.Where((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ToList();
+            if (echoTrailingList.Any()) {
+                context.ResponseTrailers.Add(echoTrailingList.Single());
+            }
+        }
+
+        private static void EnsureEchoStatus(EchoStatus responseStatus, ServerCallContext context)
+        {
+            if (responseStatus != null)
+            {
+                var statusCode = (StatusCode)responseStatus.Code;
+                context.Status = new Status(statusCode, responseStatus.Message);
+            }
+        }
+
+        private static void EnsureCompression(BoolValue? expectCompressed, ServerCallContext context)
+        {
+            if (expectCompressed != null)
+            {
+                // ServerCallContext.RequestHeaders filters out grpc-* headers
+                // Get grpc-encoding from HttpContext instead
+                var encoding = context.GetHttpContext().Request.Headers.SingleOrDefault(h => h.Key == "grpc-encoding").Value.SingleOrDefault();
+                if (expectCompressed.Value)
+                {
+                    if (encoding == null || encoding == "identity")
+                    {
+                        throw new RpcException(new Status(StatusCode.InvalidArgument, string.Empty));
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/Grpc/test/testassets/Proto/grpc/testing/empty.proto b/src/Grpc/test/testassets/Proto/grpc/testing/empty.proto
new file mode 100644
index 00000000000..6a0aa88dfde
--- /dev/null
+++ b/src/Grpc/test/testassets/Proto/grpc/testing/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/src/Grpc/test/testassets/Proto/grpc/testing/messages.proto b/src/Grpc/test/testassets/Proto/grpc/testing/messages.proto
new file mode 100644
index 00000000000..7b1b7286dce
--- /dev/null
+++ b/src/Grpc/test/testassets/Proto/grpc/testing/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/src/Grpc/test/testassets/Proto/grpc/testing/test.proto b/src/Grpc/test/testassets/Proto/grpc/testing/test.proto
new file mode 100644
index 00000000000..86d6ab60506
--- /dev/null
+++ b/src/Grpc/test/testassets/Proto/grpc/testing/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);
+}
diff --git a/src/Grpc/test/testassets/README.md b/src/Grpc/test/testassets/README.md
new file mode 100644
index 00000000000..d7798c92695
--- /dev/null
+++ b/src/Grpc/test/testassets/README.md
@@ -0,0 +1,3 @@
+InteropTestsClient and InteropTestsWebsite are copied from [grpc-dotnet](https://github.com/grpc/grpc-dotnet/tree/master/testassets).
+
+For more information about the interop tests, see the [interop tests specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md).
diff --git a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs
index 753eb1258a5..86724b970fb 100644
--- a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs
+++ b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
 using AngleSharp.Dom.Html;
 using AngleSharp.Parser.Html;
 using Microsoft.AspNetCore.Certificates.Generation;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Server.IntegrationTesting;
 using Microsoft.Extensions.CommandLineUtils;
 using Microsoft.Extensions.Logging.Abstractions;
diff --git a/src/ProjectTemplates/test/Helpers/ErrorMessages.cs b/src/ProjectTemplates/test/Helpers/ErrorMessages.cs
index c63f008f831..744ada299bf 100644
--- a/src/ProjectTemplates/test/Helpers/ErrorMessages.cs
+++ b/src/ProjectTemplates/test/Helpers/ErrorMessages.cs
@@ -1,6 +1,8 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
+using Microsoft.AspNetCore.Internal;
+
 namespace Templates.Test.Helpers
 {
     internal static class ErrorMessages
diff --git a/src/ProjectTemplates/test/Helpers/Project.cs b/src/ProjectTemplates/test/Helpers/Project.cs
index 3fdcdf9d0ce..b16801a5f74 100644
--- a/src/ProjectTemplates/test/Helpers/Project.cs
+++ b/src/ProjectTemplates/test/Helpers/Project.cs
@@ -10,6 +10,7 @@ using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.Extensions.CommandLineUtils;
 using Xunit;
 using Xunit.Abstractions;
diff --git a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs
index 3eed004ef60..5c94fde2b49 100644
--- a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs
+++ b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs
@@ -8,6 +8,7 @@ using System.Linq;
 using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.Extensions.CommandLineUtils;
 using Xunit;
 using Xunit.Abstractions;
diff --git a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs
index bd393b8d586..d702e01be1e 100644
--- a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs
+++ b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs
@@ -10,6 +10,7 @@ using System.Net.Http;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.E2ETesting;
+using Microsoft.AspNetCore.Internal;
 using Newtonsoft.Json.Linq;
 using OpenQA.Selenium;
 using Templates.Test.Helpers;
diff --git a/src/ProjectTemplates/test/Helpers/ProcessEx.cs b/src/Shared/Process/ProcessEx.cs
similarity index 99%
rename from src/ProjectTemplates/test/Helpers/ProcessEx.cs
rename to src/Shared/Process/ProcessEx.cs
index db132bdafe0..c1743a2f0a9 100644
--- a/src/ProjectTemplates/test/Helpers/ProcessEx.cs
+++ b/src/Shared/Process/ProcessEx.cs
@@ -15,7 +15,7 @@ using System.Threading.Tasks;
 using Microsoft.Extensions.Internal;
 using Xunit.Abstractions;
 
-namespace Templates.Test.Helpers
+namespace Microsoft.AspNetCore.Internal
 {
     internal class ProcessEx : IDisposable
     {
@@ -100,7 +100,7 @@ namespace Templates.Test.Helpers
             }
 
             startInfo.EnvironmentVariables["NUGET_PACKAGES"] = NUGET_PACKAGES;
-            
+
             if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("helix")))
             {
                 startInfo.EnvironmentVariables["NUGET_FALLBACK_PACKAGES"] = Environment.GetEnvironmentVariable("NUGET_FALLBACK_PACKAGES");
@@ -195,7 +195,7 @@ namespace Templates.Test.Helpers
             }
         }
 
-        private static string GetNugetPackagesRestorePath() => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NUGET_RESTORE"))) 
+        private static string GetNugetPackagesRestorePath() => (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NUGET_RESTORE")))
             ? typeof(ProcessEx).Assembly
                 .GetCustomAttributes<AssemblyMetadataAttribute>()
                 .First(attribute => attribute.Key == "TestPackageRestorePath")
-- 
GitLab