diff --git a/src/csharp/.gitignore b/src/csharp/.gitignore
index deac55029eeddea0cda72a0446f949a0a48552d9..0f96a4822194b96c319fe9e62982b22b0e2272f4 100644
--- a/src/csharp/.gitignore
+++ b/src/csharp/.gitignore
@@ -7,6 +7,7 @@ Grpc.v12.suo
 Grpc.sdf
 
 TestResult.xml
+coverage_results.xml
 /TestResults
 .vs/
 *.nupkg
diff --git a/src/csharp/.nuget/packages.config b/src/csharp/.nuget/packages.config
index a7df95cf6bd8d02347fc9409c9f57f68a8b4450d..89a310ac5697ed25fe4542a4f74e3fa1cc67dfeb 100644
--- a/src/csharp/.nuget/packages.config
+++ b/src/csharp/.nuget/packages.config
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
   <package id="NUnit.Runners" version="2.6.4" />
+  <package id="OpenCover" version="4.6.166" />
+  <package id="ReportGenerator" version="2.3.2.0" />
 </packages>
\ No newline at end of file
diff --git a/tools/run_tests/build_csharp.sh b/tools/run_tests/build_csharp.sh
index 6737d88b273fa577083d535442bc70a53ebca3c5..2c3335179229a87bc4b0a2ad305bd4264cfe13db 100755
--- a/tools/run_tests/build_csharp.sh
+++ b/tools/run_tests/build_csharp.sh
@@ -30,7 +30,7 @@
 
 set -ex
 
-if [ "$CONFIG" = "dbg" ]
+if [ "$CONFIG" = "dbg" ] || [ "$CONFIG" = "gcov" ]
 then
   MSBUILD_CONFIG="Debug"
 else
diff --git a/tools/run_tests/run_csharp.bat b/tools/run_tests/run_csharp.bat
index 310cfe0d2febe85b99b9694b0e01751d83b4c7d8..0e33e5295a4f8f1c946156a935b9320887bc121c 100644
--- a/tools/run_tests/run_csharp.bat
+++ b/tools/run_tests/run_csharp.bat
@@ -2,13 +2,23 @@
 
 setlocal
 
-@rem enter this directory
+@rem enter src/csharp directory
 cd /d %~dp0\..\..\src\csharp
 
-@rem set UUID variable to a random GUID, we will use it to put TestResults.xml to a dedicated directory, so that parallel test runs don't collide
-for /F %%i in ('powershell -Command "[guid]::NewGuid().ToString()"') do (set UUID=%%i)
+if not "%CONFIG%" == "gcov" (
+  @rem Run tests for assembly passed as 1st arg.
 
-packages\NUnit.Runners.2.6.4\tools\nunit-console-x86.exe /domain:None -labels "%1/bin/Debug/%1.dll" -work test-results/%UUID% || goto :error
+  @rem set UUID variable to a random GUID, we will use it to put TestResults.xml to a dedicated directory, so that parallel test runs don't collide
+  for /F %%i in ('powershell -Command "[guid]::NewGuid().ToString()"') do (set UUID=%%i)
+
+  packages\NUnit.Runners.2.6.4\tools\nunit-console-x86.exe /domain:None -labels "%1/bin/Debug/%1.dll" -work test-results/%UUID% || goto :error
+) else (
+  @rem Run all tests with code coverage
+
+  packages\OpenCover.4.6.166\tools\OpenCover.Console.exe -target:"packages\NUnit.Runners.2.6.4\tools\nunit-console-x86.exe" -targetdir:"." -targetargs:"/domain:None -labels Grpc.Core.Tests/bin/Debug/Grpc.Core.Tests.dll Grpc.IntegrationTesting/bin/Debug/Grpc.IntegrationTesting.dll Grpc.Examples.Tests/bin/Debug/Grpc.Examples.Tests.dll Grpc.HealthCheck.Tests/bin/Debug/Grpc.HealthCheck.Tests.dll" -filter:"+[Grpc.Core]*"  -register:user -output:coverage_results.xml || goto :error
+
+  packages\ReportGenerator.2.3.2.0\tools\ReportGenerator.exe -reports:"coverage_results.xml" -targetdir:"..\..\reports\csharp_coverage" -reporttypes:"Html;TextSummary" || goto :error
+)
 
 endlocal
 
diff --git a/tools/run_tests/run_csharp.sh b/tools/run_tests/run_csharp.sh
index c0fedca193532abf2570a7fb2258d8b104166dbf..37e86feaad3c3fe4676c068cf501e83aeb3342ae 100755
--- a/tools/run_tests/run_csharp.sh
+++ b/tools/run_tests/run_csharp.sh
@@ -34,7 +34,7 @@ CONFIG=${CONFIG:-opt}
 
 NUNIT_CONSOLE="mono packages/NUnit.Runners.2.6.4/tools/nunit-console.exe"
 
-if [ "$CONFIG" = "dbg" ]
+if [ "$CONFIG" = "dbg" ] || [ "$CONFIG" = "gcov" ]
 then
   MSBUILD_CONFIG="Debug"
 else
@@ -45,10 +45,24 @@ fi
 cd $(dirname $0)/../..
 
 root=`pwd`
-cd src/csharp
-
 export LD_LIBRARY_PATH=$root/libs/$CONFIG
 
-$NUNIT_CONSOLE -labels "$1/bin/$MSBUILD_CONFIG/$1.dll"
+if [ "$CONFIG" = "gcov" ]
+then
+  (cd src/csharp; $NUNIT_CONSOLE -labels \
+      "Grpc.Core.Tests/bin/$MSBUILD_CONFIG/Grpc.Core.Tests.dll" \
+      "Grpc.Examples.Tests/bin/$MSBUILD_CONFIG/Grpc.Examples.Tests.dll" \
+      "Grpc.HealthCheck.Tests/bin/$MSBUILD_CONFIG/Grpc.HealthCheck.Tests.dll" \
+      "Grpc.IntegrationTesting/bin/$MSBUILD_CONFIG/Grpc.IntegrationTesting.dll")
+
+  gcov objs/gcov/src/csharp/ext/*.o
+  lcov --base-directory . --directory . -c -o coverage.info
+  lcov -e coverage.info '**/src/csharp/ext/*' -o coverage.info
+  genhtml -o reports/csharp_ext_coverage --num-spaces 2 \
+    -t 'gRPC C# native extension test coverage' coverage.info \
+    --rc genhtml_hi_limit=95 --rc genhtml_med_limit=80 --no-prefix
+else
+  (cd src/csharp; $NUNIT_CONSOLE -labels "$1/bin/$MSBUILD_CONFIG/$1.dll")
+fi
 
 
diff --git a/tools/run_tests/run_tests.py b/tools/run_tests/run_tests.py
index 8a3976598acda74447eceb22838ab4539f3ca40e..8482b2fd528bc6abad3ea47d00c169d8716f8f91 100755
--- a/tools/run_tests/run_tests.py
+++ b/tools/run_tests/run_tests.py
@@ -342,10 +342,18 @@ class CSharpLanguage(object):
       cmd = 'tools\\run_tests\\run_csharp.bat'
     else:
       cmd = 'tools/run_tests/run_csharp.sh'
-    return [config.job_spec([cmd, assembly],
-            None, shortname=assembly,
-            environ=_FORCE_ENVIRON_FOR_WRAPPERS)
-            for assembly in assemblies]
+
+    if config.build_config == 'gcov':
+      # On Windows, we only collect C# code coverage.
+      # On Linux, we only collect coverage for native extension.
+      # For code coverage all tests need to run as one suite.
+      return [config.job_spec([cmd], None,
+              environ=_FORCE_ENVIRON_FOR_WRAPPERS)]
+    else:
+      return [config.job_spec([cmd, assembly],
+              None, shortname=assembly,
+              environ=_FORCE_ENVIRON_FOR_WRAPPERS)
+              for assembly in assemblies]
 
   def pre_build_steps(self):
     if self.platform == 'windows':