diff --git a/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..0ea621adea586e091d3b6dc09a7a6499857f5dfa
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs
@@ -0,0 +1,134 @@
+#region Copyright notice and license
+
+// Copyright 2018 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 Grpc.Tools;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using NUnit.Framework;
+
+namespace Grps.Tools.Tests {
+  public class DepFileUtilTests {
+
+    [Test]
+    public void HashString64Hex_IsSane() {
+      string hashFoo1 = DepFileUtil.HashString64Hex("foo");
+      string hashEmpty = DepFileUtil.HashString64Hex("");
+      string hashFoo2 = DepFileUtil.HashString64Hex("foo");
+
+      StringAssert.IsMatch("^[a-f0-9]{16}$", hashFoo1);
+      Assert.AreEqual(hashFoo1, hashFoo2);
+      Assert.AreNotEqual(hashFoo1, hashEmpty);
+    }
+
+    [Test]
+    public void GetDepFilenameForProto_IsSane() {
+      StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}_foo.protodep$",
+        DepFileUtil.GetDepFilenameForProto("out", "foo.proto"));
+      StringAssert.IsMatch(@"^[a-f0-9]{16}_foo.protodep$",
+        DepFileUtil.GetDepFilenameForProto("", "foo.proto"));
+    }
+
+    [Test]
+    public void GetDepFilenameForProto_HashesDir() {
+      string PickHash(string fname) =>
+        DepFileUtil.GetDepFilenameForProto("", fname).Substring(0, 16);
+
+      string same1 =   PickHash("dir1/dir2/foo.proto");
+      string same2 =   PickHash("dir1/dir2/proto.foo");
+      string same3 =   PickHash("dir1/dir2/proto");
+      string same4 =   PickHash("dir1/dir2/.proto");
+      string unsame1 = PickHash("dir2/foo.proto");
+      string unsame2 = PickHash("/dir2/foo.proto");
+
+      Assert.AreEqual(same1, same2);
+      Assert.AreEqual(same1, same3);
+      Assert.AreEqual(same1, same4);
+      Assert.AreNotEqual(same1, unsame1);
+      Assert.AreNotEqual(unsame1, unsame2);
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    // Full file reading tests
+
+    // Generated by protoc on Windows. Slashes vary.
+    const string depFile1 =
+@"C:\projects\foo\src\./foo.grpc.pb.cc \
+C:\projects\foo\src\./foo.grpc.pb.h \
+C:\projects\foo\src\./foo.pb.cc \
+ C:\projects\foo\src\./foo.pb.h: C:/usr/include/google/protobuf/wrappers.proto\
+   C:/usr/include/google/protobuf/any.proto\
+C:/usr/include/google/protobuf/source_context.proto\
+   C:/usr/include/google/protobuf/type.proto\
+   foo.proto";
+
+    // This has a nasty output directory with a space.
+    const string depFile2 =
+@"obj\Release x64\net45\/Foo.cs \
+obj\Release x64\net45\/FooGrpc.cs: C:/usr/include/google/protobuf/wrappers.proto\
+ C:/projects/foo/src//foo.proto";
+
+    [Test]
+    public void ReadDependencyInput_FullFile1() {
+      string[] deps = ReadDependencyInputFromFileData(depFile1, "foo.proto");
+
+      Assert.NotNull(deps);
+      Assert.That(deps, Has.Length.InRange(4, 5));  // foo.proto may or may not be listed.
+      Assert.That(deps, Has.One.EndsWith("wrappers.proto"));
+      Assert.That(deps, Has.One.EndsWith("type.proto"));
+      Assert.That(deps, Has.None.StartWith(" "));
+    }
+
+    [Test]
+    public void ReadDependencyInput_FullFile2() {
+      string[] deps = ReadDependencyInputFromFileData(depFile2, "C:/projects/foo/src/foo.proto");
+
+      Assert.NotNull(deps);
+      Assert.That(deps, Has.Length.InRange(1, 2));
+      Assert.That(deps, Has.One.EndsWith("wrappers.proto"));
+      Assert.That(deps, Has.None.StartWith(" "));
+    }
+
+    [Test]
+    public void ReadDependencyInput_FullFileUnparsable() {
+      string[] deps = ReadDependencyInputFromFileData("a:/foo.proto", "/foo.proto");
+      Assert.NotNull(deps);
+      Assert.Zero(deps.Length);
+    }
+
+    // NB in our tests files are put into the temp directory but all have
+    // different names. Avoid adding files with the same directory path and
+    // name, or add reasonable handling for it if required. Tests are run in
+    // parallel and will collide otherwise.
+    private string[] ReadDependencyInputFromFileData(string fileData, string protoName) {
+      string tempPath = Path.GetTempPath();
+      string tempfile = DepFileUtil.GetDepFilenameForProto(tempPath, protoName);
+      try {
+        File.WriteAllText(tempfile, fileData);
+        var mockEng = new Moq.Mock<IBuildEngine>();
+        var log = new TaskLoggingHelper(mockEng.Object, "x");
+        return DepFileUtil.ReadDependencyInputs(tempPath, protoName, log);
+      } finally {
+        try {
+          File.Delete(tempfile);
+        } catch { }
+      }
+    }
+  }
+}
diff --git a/src/csharp/Grpc.Tools.Tests/GeneratorTests.cs b/src/csharp/Grpc.Tools.Tests/GeneratorTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..0a273380b989a65cf162d8a8a40a7bd045f1b5c2
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/GeneratorTests.cs
@@ -0,0 +1,165 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System.IO;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests {
+  public class GeneratorTests {
+    protected Mock<IBuildEngine> _mockEngine;
+    protected TaskLoggingHelper _log;
+
+    [SetUp]
+    public void SetUp() {
+      _mockEngine = new Mock<IBuildEngine>();
+      _log = new TaskLoggingHelper(_mockEngine.Object, "dummy");
+    }
+
+    [TestCase("csharp")]
+    [TestCase("CSharp")]
+    [TestCase("cpp")]
+    public void ValidLanguages(string lang) {
+      Assert.IsNotNull(GeneratorServices.GetForLanguage(lang, _log));
+      _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Never);
+    }
+
+    [TestCase("")]
+    [TestCase("COBOL")]
+    public void InvalidLanguages(string lang) {
+      Assert.IsNull(GeneratorServices.GetForLanguage(lang, _log));
+      _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Once);
+    }
+  };
+
+  public class CSharpGeneratorTests : GeneratorTests {
+    GeneratorServices _generator;
+
+    [SetUp]
+    public new void SetUp() {
+      _generator = GeneratorServices.GetForLanguage("CSharp", _log);
+    }
+
+    [TestCase("foo.proto", "Foo.cs", "FooGrpc.cs")]
+    [TestCase("sub/foo.proto", "Foo.cs", "FooGrpc.cs")]
+    [TestCase("one_two.proto", "OneTwo.cs", "OneTwoGrpc.cs")]
+    [TestCase("__one_two!.proto", "OneTwo!.cs", "OneTwo!Grpc.cs")]
+    [TestCase("one(two).proto", "One(two).cs", "One(two)Grpc.cs")]
+    [TestCase("one_(two).proto", "One(two).cs", "One(two)Grpc.cs")]
+    [TestCase("one two.proto", "One two.cs", "One twoGrpc.cs")]
+    [TestCase("one_ two.proto", "One two.cs", "One twoGrpc.cs")]
+    [TestCase("one .proto", "One .cs", "One Grpc.cs")]
+    public void NameMangling(string proto, string expectCs, string expectGrpcCs) {
+      var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "grpcservices", "both"));
+      Assert.AreEqual(2, poss.Length);
+      Assert.Contains(expectCs, poss);
+      Assert.Contains(expectGrpcCs, poss);
+    }
+
+    [Test]
+    public void NoGrpcOneOutput() {
+      var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto"));
+      Assert.AreEqual(1, poss.Length);
+    }
+
+    [TestCase("none")]
+    [TestCase("")]
+    public void GrpcNoneOneOutput(string grpc) {
+      var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+      var poss = _generator.GetPossibleOutputs(item);
+      Assert.AreEqual(1, poss.Length);
+    }
+
+    [TestCase("client")]
+    [TestCase("server")]
+    [TestCase("both")]
+    public void GrpcEnabledTwoOutputs(string grpc) {
+      var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+      var poss = _generator.GetPossibleOutputs(item);
+      Assert.AreEqual(2, poss.Length);
+    }
+
+    [Test]
+    public void OutputDirMetadataRecognized() {
+      var item = Utils.MakeItem("foo.proto", "OutputDir", "out");
+      var poss = _generator.GetPossibleOutputs(item);
+      Assert.AreEqual(1, poss.Length);
+      Assert.That(poss[0], Is.EqualTo("out/Foo.cs") | Is.EqualTo("out\\Foo.cs"));
+    }
+  };
+
+  public class CppGeneratorTests : GeneratorTests {
+    GeneratorServices _generator;
+
+    [SetUp]
+    public new void SetUp() {
+      _generator = GeneratorServices.GetForLanguage("Cpp", _log);
+    }
+
+    [TestCase("foo.proto", "", "foo")]
+    [TestCase("foo.proto", ".", "foo")]
+    [TestCase("foo.proto", "./", "foo")]
+    [TestCase("sub/foo.proto", "", "sub/foo")]
+    [TestCase("root/sub/foo.proto", "root", "sub/foo")]
+    [TestCase("root/sub/foo.proto", "root", "sub/foo")]
+    [TestCase("/root/sub/foo.proto", "/root", "sub/foo")]
+    public void RelativeDirectoryCompute(string proto, string root, string expectStem) {
+      if (Path.DirectorySeparatorChar == '\\')
+        expectStem = expectStem.Replace('/', '\\');
+      var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "ProtoRoot", root));
+      Assert.AreEqual(2, poss.Length);
+      Assert.Contains(expectStem + ".pb.cc", poss);
+      Assert.Contains(expectStem + ".pb.h", poss);
+    }
+
+    [Test]
+    public void NoGrpcTwoOutputs() {
+      var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto"));
+      Assert.AreEqual(2, poss.Length);
+    }
+
+    [TestCase("false")]
+    [TestCase("")]
+    public void GrpcDisabledTwoOutput(string grpc) {
+      var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+      var poss = _generator.GetPossibleOutputs(item);
+      Assert.AreEqual(2, poss.Length);
+    }
+
+    [TestCase("true")]
+    public void GrpcEnabledFourOutputs(string grpc) {
+      var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+      var poss = _generator.GetPossibleOutputs(item);
+      Assert.AreEqual(4, poss.Length);
+      Assert.Contains("foo.pb.cc", poss);
+      Assert.Contains("foo.pb.h", poss);
+      Assert.Contains("foo_grpc.pb.cc", poss);
+      Assert.Contains("foo_grpc.pb.h", poss);
+    }
+
+    [Test]
+    public void OutputDirMetadataRecognized() {
+      var item = Utils.MakeItem("foo.proto", "OutputDir", "out");
+      var poss = _generator.GetPossibleOutputs(item);
+      Assert.AreEqual(2, poss.Length);
+      Assert.That(Path.GetDirectoryName(poss[0]), Is.EqualTo("out"));
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..585e4518b59a5420545c270e748d2eaa8fcfe3f6
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+
+  <PropertyGroup>
+    <TargetFrameworks>netcoreapp1.0;net45</TargetFrameworks>
+    <OutputType>Exe</OutputType>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Grpc.Tools\Grpc.Tools.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Moq" Version="4.7.145" />
+    <PackageReference Include="NUnit" Version="3.9.0" />
+    <PackageReference Include="NUnitLite" Version="3.9.0" />
+  </ItemGroup>
+
+  <ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' ">
+    <PackageReference Include="Microsoft.Build.Framework" Version="15.5.180" />
+    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.5.180" />
+  </ItemGroup>
+
+  <ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
+    <Reference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.v4.0" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/csharp/Grpc.Tools.Tests/NUnitMain.cs b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs
new file mode 100644
index 0000000000000000000000000000000000000000..c4452c50d2c8c5cbba5419c4d2c30bf83f8f8fe6
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs
@@ -0,0 +1,31 @@
+#region Copyright notice and license
+
+// Copyright 2018 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.Reflection;
+using NUnitLite;
+
+namespace Grps.Tools.Tests {
+  static class NUnitMain {
+    public static int Main(string[] args) =>
+#if NETCOREAPP1_0
+      new AutoRun(typeof(NUnitMain).GetTypeInfo().Assembly).Execute(args);
+#else
+      new AutoRun().Execute(args);
+#endif
+  };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileTaskTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileTaskTest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..86c78289b29692650ccf8608328c682546ecd955
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileTaskTest.cs
@@ -0,0 +1,232 @@
+#region Copyright notice and license
+
+// Copyright 2018 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.Reflection;
+using Microsoft.Build.Framework;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests {
+  public class ProtoCompileBasicTests {
+    // Mock task class that stops right before invoking protoc.
+    public class ProtoCompileTestable : ProtoCompile {
+      public string LastPathToTool { get; private set; }
+      public string[] LastResponseFile { get; private set; }
+
+      protected override int ExecuteTool(string pathToTool,
+                                         string response,
+                                         string commandLine) {
+        // We should never be using command line commands.
+        Assert.That(commandLine, Is.Null | Is.Empty);
+
+        // Must receive a path to tool
+        Assert.That(pathToTool, Is.Not.Null & Is.Not.Empty);
+        Assert.That(response, Is.Not.Null & Does.EndWith("\n"));
+
+        LastPathToTool = pathToTool;
+        LastResponseFile = response.Remove(response.Length - 1).Split('\n');
+
+        // Do not run the tool, but pretend it ran successfully.
+        return 0;
+      }
+    };
+
+    protected Mock<IBuildEngine> _mockEngine;
+    protected ProtoCompileTestable _task;
+
+    [SetUp]
+    public void SetUp() {
+      _mockEngine = new Mock<IBuildEngine>();
+      _task = new ProtoCompileTestable {
+        BuildEngine = _mockEngine.Object
+      };
+    }
+
+    [TestCase("ProtoBuf")]
+    [TestCase("Generator")]
+    [TestCase("OutputDir")]
+    [Description("We trust MSBuild to initialize these properties.")]
+    public void RequiredAttributePresentOnProperty(string prop) {
+      var pinfo = _task.GetType()?.GetProperty(prop);
+      Assert.NotNull(pinfo);
+      Assert.That(pinfo, Has.Attribute<RequiredAttribute>());
+    }
+  };
+
+  internal class ProtoCompileCommandLineGeneratorTests : ProtoCompileBasicTests {
+    [SetUp]
+    public new void SetUp() {
+      _task.Generator = "csharp";
+      _task.OutputDir = "outdir";
+      _task.ProtoBuf = Utils.MakeSimpleItems("a.proto");
+    }
+
+    void ExecuteExpectSuccess() {
+      _mockEngine
+        .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
+        .Callback((BuildErrorEventArgs e) =>
+            Assert.Fail($"Error logged by build engine:\n{e.Message}"));
+      bool result = _task.Execute();
+      Assert.IsTrue(result);
+    }
+
+    [Test]
+    public void MinimalCompile() {
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastPathToTool, Does.Match(@"protoc(.exe)?$"));
+      Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+        "--csharp_out=outdir", "a.proto" }));
+    }
+
+    [Test]
+    public void CompileTwoFiles() {
+      _task.ProtoBuf = Utils.MakeSimpleItems("a.proto", "foo/b.proto");
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+        "--csharp_out=outdir", "a.proto", "foo/b.proto" }));
+    }
+
+    [Test]
+    public void CompileWithProtoPaths() {
+      _task.ProtoPath = new[] { "/path1", "/path2" };
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+        "--csharp_out=outdir", "--proto_path=/path1",
+        "--proto_path=/path2", "a.proto" }));
+    }
+
+    [TestCase("Cpp")]
+    [TestCase("CSharp")]
+    [TestCase("Java")]
+    [TestCase("Javanano")]
+    [TestCase("Js")]
+    [TestCase("Objc")]
+    [TestCase("Php")]
+    [TestCase("Python")]
+    [TestCase("Ruby")]
+    public void CompileWithOptions(string gen) {
+      _task.Generator = gen;
+      _task.OutputOptions = new[] { "foo", "bar" };
+      ExecuteExpectSuccess();
+      gen = gen.ToLowerInvariant();
+      Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+        $"--{gen}_out=outdir", $"--{gen}_opt=foo,bar", "a.proto" }));
+    }
+
+    [Test]
+    public void OutputDependencyFile() {
+      _task.DependencyOut = "foo/my.protodep";
+      // Task fails trying to read the non-generated file; we ignore that.
+      _task.Execute();
+      Assert.That(_task.LastResponseFile,
+        Does.Contain("--dependency_out=foo/my.protodep"));
+    }
+
+    [Test]
+    public void OutputDependencyWithProtoDepDir() {
+      _task.ProtoDepDir = "foo";
+      // Task fails trying to read the non-generated file; we ignore that.
+      _task.Execute();
+      Assert.That(_task.LastResponseFile,
+        Has.One.Match(@"^--dependency_out=foo[/\\].+_a.protodep$"));
+    }
+
+    [Test]
+    public void GenerateGrpc() {
+      _task.GrpcPluginExe = "/foo/grpcgen";
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
+        "--csharp_out=outdir", "--grpc_out=outdir",
+        "--plugin=protoc-gen-grpc=/foo/grpcgen" }));
+    }
+
+    [Test]
+    public void GenerateGrpcWithOutDir() {
+      _task.GrpcPluginExe = "/foo/grpcgen";
+      _task.GrpcOutputDir = "gen-out";
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
+        "--csharp_out=outdir", "--grpc_out=gen-out" }));
+    }
+
+    [Test]
+    public void GenerateGrpcWithOptions() {
+      _task.GrpcPluginExe = "/foo/grpcgen";
+      _task.GrpcOutputOptions = new[] { "baz", "quux" };
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastResponseFile,
+                  Does.Contain("--grpc_opt=baz,quux"));
+    }
+
+    [Test]
+    public void DirectoryArgumentsSlashTrimmed() {
+      _task.GrpcPluginExe = "/foo/grpcgen";
+      _task.GrpcOutputDir = "gen-out/";
+      _task.OutputDir = "outdir/";
+      _task.ProtoPath = new[] { "/path1/", "/path2/" };
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
+        "--proto_path=/path1", "--proto_path=/path2",
+        "--csharp_out=outdir", "--grpc_out=gen-out" }));
+    }
+
+    [TestCase("."      , ".")]
+    [TestCase("/"      , "/")]
+    [TestCase("//"     , "/")]
+    [TestCase("/foo/"  , "/foo")]
+    [TestCase("/foo"   , "/foo")]
+    [TestCase("foo/"   , "foo")]
+    [TestCase("foo//"  , "foo")]
+    [TestCase("foo/\\" , "foo")]
+    [TestCase("foo\\/" , "foo")]
+    [TestCase("C:\\foo", "C:\\foo")]
+    [TestCase("C:"     , "C:")]
+    [TestCase("C:\\"   , "C:\\")]
+    [TestCase("C:\\\\" , "C:\\")]
+    public void DirectorySlashTrimmingCases(string given, string expect) {
+      _task.OutputDir = given;
+      ExecuteExpectSuccess();
+      Assert.That(_task.LastResponseFile,
+                  Does.Contain("--csharp_out=" + expect));
+    }
+  };
+
+  internal class ProtoCompileCommandLinePrinterTests : ProtoCompileBasicTests {
+    [SetUp]
+    public new void SetUp() {
+      _task.Generator = "csharp";
+      _task.OutputDir = "outdir";
+      _task.ProtoBuf = Utils.MakeSimpleItems("a.proto");
+
+      _mockEngine
+        .Setup(me => me.LogMessageEvent(It.IsAny<BuildMessageEventArgs>()))
+        .Callback((BuildMessageEventArgs e) =>
+            Assert.Fail($"Error logged by build engine:\n{e.Message}"))
+        .Verifiable("Command line was not output by the task.");
+    }
+
+    void ExecuteExpectSuccess() {
+      _mockEngine
+        .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
+        .Callback((BuildErrorEventArgs e) =>
+            Assert.Fail($"Error logged by build engine:\n{e.Message}"));
+      bool result = _task.Execute();
+      Assert.IsTrue(result);
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..3ba0bdfbf6b2b845942c0d0ecce9d75e7466ec26
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs
@@ -0,0 +1,53 @@
+#region Copyright notice and license
+
+// Copyright 2018 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 Microsoft.Build.Framework;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests {
+  // This test requires that environment variables be set to the exected
+  // output of the task in its external test harness:
+  //   PROTOTOOLS_TEST_CPU = { x64 | x86 }
+  //   PROTOTOOLS_TEST_OS = { linux | macosx | windows }
+  public class ProtoToolsPlatformTaskTests {
+    static string s_expectOs;
+    static string s_expectCpu;
+
+    [OneTimeSetUp]
+    public static void Init() {
+      s_expectCpu = Environment.GetEnvironmentVariable("PROTOTOOLS_TEST_CPU");
+      s_expectOs = Environment.GetEnvironmentVariable("PROTOTOOLS_TEST_OS");
+      if (s_expectCpu == null || s_expectOs == null)
+        Assert.Inconclusive("This test requires PROTOTOOLS_TEST_CPU and " +
+          "PROTOTOOLS_TEST_OS set in the environment to match the OS it runs on.");
+    }
+
+    [Test]
+    public void CpuAndOsMatchExpected() {
+      var mockEng = new Mock<IBuildEngine>();
+      var task = new ProtoToolsPlatform() {
+        BuildEngine = mockEng.Object
+      };
+      task.Execute();
+      Assert.AreEqual(s_expectCpu, task.Cpu);
+      Assert.AreEqual(s_expectOs, task.Os);
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/Utils.cs b/src/csharp/Grpc.Tools.Tests/Utils.cs
new file mode 100644
index 0000000000000000000000000000000000000000..618e33545244d0e0527482057c94169ad7f5faac
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/Utils.cs
@@ -0,0 +1,43 @@
+#region Copyright notice and license
+
+// Copyright 2018 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.Linq;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools.Tests {
+  static class Utils {
+    // Build an item with a name from args[0] and metadata key-value pairs
+    // from the rest of args, interleaved.
+    // This does not do any checking, and expects an odd number of args.
+    public static ITaskItem MakeItem(params string[] args) {
+      var item = new TaskItem(args[0]);
+      for (int i = 1; i < args.Length; i += 2)
+        item.SetMetadata(args[i], args[i + 1]);
+      return item;
+    }
+
+    // Return an array of items from given itemspecs.
+    public static ITaskItem[] MakeSimpleItems(params string[] specs) {
+      return specs.Select(s => new TaskItem(s)).ToArray();
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools.nuspec b/src/csharp/Grpc.Tools.nuspec
deleted file mode 100644
index 0cae5572fd8deb51fcd0eaddaeee3dc591ccd0a3..0000000000000000000000000000000000000000
--- a/src/csharp/Grpc.Tools.nuspec
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<package>
-  <metadata>
-    <id>Grpc.Tools</id>
-    <title>gRPC C# Tools</title>
-    <summary>Tools for C# implementation of gRPC - an RPC library and framework</summary>
-    <description>Precompiled protobuf compiler and gRPC protobuf compiler plugin for generating gRPC client/server C# code. Binaries are available for Windows, Linux and MacOS.</description>
-    <version>$version$</version>
-    <authors>Google Inc.</authors>
-    <owners>grpc-packages</owners>
-    <licenseUrl>https://github.com/grpc/grpc/blob/master/LICENSE</licenseUrl>
-    <projectUrl>https://github.com/grpc/grpc</projectUrl>
-    <requireLicenseAcceptance>false</requireLicenseAcceptance>
-    <releaseNotes>Release $version$</releaseNotes>
-    <copyright>Copyright 2015, Google Inc.</copyright>
-    <tags>gRPC RPC Protocol HTTP/2</tags>
-  </metadata>
-  <files>
-    <!-- forward slashes in src path enable building on Linux -->
-    <file src="protoc_plugins/protoc_windows_x86/protoc.exe" target="tools/windows_x86/protoc.exe" />
-    <file src="protoc_plugins/protoc_windows_x86/grpc_csharp_plugin.exe" target="tools/windows_x86/grpc_csharp_plugin.exe" />
-    <file src="protoc_plugins/protoc_windows_x64/protoc.exe" target="tools/windows_x64/protoc.exe" />
-    <file src="protoc_plugins/protoc_windows_x64/grpc_csharp_plugin.exe" target="tools/windows_x64/grpc_csharp_plugin.exe" />
-    <file src="protoc_plugins/protoc_linux_x86/protoc" target="tools/linux_x86/protoc" />
-    <file src="protoc_plugins/protoc_linux_x86/grpc_csharp_plugin" target="tools/linux_x86/grpc_csharp_plugin" />
-    <file src="protoc_plugins/protoc_linux_x64/protoc" target="tools/linux_x64/protoc" />
-    <file src="protoc_plugins/protoc_linux_x64/grpc_csharp_plugin" target="tools/linux_x64/grpc_csharp_plugin" />
-    <file src="protoc_plugins/protoc_macos_x86/protoc" target="tools/macosx_x86/protoc" />
-    <file src="protoc_plugins/protoc_macos_x86/grpc_csharp_plugin" target="tools/macosx_x86/grpc_csharp_plugin" />
-    <file src="protoc_plugins/protoc_macos_x64/protoc" target="tools/macosx_x64/protoc" />
-    <file src="protoc_plugins/protoc_macos_x64/grpc_csharp_plugin" target="tools/macosx_x64/grpc_csharp_plugin" />
-  </files>
-</package>
diff --git a/src/csharp/Grpc.Tools/Common.cs b/src/csharp/Grpc.Tools/Common.cs
new file mode 100644
index 0000000000000000000000000000000000000000..df539f8c4f9fdafbce71d3cbfeb40f1c6fe96fa6
--- /dev/null
+++ b/src/csharp/Grpc.Tools/Common.cs
@@ -0,0 +1,105 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+
+[assembly: InternalsVisibleTo("Grpc.Tools.Tests")]
+
+namespace Grpc.Tools {
+  // Metadata names that we refer to often.
+  static class Metadata {
+    // On output dependency lists.
+    public static string kSource = "Source";
+    // On ProtoBuf items.
+    public static string kProtoRoot = "ProtoRoot";
+    public static string kOutputDir = "OutputDir";
+    public static string kGrpcServices = "GrpcServices";
+    public static string kGrpcOutputDir = "GrpcOutputDir";
+  };
+
+  // A few flags used to control the behavior under various platforms.
+  internal static class Platform {
+    public enum OsKind { Unknown, Windows, Linux, MacOsX };
+    public static readonly OsKind Os;
+
+    public enum CpuKind { Unknown, X86, X64 };
+    public static readonly CpuKind Cpu;
+
+    // This is not necessarily true, but good enough. BCL lacks a per-FS
+    // API to determine file case sensitivity.
+    public static bool IsFsCaseInsensitive => Os == OsKind.Windows;
+    public static bool IsWindows => Os == OsKind.Windows;
+
+    static Platform() {
+#if NETSTANDARD
+      Os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? OsKind.Windows
+         : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OsKind.Linux
+         : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OsKind.MacOsX
+         : OsKind.Unknown;
+
+      switch (RuntimeInformation.OSArchitecture) {
+        case Architecture.X86: Cpu = CpuKind.X86; break;
+        case Architecture.X64: Cpu = CpuKind.X64; break;
+        // We do not have build tools for other architectures.
+        default: Cpu = CpuKind.Unknown; break;
+      }
+#else
+      // Running under either Mono or full MS framework.
+      Os = OsKind.Windows;
+      if (Type.GetType("Mono.Runtime", throwOnError: false) != null) {
+        // Congratulations. We are running under Mono.
+        var plat = Environment.OSVersion.Platform;
+        if (plat == PlatformID.MacOSX) {
+          Os = OsKind.MacOsX;
+        } else if (plat == PlatformID.Unix || (int)plat == 128) {
+          // TODO(kkm): This is how Mono detects OSX internally. Looks cheesy
+          // to me. Would not testing for /proc absence be more reliable? OSX
+          // did never have it, AFAIK.
+          Os = File.Exists("/usr/lib/libc.dylib") ? OsKind.MacOsX : OsKind.Linux;
+        }
+      }
+
+      // Hope we are not building on ARM under Xamarin!
+      Cpu = Environment.Is64BitOperatingSystem ? CpuKind.X64 : CpuKind.X86;
+#endif
+    }
+  };
+
+  // Exception handling helpers.
+  static class Exceptions {
+    // Returns true iff the exception indicates an error from an I/O call. See
+    // https://github.com/Microsoft/msbuild/blob/v15.4.8.50001/src/Shared/ExceptionHandling.cs#L101
+    static public bool IsIoRelated(Exception ex) =>
+      ex is IOException ||
+      (ex is ArgumentException && !(ex is ArgumentNullException)) ||
+      ex is SecurityException ||
+      ex is UnauthorizedAccessException ||
+      ex is NotSupportedException;
+  };
+
+  // String helpers.
+  static class Strings {
+    // Compare string to argument using OrdinalIgnoreCase comparison.
+    public static bool EqualNoCase(this string a, string b) =>
+      string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
+  }
+}
diff --git a/src/csharp/Grpc.Tools/DepFileUtil.cs b/src/csharp/Grpc.Tools/DepFileUtil.cs
new file mode 100644
index 0000000000000000000000000000000000000000..2a931b7295d2d7786730616f593a3a098386983a
--- /dev/null
+++ b/src/csharp/Grpc.Tools/DepFileUtil.cs
@@ -0,0 +1,198 @@
+#region Copyright notice and license
+
+// Copyright 2018 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.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools {
+  internal static class DepFileUtil {
+/*
+   Sample dependency files. Notable features we have to deal with:
+    * Slash doubling, must normalize them.
+    * Spaces in file names. Cannot just "unwrap" the line on backslash at eof;
+      rather, treat every line as containing one file name except for one with
+      the ':' separator, as containing exactly two.
+    * Deal with ':' also being drive letter separator (second example).
+
+obj\Release\net45\/Foo.cs \
+obj\Release\net45\/FooGrpc.cs: C:/foo/include/google/protobuf/wrappers.proto\
+ C:/projects/foo/src//foo.proto
+
+C:\projects\foo\src\./foo.grpc.pb.cc \
+C:\projects\foo\src\./foo.grpc.pb.h \
+C:\projects\foo\src\./foo.pb.cc \
+C:\projects\foo\src\./foo.pb.h: C:/foo/include/google/protobuf/wrappers.proto\
+ C:/foo/include/google/protobuf/any.proto\
+ C:/foo/include/google/protobuf/source_context.proto\
+ C:/foo/include/google/protobuf/type.proto\
+ foo.proto
+*/
+
+    // Read file names from the dependency file to the right of ':'.
+    public static string[] ReadDependencyInputs(string protoDepDir, string proto,
+                                                TaskLoggingHelper log) {
+      string depFilename = GetDepFilenameForProto(protoDepDir, proto);
+      string[] lines = ReadDepFileLines(depFilename, false, log);
+      if (lines.Length == 0) {
+        return lines;
+      }
+
+      var result = new List<string>();
+      bool skip = true;
+      foreach (string line in lines) {
+        // Start at the only line separating dependency outputs from inputs.
+        int ix = skip ? FindLineSeparator(line) : -1;
+        skip = skip && ix < 0;
+        if (skip) continue;
+        string file = ExtractFilenameFromLine(line, ix + 1, line.Length);
+        if (file == "") {
+          log.LogMessage(MessageImportance.Low,
+    $"Skipping unparsable dependency file {depFilename}.\nLine with error: '{line}'");
+          return new string[0];
+        }
+
+        // Do not bend over backwards trying not to include a proto into its
+        // own list of dependencies. Since a file is not older than self,
+        // it is safe to add; this is purely a memory optimization.
+        if (file != proto) {
+          result.Add(file);
+        }
+      }
+      return result.ToArray();
+    }
+
+    // Read file names from the dependency file to the left of ':'.
+    public static string[] ReadDependencyOutputs(string depFilename,
+                                                TaskLoggingHelper log) {
+      string[] lines = ReadDepFileLines(depFilename, true, log);
+      if (lines.Length == 0) {
+        return lines;
+      }
+
+      var result = new List<string>();
+      foreach (string line in lines) {
+        int ix = FindLineSeparator(line);
+        string file = ExtractFilenameFromLine(line, 0, ix >= 0 ? ix : line.Length);
+        if (file == "") {
+          log.LogError("Unable to parse generated dependency file {0}.\n" +
+                       "Line with error: '{1}'", depFilename, line);
+          return new string[0];
+        }
+        result.Add(file);
+
+        // If this is the line with the separator, do not read further.
+        if (ix >= 0)
+          break;
+      }
+      return result.ToArray();
+    }
+
+    // Get complete dependency file name from directory hash and file name,
+    // tucked onto protoDepDir, e. g.
+    // ("out", "foo/file.proto") => "out/deadbeef12345678_file.protodep".
+    // This way, the filenames are unique but still possible to make sense of.
+    public static string GetDepFilenameForProto(string protoDepDir, string proto) {
+      string dirname = Path.GetDirectoryName(proto);
+      if (Platform.IsFsCaseInsensitive) {
+        dirname = dirname.ToLowerInvariant();
+      }
+      string dirhash = HashString64Hex(dirname);
+      string filename = Path.GetFileNameWithoutExtension(proto);
+      return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep");
+    }
+
+    // Get a 64-bit hash for a directory string. We treat it as if it were
+    // unique, since there are not so many distinct proto paths in a project.
+    // We take the first 64 bit of the string SHA1.
+    // Internal for tests access only.
+    internal static string HashString64Hex(string str) {
+      using (var sha1 = System.Security.Cryptography.SHA1.Create()) {
+        byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(str));
+        var hashstr = new StringBuilder(16);
+        for (int i = 0; i < 8; i++) {
+          hashstr.Append(hash[i].ToString("x2"));
+        }
+        return hashstr.ToString();
+      }
+    }
+
+    // Extract filename between 'beg' (inclusive) and 'end' (exclusive) from
+    // line 'line', skipping over trailing and leading whitespace, and, when
+    // 'end' is immediately past end of line 'line', also final '\' (used
+    // as a line continuation token in the dep file).
+    // Returns an empty string if the filename cannot be extracted.
+    static string ExtractFilenameFromLine(string line, int beg, int end) {
+      while (beg < end && char.IsWhiteSpace(line[beg])) beg++;
+      if (beg < end && end == line.Length && line[end - 1] == '\\') end--;
+      while (beg < end && char.IsWhiteSpace(line[end - 1])) end--;
+      if (beg == end) return "";
+
+      string filename = line.Substring(beg, end - beg);
+      try {
+        // Normalize file name.
+        return Path.Combine(
+          Path.GetDirectoryName(filename),
+          Path.GetFileName(filename));
+      } catch (Exception ex) when (Exceptions.IsIoRelated(ex)) {
+        return "";
+      }
+    }
+
+    // Finds the index of the ':' separating dependency clauses in the line,
+    // not taking Windows drive spec into account. Returns the index of the
+    // separating ':', or -1 if no separator found.
+    static int FindLineSeparator(string line) {
+      // Mind this case where the first ':' is not separator:
+      // C:\foo\bar\.pb.h: C:/protobuf/wrappers.proto\
+      int ix = line.IndexOf(':');
+      if (ix <= 0 || ix == line.Length - 1
+          || (line[ix + 1] != '/' && line[ix + 1] != '\\')
+          || !char.IsLetter(line[ix - 1]))
+        return ix;  // Not a windows drive: no letter before ':', or no '\' after.
+      for (int j = ix - 1; --j >= 0;) {
+        if (!char.IsWhiteSpace(line[j]))
+          return ix;  // Not space or BOL only before "X:/".
+      }
+      return line.IndexOf(':', ix + 1);
+    }
+
+    // Read entire dependency file. The 'required' parameter controls error
+    // logging behavior in case the file not found. We require this file when
+    // compiling, but reading it is optional when computing depnedencies.
+    static string[] ReadDepFileLines(string filename, bool required,
+                                     TaskLoggingHelper log) {
+      try {
+        var result = File.ReadAllLines(filename);
+        if (!required)
+          log.LogMessage(MessageImportance.Low, $"Using dependency file {filename}");
+        return result;
+      } catch (Exception ex) when (Exceptions.IsIoRelated(ex)) {
+        if (required) {
+          log.LogError($"Unable to load {filename}: {ex.GetType().Name}: {ex.Message}");
+        } else {
+          log.LogMessage(MessageImportance.Low, $"Skippping {filename}: {ex.Message}");
+        }
+        return new string[0];
+      }
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools/GeneratorServices.cs b/src/csharp/Grpc.Tools/GeneratorServices.cs
new file mode 100644
index 0000000000000000000000000000000000000000..e1f266aa16ebe02b5c57cf9f8aedd52bd0bbf640
--- /dev/null
+++ b/src/csharp/Grpc.Tools/GeneratorServices.cs
@@ -0,0 +1,168 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#endregion
+
+using System;
+using System.IO;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools {
+  // Abstract class for language-specific analysis behavior, such
+  // as guessing the generated files the same way protoc does.
+  internal abstract class GeneratorServices {
+    protected readonly TaskLoggingHelper Log;
+    protected GeneratorServices(TaskLoggingHelper log) {
+      Log = log;
+    }
+
+    // Obtain a service for the given language (csharp, cpp).
+    public static GeneratorServices GetForLanguage(string lang, TaskLoggingHelper log) {
+      if (lang.EqualNoCase("csharp"))
+        return new CSharpGeneratorServices(log);
+      if (lang.EqualNoCase("cpp"))
+        return new CppGeneratorServices(log);
+      log.LogError("Invalid value '{0}' for task property 'Generator'. " +
+        "Supported generator languages: CSharp, Cpp.", lang);
+      return null;
+    }
+
+    // Guess whether item's metadata suggests gRPC stub generation.
+    // When "gRPCServices" is not defined, assume gRPC is not used.
+    // When defined, C# uses "none" to skip gRPC, C++ uses "false", so
+    // recognize both. Since the value is tightly coupled to the scripts,
+    // we do not try to validate the value; scripts take care of that.
+    // It is safe to assume that gRPC is requested for any other value.
+    protected bool GrpcOutputPossible(ITaskItem proto) {
+      string gsm = proto.GetMetadata(Metadata.kGrpcServices);
+      return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none")
+          && !gsm.EqualNoCase("false");
+    }
+
+    public abstract string[] GetPossibleOutputs(ITaskItem proto);
+  };
+
+  // C# generator services.
+  internal class CSharpGeneratorServices : GeneratorServices {
+    public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) {}
+
+    public override string[] GetPossibleOutputs(ITaskItem protoItem) {
+      bool doGrpc = GrpcOutputPossible(protoItem);
+      string filename = LowerUnderscoreToUpperCamel(
+        Path.GetFileNameWithoutExtension(protoItem.ItemSpec));
+
+      var outputs = new string[doGrpc ? 2 : 1];
+      string outdir = protoItem.GetMetadata(Metadata.kOutputDir);
+      string fileStem = Path.Combine(outdir, filename);
+      outputs[0] = fileStem + ".cs";
+      if (doGrpc) {
+        // Override outdir if kGrpcOutputDir present, default to proto output.
+        outdir = protoItem.GetMetadata(Metadata.kGrpcOutputDir);
+        if (outdir != "") {
+          fileStem = Path.Combine(outdir, filename);
+        }
+        outputs[1] = fileStem + "Grpc.cs";
+      }
+      return outputs;
+    }
+
+    string LowerUnderscoreToUpperCamel(string str) {
+      // See src/compiler/generator_helpers.h:118
+      var result = new StringBuilder(str.Length, str.Length);
+      bool cap = true;
+      foreach (char c in str) {
+        if (c == '_') {
+          cap = true;
+        } else if (cap) {
+          result.Append(char.ToUpperInvariant(c));
+          cap = false;
+        } else {
+          result.Append(c);
+        }
+      }
+      return result.ToString();
+    }
+  };
+
+  // C++ generator services.
+  internal class CppGeneratorServices : GeneratorServices {
+    public CppGeneratorServices(TaskLoggingHelper log) : base(log) { }
+
+    public override string[] GetPossibleOutputs(ITaskItem protoItem) {
+      bool doGrpc = GrpcOutputPossible(protoItem);
+      string root = protoItem.GetMetadata(Metadata.kProtoRoot);
+      string proto = protoItem.ItemSpec;
+      string filename = Path.GetFileNameWithoutExtension(proto);
+      // E. g., ("foo/", "foo/bar/x.proto") => "bar"
+      string relative = GetRelativeDir(root, proto);
+
+      var outputs = new string[doGrpc ? 4 : 2];
+      string outdir = protoItem.GetMetadata(Metadata.kOutputDir);
+      string fileStem = Path.Combine(outdir, relative, filename);
+      outputs[0] = fileStem + ".pb.cc";
+      outputs[1] = fileStem + ".pb.h";
+      if (doGrpc) {
+        // Override outdir if kGrpcOutputDir present, default to proto output.
+        outdir = protoItem.GetMetadata(Metadata.kGrpcOutputDir);
+        if (outdir != "") {
+          fileStem = Path.Combine(outdir, relative, filename);
+        }
+        outputs[2] = fileStem + "_grpc.pb.cc";
+        outputs[3] = fileStem + "_grpc.pb.h";
+      }
+      return outputs;
+    }
+
+    // Calculate part of proto path relative to root. Protoc is very picky
+    // about them matching exactly, so can be we. Expect root be exact prefix
+    // to proto, minus some slash normalization.
+    string GetRelativeDir(string root, string proto) {
+      string protoDir = Path.GetDirectoryName(proto);
+      string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root)));
+      if (rootDir == s_dotSlash) {
+        // Special case, otherwise we can return "./" instead of "" below!
+        return protoDir;
+      }
+      if (Platform.IsFsCaseInsensitive) {
+        protoDir = protoDir.ToLowerInvariant();
+        rootDir = rootDir.ToLowerInvariant();
+      }
+      protoDir = EndWithSlash(protoDir);
+      if (!protoDir.StartsWith(rootDir)) {
+        Log.LogWarning("ProtoBuf item '{0}' has the ProtoRoot metadata '{1}' " +
+          "which is not prefix to its path. Cannot compute relative path.",
+          proto, root);
+        return "";
+      }
+      return protoDir.Substring(rootDir.Length);
+    }
+
+    // './' or '.\', normalized per system.
+    static string s_dotSlash = "." + Path.DirectorySeparatorChar;
+
+    static string EndWithSlash(string str) {
+      if (str == "") {
+        return s_dotSlash;
+      } else if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/') {
+        return str + Path.DirectorySeparatorChar;
+      } else {
+        return str;
+      }
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools/Grpc.Tools.csproj b/src/csharp/Grpc.Tools/Grpc.Tools.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..46a6d4670d25a3f468f5c4353b240ea6eda54a1d
--- /dev/null
+++ b/src/csharp/Grpc.Tools/Grpc.Tools.csproj
@@ -0,0 +1,95 @@
+<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+
+  <Import Project="..\Grpc.Core\Version.csproj.include" />
+
+  <PropertyGroup>
+    <AssemblyName>Protobuf.MSBuild</AssemblyName>
+    <Version>$(GrpcCsharpVersion)</Version>
+    <!-- If changing targets, change also paths in Google.Protobuf.Tools.targets. -->
+    <TargetFrameworks>netstandard1.3;net40</TargetFrameworks>
+  </PropertyGroup>
+
+  <PropertyGroup Label="Asset root folders. TODO(kkm): Change with package separation.">
+    <!-- TODO(kkm): Rework whole section when splitting packages.  -->
+    <!-- GRPC: ../../third_party/protobuf/src/google/protobuf/  -->
+    <!-- GPB:  ../src/google/protobuf/ -->
+    <Assets_ProtoInclude>../../../third_party/protobuf/src/google/protobuf/</Assets_ProtoInclude>
+
+    <!-- GPB:  protoc\ -->
+    <!-- GRPC: protoc_plugins\protoc_ -->
+    <Assets_ProtoCompiler>../protoc_plugins/protoc_</Assets_ProtoCompiler>
+
+    <!-- GRPC: protoc_plugins\ -->
+    <Assets_GrpcPlugins>../protoc_plugins/</Assets_GrpcPlugins>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <_NetStandard>False</_NetStandard>
+    <_NetStandard Condition=" $(TargetFramework.StartsWith('netstandard')) or $(TargetFramework.StartsWith('netcore')) ">True</_NetStandard>
+
+    <!-- So we do not hardcode an exact version into #if's. -->
+    <DefineConstants Condition="$(_NetStandard)">$(DefineConstants);NETSTANDARD</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Label="NuGet package definition" Condition=" '$(Configuration)' == 'Release' ">
+    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+    <PackageOutputPath>../../../artifacts</PackageOutputPath>
+
+    <!-- TODO(kkm): Change to "build\" after splitting. -->
+    <BuildOutputTargetFolder>build\_protobuf\</BuildOutputTargetFolder>
+    <DevelopmentDependency>true</DevelopmentDependency>
+    <NoPackageAnalysis>true</NoPackageAnalysis>
+    <PackageId>Grpc.Tools</PackageId>
+    <Description>gRPC and Protocol Buffer compiler for managed C# and native C++ projects.
+
+Add this package to a project that contains .proto files to be compiled to code.
+It contains the compilers, include files and project system integration for gRPC
+and Protocol buffer service description files necessary to build them on Windows,
+Linux and MacOS. Managed runtime is supplied separately in the Grpc.Core package.</Description>
+    <Copyright>Copyright 2018 gRPC authors</Copyright>
+    <Authors>gRPC authors</Authors>
+    <PackageLicenseUrl>https://github.com/grpc/grpc/blob/master/LICENSE</PackageLicenseUrl>
+    <PackageProjectUrl>https://github.com/grpc/grpc</PackageProjectUrl>
+    <PackageTags>gRPC RPC protocol HTTP/2</PackageTags>
+  </PropertyGroup>
+
+  <ItemGroup Label="NuGet package assets">
+    <None Pack="true" PackagePath="build\" Include="build\**\*.xml; build\**\*.props; build\**\*.targets;" />
+
+    <!-- Protobuf assets (for Google.Protobuf.Tools) -->
+    <_ProtoTemp Include="any.proto;api.proto;descriptor.proto;duration.proto;" />
+    <_ProtoTemp Include="empty.proto;field_mask.proto;source_context.proto;" />
+    <_ProtoTemp Include="struct.proto;timestamp.proto;type.proto;wrappers.proto" />
+    <_Asset PackagePath="build/native/include/google/protobuf/" Include="@(_ProtoTemp->'$(Assets_ProtoInclude)%(Identity)')" />
+
+    <!-- TODO(kkm): GPB builds assets into "macosx", GRPC into "macos". -->
+    <_Asset PackagePath="build/native/bin/windows/protoc.exe" Include="$(Assets_ProtoCompiler)windows_x86/protoc.exe" />
+    <_Asset PackagePath="build/native/bin/linux_x86/protoc" Include="$(Assets_ProtoCompiler)linux_x86/protoc" />
+    <_Asset PackagePath="build/native/bin/linux_x64/protoc" Include="$(Assets_ProtoCompiler)linux_x64/protoc" />
+    <_Asset PackagePath="build/native/bin/macosx_x86/protoc" Include="$(Assets_ProtoCompiler)macos_x86/protoc" /> <!-- GPB: macosx-->
+    <_Asset PackagePath="build/native/bin/macosx_x64/protoc" Include="$(Assets_ProtoCompiler)macos_x64/protoc" /> <!-- GPB: macosx-->
+
+    <!-- gRPC assets (for Grpc.Tools) -->
+    <_Asset PackagePath="build/native/bin/windows/grpc_csharp_plugin.exe" Include="$(Assets_GrpcPlugins)protoc_windows_x86/grpc_csharp_plugin.exe" />
+    <_Asset PackagePath="build/native/bin/linux_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x86/grpc_csharp_plugin" />
+    <_Asset PackagePath="build/native/bin/linux_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x64/grpc_csharp_plugin" />
+    <_Asset PackagePath="build/native/bin/macosx_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x86/grpc_csharp_plugin" />
+    <_Asset PackagePath="build/native/bin/macosx_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x64/grpc_csharp_plugin" />
+
+    <None Include="@(_Asset)" Pack="true" Visible="false" />
+  </ItemGroup>
+
+  <ItemGroup Condition="!$(_NetStandard)">
+    <Reference Include="Microsoft.Build.Framework" Pack="false" />
+    <Reference Include="Microsoft.Build.Utilities.v4.0" Pack="false" />
+  </ItemGroup>
+
+  <ItemGroup Condition="$(_NetStandard)">
+    <PackageReference Include="Microsoft.Build.Framework" Version="15.5.180" />
+    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.5.180" />
+    <!--  Set PrivateAssets="All" on all items, so that even implicit package
+          dependencies do not become dependencies of this package. -->
+    <PackageReference Update="@(PackageReference)" PrivateAssets="All" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs
new file mode 100644
index 0000000000000000000000000000000000000000..76c2338ef961381ed809f524d995c0fb74fcd99c
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoCompile.cs
@@ -0,0 +1,409 @@
+#region Copyright notice and license
+
+// Copyright 2018 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.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools {
+  /// <summary>
+  /// Run Google proto compiler (protoc).
+  ///
+  /// After a successful run, the task reads the dependency file if specified
+  /// to be saved by the compiler, and returns its output files.
+  ///
+  /// This task (unlike PrepareProtoCompile) does not attempt to guess anything
+  /// about language-specific behavior of protoc, and therefore can be used for
+  /// any language outputs.
+  /// </summary>
+  public class ProtoCompile : ToolTask {
+/*
+
+Usage: /home/kkm/work/protobuf/src/.libs/lt-protoc [OPTION] PROTO_FILES
+Parse PROTO_FILES and generate output based on the options given:
+  -IPATH, --proto_path=PATH   Specify the directory in which to search for
+                              imports.  May be specified multiple times;
+                              directories will be searched in order.  If not
+                              given, the current working directory is used.
+  --version                   Show version info and exit.
+  -h, --help                  Show this text and exit.
+  --encode=MESSAGE_TYPE       Read a text-format message of the given type
+                              from standard input and write it in binary
+                              to standard output.  The message type must
+                              be defined in PROTO_FILES or their imports.
+  --decode=MESSAGE_TYPE       Read a binary message of the given type from
+                              standard input and write it in text format
+                              to standard output.  The message type must
+                              be defined in PROTO_FILES or their imports.
+  --decode_raw                Read an arbitrary protocol message from
+                              standard input and write the raw tag/value
+                              pairs in text format to standard output.  No
+                              PROTO_FILES should be given when using this
+                              flag.
+  --descriptor_set_in=FILES   Specifies a delimited list of FILES
+                              each containing a FileDescriptorSet (a
+                              protocol buffer defined in descriptor.proto).
+                              The FileDescriptor for each of the PROTO_FILES
+                              provided will be loaded from these
+                              FileDescriptorSets. If a FileDescriptor
+                              appears multiple times, the first occurrence
+                              will be used.
+  -oFILE,                     Writes a FileDescriptorSet (a protocol buffer,
+    --descriptor_set_out=FILE defined in descriptor.proto) containing all of
+                              the input files to FILE.
+  --include_imports           When using --descriptor_set_out, also include
+                              all dependencies of the input files in the
+                              set, so that the set is self-contained.
+  --include_source_info       When using --descriptor_set_out, do not strip
+                              SourceCodeInfo from the FileDescriptorProto.
+                              This results in vastly larger descriptors that
+                              include information about the original
+                              location of each decl in the source file as
+                              well as surrounding comments.
+  --dependency_out=FILE       Write a dependency output file in the format
+                              expected by make. This writes the transitive
+                              set of input file paths to FILE
+  --error_format=FORMAT       Set the format in which to print errors.
+                              FORMAT may be 'gcc' (the default) or 'msvs'
+                              (Microsoft Visual Studio format).
+  --print_free_field_numbers  Print the free field numbers of the messages
+                              defined in the given proto files. Groups share
+                              the same field number space with the parent
+                              message. Extension ranges are counted as
+                              occupied fields numbers.
+
+  --plugin=EXECUTABLE         Specifies a plugin executable to use.
+                              Normally, protoc searches the PATH for
+                              plugins, but you may specify additional
+                              executables not in the path using this flag.
+                              Additionally, EXECUTABLE may be of the form
+                              NAME=PATH, in which case the given plugin name
+                              is mapped to the given executable even if
+                              the executable's own name differs.
+  --cpp_out=OUT_DIR           Generate C++ header and source.
+  --csharp_out=OUT_DIR        Generate C# source file.
+  --java_out=OUT_DIR          Generate Java source file.
+  --javanano_out=OUT_DIR      Generate Java Nano source file.
+  --js_out=OUT_DIR            Generate JavaScript source.
+  --objc_out=OUT_DIR          Generate Objective C header and source.
+  --php_out=OUT_DIR           Generate PHP source file.
+  --python_out=OUT_DIR        Generate Python source file.
+  --ruby_out=OUT_DIR          Generate Ruby source file.
+  @<filename>                 Read options and filenames from file. If a
+                              relative file path is specified, the file
+                              will be searched in the working directory.
+                              The --proto_path option will not affect how
+                              this argument file is searched. Content of
+                              the file will be expanded in the position of
+                              @<filename> as in the argument list. Note
+                              that shell expansion is not applied to the
+                              content of the file (i.e., you cannot use
+                              quotes, wildcards, escapes, commands, etc.).
+                              Each line corresponds to a single argument,
+                              even if it contains spaces.
+*/
+    static string[] s_supportedGenerators = new[] {
+      "cpp", "csharp", "java",
+      "javanano", "js", "objc",
+      "php", "python", "ruby",
+    };
+
+    /// <summary>
+    /// Code generator.
+    /// </summary>
+    [Required]
+    public string Generator { get; set; }
+
+    /// <summary>
+    /// Protobuf files to compile.
+    /// </summary>
+    [Required]
+    public ITaskItem[] ProtoBuf { get; set; }
+
+    /// <summary>
+    /// Directory where protoc dependency files are cached. If provided, dependency
+    /// output filename is autogenerated from source directory hash and file name.
+    /// Mutually exclusive with DependencyOut.
+    /// Switch: --dependency_out (with autogenerated file name).
+    /// </summary>
+    public string ProtoDepDir { get; set; }
+
+    /// <summary>
+    /// Dependency file full name. Mutually exclusive with ProtoDepDir.
+    /// Autogenerated file name is available in this property after execution.
+    /// Switch: --dependency_out.
+    /// </summary>
+    [Output]
+    public string DependencyOut { get; set; }
+
+    /// <summary>
+    /// The directories to search for imports. Directories will be searched
+    /// in order. If not given, the current working directory is used.
+    /// Switch: --proto_path.
+    /// </summary>
+    public string[] ProtoPath { get; set; }
+
+    /// <summary>
+    /// Generated code directory. The generator property determines the language.
+    /// Switch: --GEN-out= (for different generators GEN).
+    /// </summary>
+    [Required]
+    public string OutputDir { get; set; }
+
+    /// <summary>
+    /// Codegen options. See also OptionsFromMetadata.
+    /// Switch: --GEN_out= (for different generators GEN).
+    /// </summary>
+    public string[] OutputOptions { get; set; }
+
+    /// <summary>
+    /// Full path to the gRPC plugin executable. If specified, gRPC generation
+    /// is enabled for the files.
+    /// Switch: --plugin=protoc-gen-grpc=
+    /// </summary>
+    public string GrpcPluginExe { get; set; }
+
+    /// <summary>
+    /// Generated gRPC  directory. The generator property determines the
+    /// language. If gRPC is enabled but this is not given, OutputDir is used.
+    /// Switch: --grpc_out=
+    /// </summary>
+    public string GrpcOutputDir { get; set; }
+
+    /// <summary>
+    /// gRPC Codegen options. See also OptionsFromMetadata.
+    /// --grpc_opt=opt1,opt2=val (comma-separated).
+    /// </summary>
+    public string[] GrpcOutputOptions { get; set; }
+
+    /// <summary>
+    /// List of files written in addition to generated outputs. Includes a
+    /// single item for the dependency file if written.
+    /// </summary>
+    [Output]
+    public ITaskItem[] AdditionalFileWrites { get; private set; }
+
+    /// <summary>
+    /// List of language files generated by protoc. Empty unless DependencyOut
+    /// or ProtoDepDir is set, since the file writes are extracted from protoc
+    /// dependency output file.
+    /// </summary>
+    [Output]
+    public ITaskItem[] GeneratedFiles { get; private set; }
+
+    // Hide this property from MSBuild, we should never use a shell script.
+    private new bool UseCommandProcessor { get; set; }
+
+    protected override string ToolName =>
+      Platform.IsWindows ? "protoc.exe" : "protoc";
+
+    // Since we never try to really locate protoc.exe somehow, just try ToolExe
+    // as the full tool location. It will be either just protoc[.exe] from
+    // ToolName above if not set by the user, or a user-supplied full path. The
+    // base class will then resolve the former using system PATH.
+    protected override string GenerateFullPathToTool() => ToolExe;
+
+    // Log protoc errors with the High priority (bold white in MsBuild,
+    // printed with -v:n, and shown in the Output windows in VS).
+    protected override MessageImportance StandardErrorLoggingImportance =>
+      MessageImportance.High;
+
+    // Called by base class to validate arguments and make them consistent.
+    protected override bool ValidateParameters() {
+      // Part of proto command line switches, must be lowercased.
+      Generator = Generator.ToLowerInvariant();
+      if (!System.Array.Exists(s_supportedGenerators, g => g == Generator))
+        Log.LogError("Invalid value for Generator='{0}'. Supported generators: {1}",
+                     Generator, string.Join(", ", s_supportedGenerators));
+
+      if (ProtoDepDir != null && DependencyOut != null)
+        Log.LogError("Properties ProtoDepDir and DependencyOut may not be both specified");
+
+      if (ProtoBuf.Length > 1 && (ProtoDepDir != null || DependencyOut != null))
+        Log.LogError("Proto compiler currently allows only one input when " +
+                     "--dependency_out is specified (via ProtoDepDir or DependencyOut). " +
+                     "Tracking issue: https://github.com/google/protobuf/pull/3959");
+
+      // Use ProtoDepDir to autogenerate DependencyOut
+      if (ProtoDepDir != null) {
+        DependencyOut = DepFileUtil.GetDepFilenameForProto(ProtoDepDir, ProtoBuf[0].ItemSpec);
+      }
+
+      if (GrpcPluginExe == null) {
+        GrpcOutputOptions = null;
+        GrpcOutputDir = null;
+      } else if (GrpcOutputDir == null) {
+        // Use OutputDir for gRPC output if not specified otherwise by user.
+        GrpcOutputDir = OutputDir;
+      }
+
+      return !Log.HasLoggedErrors && base.ValidateParameters();
+    }
+
+    // Protoc chokes on BOM, naturally. I would!
+    static readonly Encoding s_utf8WithoutBom = new UTF8Encoding(false);
+    protected override Encoding ResponseFileEncoding => s_utf8WithoutBom;
+
+    // Protoc takes one argument per line from the response file, and does not
+    // require any quoting whatsoever. Otherwise, this is similar to the
+    // standard CommandLineBuilder
+    class ProtocResponseFileBuilder {
+      StringBuilder _data = new StringBuilder(1000);
+      public override string ToString() => _data.ToString();
+
+      // If 'value' is not empty, append '--name=value\n'.
+      public void AddSwitchMaybe(string name, string value) {
+        if (!string.IsNullOrEmpty(value)) {
+          _data.Append("--").Append(name).Append("=")
+               .Append(value).Append('\n');
+        }
+      }
+
+      // Add switch with the 'values' separated by commas, for options.
+      public void AddSwitchMaybe(string name, string[] values) {
+        if (values?.Length > 0) {
+          _data.Append("--").Append(name).Append("=")
+               .Append(string.Join(",", values)).Append('\n');
+        }
+      }
+
+      // Add a positional argument to the file data.
+      public void AddArg(string arg) {
+        _data.Append(arg).Append('\n');
+      }
+    };
+
+    // Called by the base ToolTask to get response file contents.
+    protected override string GenerateResponseFileCommands() {
+      var cmd = new ProtocResponseFileBuilder();
+      cmd.AddSwitchMaybe(Generator + "_out", TrimEndSlash(OutputDir));
+      cmd.AddSwitchMaybe(Generator + "_opt", OutputOptions);
+      cmd.AddSwitchMaybe("plugin=protoc-gen-grpc", GrpcPluginExe);
+      cmd.AddSwitchMaybe("grpc_out", TrimEndSlash(GrpcOutputDir));
+      cmd.AddSwitchMaybe("grpc_opt", GrpcOutputOptions);
+      if (ProtoPath != null) {
+        foreach (string path in ProtoPath)
+          cmd.AddSwitchMaybe("proto_path", TrimEndSlash(path));
+      }
+      cmd.AddSwitchMaybe("dependency_out", DependencyOut);
+      foreach (var proto in ProtoBuf) {
+        cmd.AddArg(proto.ItemSpec);
+      }
+      return cmd.ToString();
+    }
+
+    // Protoc cannot digest trailing slashes in directory names,
+    // curiously under Linux, but not in Windows.
+    static string TrimEndSlash(string dir) {
+      if (dir == null || dir.Length <= 1) {
+        return dir;
+      }
+      string trim = dir.TrimEnd('/', '\\');
+      // Do not trim the root slash, drive letter possible.
+      if (trim.Length == 0) {
+        // Slashes all the way down.
+        return dir.Substring(0, 1);
+      }
+      if (trim.Length == 2 && dir.Length > 2 && trim[1] == ':') {
+        // We have a drive letter and root, e. g. 'C:\'
+        return dir.Substring(0, 3);
+      }
+      return trim;
+    }
+
+    // Called by the base class to log tool's command line.
+    //
+    // Protoc command file is peculiar, with one argument per line, separated
+    // by newlines. Unwrap it for log readability into a single line, and also
+    // quote arguments, lest it look weird and so it may be copied and pasted
+    // into shell. Since this is for logging only, correct enough is correct.
+    protected override void LogToolCommand(string cmd) {
+      var printer = new StringBuilder(1024);
+
+      // Print 'str' slice into 'printer', wrapping in quotes if contains some
+      // interesting characters in file names, or if empty string. The list of
+      // characters requiring quoting is not by any means exhaustive; we are
+      // just striving to be nice, not guaranteeing to be nice.
+      var quotable = new[] { ' ', '!', '$', '&', '\'', '^' };
+      void PrintQuoting(string str, int start, int count) {
+        bool wrap = count == 0 || str.IndexOfAny(quotable, start, count) >= 0;
+        if (wrap) printer.Append('"');
+        printer.Append(str, start, count);
+        if (wrap) printer.Append('"');
+      }
+
+      for (int ib = 0, ie; (ie = cmd.IndexOf('\n', ib)) >= 0; ib = ie + 1) {
+        // First line only contains both the program name and the first switch.
+        // We can rely on at least the '--out_dir' switch being always present.
+        if (ib == 0) {
+          int iep = cmd.IndexOf(" --");
+          if (iep > 0) {
+            PrintQuoting(cmd, 0, iep);
+            ib = iep + 1;
+          }
+        }
+        printer.Append(' ');
+        if (cmd[ib] == '-') {
+          // Print switch unquoted, including '=' if any.
+          int iarg = cmd.IndexOf('=', ib, ie - ib);
+          if (iarg < 0) {
+            // Bare switch without a '='.
+            printer.Append(cmd, ib, ie - ib);
+            continue;
+          }
+          printer.Append(cmd, ib, iarg + 1 - ib);
+          ib = iarg + 1;
+        }
+        // A positional argument or switch value.
+        PrintQuoting(cmd, ib, ie - ib);
+      }
+
+      base.LogToolCommand(printer.ToString());
+    }
+
+    // Main task entry point.
+    public override bool Execute() {
+      base.UseCommandProcessor = false;
+
+      bool ok = base.Execute();
+      if (!ok) {
+        return false;
+      }
+
+      // Read dependency output file from the compiler to retrieve the
+      // definitive list of created files. Report the dependency file
+      // itself as having been written to.
+      if (DependencyOut != null) {
+        string[] outputs = DepFileUtil.ReadDependencyOutputs(DependencyOut, Log);
+        if (HasLoggedErrors) {
+          return false;
+        }
+
+        GeneratedFiles = new ITaskItem[outputs.Length];
+        for (int i = 0; i < outputs.Length; i++) {
+          GeneratedFiles[i] = new TaskItem(outputs[i]);
+        }
+        AdditionalFileWrites = new ITaskItem[] {
+          new TaskItem(DependencyOut)
+        };
+      }
+
+      return true;
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs
new file mode 100644
index 0000000000000000000000000000000000000000..9afea9255e50bb8ca670305745bdf3b61cdb2d1d
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs
@@ -0,0 +1,80 @@
+#region Copyright notice and license
+
+// Copyright 2018 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.Collections.Generic;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools {
+  public class ProtoCompilerOutputs : Task {
+    /// <summary>
+    /// Code generator. Currently supported are "csharp", "cpp".
+    /// </summary>
+    [Required]
+    public string Generator { get; set; }
+
+    /// <summary>
+    /// All Proto files in the project. The task computes possible outputs
+    /// from these proto files, and returns them in the PossibleOutputs list.
+    /// Not all of these might be actually produced by protoc; this is dealt
+    /// with later in the ProtoCompile task which returns the list of
+    /// files actually produced by the compiler.
+    /// </summary>
+    [Required]
+    public ITaskItem[] ProtoBuf { get; set; }
+
+    /// <summary>
+    /// Output items per each potential output. We do not look at existing
+    /// cached dependency even if they exist, since file may be refactored,
+    /// affecting whether or not gRPC code file is generated from a given proto.
+    /// Instead, all potentially possible generated sources are collected.
+    /// It is a wise idea to generate empty files later for those potentials
+    /// that are not actually created by protoc, so the dependency checks
+    /// result in a minimal recompilation. The Protoc task can output the
+    /// list of files it actually produces, given right combination of its
+    /// properties.
+    /// Output items will have the Source metadata set on them:
+    ///     <ItemName Include="MyProto.cs" Source="my_proto.proto" />
+    /// </summary>
+    [Output]
+    public ITaskItem[] PossibleOutputs { get; private set; }
+
+    public override bool Execute() {
+      var generator = GeneratorServices.GetForLanguage(Generator, Log);
+      if (generator == null) {
+        // Error already logged, just return.
+        return false;
+      }
+
+      // Get language-specific possible output. The generator expects certain
+      // metadata be set on the proto item.
+      var possible = new List<ITaskItem>();
+      foreach (var proto in ProtoBuf) {
+        var outputs = generator.GetPossibleOutputs(proto);
+        foreach (string output in outputs) {
+          var ti = new TaskItem(output);
+          ti.SetMetadata(Metadata.kSource, proto.ItemSpec);
+          possible.Add(ti);
+        }
+      }
+      PossibleOutputs = possible.ToArray();
+
+      return !Log.HasLoggedErrors;
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools/ProtoReadDependencies.cs b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs
new file mode 100644
index 0000000000000000000000000000000000000000..2ee038914699b64c9b9199fee1a4c51faf0afb37
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs
@@ -0,0 +1,70 @@
+#region Copyright notice and license
+
+// Copyright 2018 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.Collections.Generic;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools {
+  public class ProtoReadDependencies : Task {
+    /// <summary>
+    /// The collection is used to collect possible additional dependencies
+    /// of proto files cached under ProtoDepDir.
+    /// </summary>
+    [Required]
+    public ITaskItem[] ProtoBuf { get; set; }
+
+    /// <summary>
+    /// Directory where protoc dependency files are cached.
+    /// </summary>
+    [Required]
+    public string ProtoDepDir { get; set; }
+
+    /// <summary>
+    /// Additional items that a proto file depends on. This list may include
+    /// extra dependencies; we do our best to include as few extra positives
+    /// as reasonable to avoid missing any. The collection item is the
+    /// dependency, and its Source metadatum is the dependent proto file, like
+    ///     <ItemName Include="/usr/include/proto/wrapper.proto"
+    ///               Source="my_proto.proto" />
+    /// </summary>
+    [Output]
+    public ITaskItem[] Dependencies { get; private set; }
+
+    public override bool Execute() {
+      // Read dependency files, where available. There might be none,
+      // just use a best effort.
+      if (ProtoDepDir != null) {
+       var dependencies = new List<ITaskItem>();
+       foreach (var proto in ProtoBuf) {
+          string[] deps = DepFileUtil.ReadDependencyInputs(ProtoDepDir, proto.ItemSpec, Log);
+          foreach (string dep in deps) {
+            var ti = new TaskItem(dep);
+            ti.SetMetadata(Metadata.kSource, proto.ItemSpec);
+            dependencies.Add(ti);
+          }
+        }
+        Dependencies = dependencies.ToArray();
+      } else {
+        Dependencies = new ITaskItem[0];
+      }
+
+      return !Log.HasLoggedErrors;
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f505b86fe41a67b7358a7df2378382fb4c5ea2d8
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs
@@ -0,0 +1,58 @@
+#region Copyright notice and license
+
+// Copyright 2018 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 Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools {
+  /// <summary>
+  /// A helper task to resolve actual OS type and bitness.
+  /// </summary>
+  public class ProtoToolsPlatform : Task {
+    /// <summary>
+    /// Return one of 'linux', 'macosx' or 'windows'.
+    /// If the OS is unknown, the property is not set.
+    /// </summary>
+    [Output]
+    public string Os { get; set; }
+
+    /// <summary>
+    /// Return one of 'x64' or 'x86'.
+    /// If the CPU is unknown, the property is not set.
+    /// </summary>
+    [Output]
+    public string Cpu { get; set; }
+
+
+    public override bool Execute() {
+      switch (Platform.Os) {
+        case Platform.OsKind.Linux: Os = "linux"; break;
+        case Platform.OsKind.MacOsX: Os = "macosx"; break;
+        case Platform.OsKind.Windows: Os = "windows"; break;
+        default: Os = ""; break;
+      }
+
+      switch (Platform.Cpu) {
+        case Platform.CpuKind.X86: Cpu = "x86"; break;
+        case Platform.CpuKind.X64: Cpu = "x64"; break;
+        default: Cpu = ""; break;
+      }
+      return true;
+    }
+  };
+}
diff --git a/src/csharp/Grpc.Tools/build/Grpc.Tools.props b/src/csharp/Grpc.Tools/build/Grpc.Tools.props
new file mode 100644
index 0000000000000000000000000000000000000000..dbcd8bf494bc6954500a1e6ccc047be8036122d9
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.props
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+  </PropertyGroup>
+
+  <!-- Name of this file must match package ID. -->
+  <!-- Packages will be split later. -->
+  <Import Project="_grpc/_Grpc.Tools.props"/>
+  <Import Project="_protobuf/Google.Protobuf.Tools.props"/>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/Grpc.Tools.targets b/src/csharp/Grpc.Tools/build/Grpc.Tools.targets
new file mode 100644
index 0000000000000000000000000000000000000000..c0a5b1e2c5d703c8decff86f7b584aa77b0b5abd
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.targets
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+  </PropertyGroup>
+
+  <!-- Name of this file must match package ID. -->
+  <!-- Packages will be split later. -->
+  <Import Project="_grpc/_Grpc.Tools.targets"/>
+  <Import Project="_protobuf/Google.Protobuf.Tools.targets"/>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml b/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..54468eb5eff82cb6fca4e4f46932f0d4c3540a47
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml
@@ -0,0 +1,30 @@
+<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
+  <Rule Name="ProtoBuf"
+        DisplayName="File Properties"
+        PageTemplate="generic"
+        Description="File Properties"
+        OverrideMode="Extend">
+    <Rule.DataSource>
+      <DataSource Persistence="ProjectFile" Label="Configuration" ItemType="ProtoBuf"
+                  HasConfigurationCondition="false" SourceOfDefaultValue="AfterContext" />
+    </Rule.DataSource>
+
+    <Rule.Categories>
+      <Category Name="gRPC" DisplayName="gRPC" />
+    </Rule.Categories>
+
+    <EnumProperty Name="GrpcServices" DisplayName="gRPC Stub Classes"
+                  Category="gRPC" Default="Both"
+                  Description="Generate gRPC server and client stub classes.">
+      <EnumValue Name="Both" DisplayName="Client and Server" IsDefault="true" />
+      <EnumValue Name="Client" DisplayName="Client only" />
+      <EnumValue Name="Server" DisplayName="Server only" />
+      <EnumValue Name="None" DisplayName="Do not generate" />
+      <EnumProperty.DataSource>
+        <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext"
+                    PersistenceStyle="Attribute" />
+      </EnumProperty.DataSource>
+    </EnumProperty>
+
+  </Rule>
+</ProjectSchemaDefinitions>
diff --git a/src/csharp/Grpc.Tools/build/_grpc/README b/src/csharp/Grpc.Tools/build/_grpc/README
new file mode 100644
index 0000000000000000000000000000000000000000..4a7204b9ff312fece2c9683f7d4717097b415577
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/README
@@ -0,0 +1,3 @@
+TODO(kkm): These file will go into Grpc.Tools/build after package split.
+           Remove leading underscores from file names; they are hiding the
+           files from some NuGet versions which pull them into project.
diff --git a/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props
new file mode 100644
index 0000000000000000000000000000000000000000..8ce07c48abae81a1308eef63e18a786a04fd8b0e
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+  </PropertyGroup>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets
new file mode 100644
index 0000000000000000000000000000000000000000..0042bf2bfa97c2676ad75341d72127a7e36d34cb
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+    <gRPC_PluginFileName Condition=" '$(gRPC_PluginFileName)' == '' and '$(Language)' == 'C#' ">grpc_csharp_plugin</gRPC_PluginFileName>
+  </PropertyGroup>
+
+  <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+    <!-- Extend property pages with gRPC properties. -->
+    <PropertyPageSchema Include="$(MSBuildThisFileDirectory)Grpc.CSharp.xml">
+      <Context>File;BrowseObject</Context>
+    </PropertyPageSchema>
+  </ItemGroup>
+
+  <ItemDefinitionGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+    <ProtoBuf>
+      <GrpcServices Condition="'%(ProtoBuf.GrpcServices)' == '' ">Both</GrpcServices>
+    </ProtoBuf>
+  </ItemDefinitionGroup>
+
+  <!-- This target is invoked in a C# project, or can be called in a customized project. -->
+  <Target Name="gRPC_ResolvePluginFullPath" AfterTargets="Protobuf_ResolvePlatform">
+    <PropertyGroup>
+      <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' "
+           >$(Protobuf_PackagedToolsPath)bin\$(Protobuf_ToolsOs)\$(gRPC_PluginFileName).exe</gRPC_PluginFullPath>
+      <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' "
+           >$(Protobuf_PackagedToolsPath)bin/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/$(gRPC_PluginFileName)</gRPC_PluginFullPath>
+    </PropertyGroup>
+  </Target>
+
+  <Target Name="_gRPC_PrepareCompileOptions" AfterTargets="Protobuf_PrepareCompileOptions">
+    <ItemGroup Condition=" '$(Language)' == 'C#' ">
+      <Protobuf_Compile Condition=" %(Protobuf_Compile.GrpcServices) != 'None' ">
+        <GrpcPluginExe Condition=" '%(Protobuf_Compile.GrpcPluginExe)' == '' ">$(gRPC_PluginFullPath)</GrpcPluginExe>
+        <GrpcOutputDir Condition=" '%(Protobuf_Compile.GrpcOutputDir)' == '' " >%(Protobuf_Compile.OutputDir)</GrpcOutputDir>
+        <_GrpcOutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._GrpcOutputOptions);internal_access</_GrpcOutputOptions>
+      </Protobuf_Compile>
+      <Protobuf_Compile Condition=" '%(Protobuf_Compile.GrpcServices)' == 'Client' ">
+        <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_server</_GrpcOutputOptions>
+      </Protobuf_Compile>
+      <Protobuf_Compile Condition=" '%(Protobuf_Compile.GrpcServices)' == 'Server' ">
+        <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_client</_GrpcOutputOptions>
+      </Protobuf_Compile>
+    </ItemGroup>
+  </Target>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props
new file mode 100644
index 0000000000000000000000000000000000000000..06ee9bcda8a633accbb8f5140d8da0c463eff1e7
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+
+    <!-- Revision number of this package conventions (as if "API" version). -->
+    <Protobuf_ToolingRevision>1</Protobuf_ToolingRevision>
+
+    <!-- TODO(kkm): Remove "../" when separating packages. -->
+    <Protobuf_PackagedToolsPath>$( [System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)../native/) )</Protobuf_PackagedToolsPath>
+    <Protobuf_StandardImportsPath>$(Protobuf_PackagedToolsPath)include</Protobuf_StandardImportsPath>
+  </PropertyGroup>
+
+  <!-- NET SDK projects only: include proto files by default. Other project
+       types are not setting or using $(EnableDefaultItems).
+       Note that MSBuild evaluates all ItemGroups and their conditions in the
+       final pass over the build script, so properties like EnableDefaultProtoBufItems
+       here can be changed later in the project. -->
+  <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' ">
+    <ProtoBuf Include="**/*.proto"
+              Condition=" '$(EnableDefaultItems)' == 'true' and '$(EnableDefaultProtoBufItems)' == 'true' " />
+  </ItemGroup>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets
new file mode 100644
index 0000000000000000000000000000000000000000..5a8d3f20276ad73973d90552830032305509aebf
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets
@@ -0,0 +1,383 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+    <!-- We allow a non-C# generator be set by the user, but skip adding outputs to Compile in this case. -->
+    <Protobuf_Generator Condition=" '$(Protobuf_Generator)' == '' and '$(Language)' == 'C#' ">CSharp</Protobuf_Generator>
+    <!-- Configuration is passing the smoke test. -->
+    <Protobuf_ProjectSupported Condition=" '$(Protobuf_Generator)' != '' ">true</Protobuf_ProjectSupported>
+    <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' == 'Core' ">netstandard1.3\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
+    <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net40\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
+  </PropertyGroup>
+
+  <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoToolsPlatform" />
+  <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoCompilerOutputs" />
+  <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoReadDependencies" />
+  <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoCompile" />
+
+  <PropertyGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' ">
+    <Protobuf_IntermediatePath Condition=" '$(Protobuf_IntermediatePath)' == '' ">$(IntermediateOutputPath)</Protobuf_IntermediatePath>
+    <Protobuf_OutputPath Condition=" '$(Protobuf_OutputPath)' == '' ">$(Protobuf_IntermediatePath)</Protobuf_OutputPath>
+    <Protobuf_DepFilesPath Condition=" '$(Protobuf_DepFilesPath)' == '' ">$(Protobuf_IntermediatePath)</Protobuf_DepFilesPath>
+  </PropertyGroup>
+
+  <ItemDefinitionGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+    <ProtoBuf>
+      <Access Condition="'%(ProtoBuf.Access)' == '' ">Public</Access>
+      <ProtoCompile Condition="'%(ProtoBuf.ProtoCompile)' == '' ">True</ProtoCompile>
+      <ProtoRoot Condition="'%(ProtoBuf.ProtoRoot)' == '' " />
+      <CompileOutputs Condition="'%(ProtoBuf.CompileOutputs)' == ''">True</CompileOutputs>
+      <OutputDir Condition="'%(ProtoBuf.OutputDir)' == '' ">$(Protobuf_OutputPath)</OutputDir>
+    </ProtoBuf>
+  </ItemDefinitionGroup>
+
+  <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+    <PropertyPageSchema Include="$(MSBuildThisFileDirectory)Protobuf.CSharp.xml">
+      <Context>File;BrowseObject</Context>
+    </PropertyPageSchema>
+    <AvailableItemName Include="ProtoBuf" />
+  </ItemGroup>
+
+  <PropertyGroup>
+    <!-- NET SDK: by default, do not include proto files in the directory.
+         Current Microsoft's recommendation is against globbing:
+         https://docs.microsoft.com/en-us/dotnet/core/tools/csproj#recommendation -->
+    <EnableDefaultProtoBufItems Condition=" '$(EnableDefaultProtoBufItems)' == '' ">false</EnableDefaultProtoBufItems>
+  </PropertyGroup>
+
+  <!-- Check configuration sanity before build. -->
+  <Target Name="_Protobuf_SanityCheck" BeforeTargets="PrepareForBuild">
+    <Error
+      Condition=" '$(Protobuf_ProjectSupported)' != 'true' "
+      Text="Google.Protobuf.Tools proto compilation is only supported by default in a C# project (extension .csproj)" />
+  </Target>
+
+  <!--================================================================================
+                                     Tool path resolution
+   =================================================================================-->
+
+  <!-- Extension point for plugin packages: use Protobuf_ToolsOs and Protobuf_ToolsCpu
+       to resolve executable. Either or both may be blank, however, if resolution
+       fails; do check them before using. -->
+  <Target Name="Protobuf_ResolvePlatform">
+    <ProtoToolsPlatform>
+      <Output TaskParameter="Os" PropertyName="_Protobuf_ToolsOs"/>
+      <Output TaskParameter="Cpu" PropertyName="_Protobuf_ToolsCpu"/>
+    </ProtoToolsPlatform>
+
+    <PropertyGroup>
+      <!-- First try environment variable. -->
+      <Protobuf_ToolsOs>$(PROTOBUF_TOOLS_OS)</Protobuf_ToolsOs>
+      <Protobuf_ToolsCpu>$(PROTOBUF_TOOLS_CPU)</Protobuf_ToolsCpu>
+      <Protobuf_ProtocFullPath>$(PROTOBUF_PROTOC)</Protobuf_ProtocFullPath>
+
+      <!-- Next try OS and CPU resolved by ProtoToolsPlatform. -->
+      <Protobuf_ToolsOs Condition=" '$(Protobuf_ToolsOs)' == '' ">$(_Protobuf_ToolsOs)</Protobuf_ToolsOs>
+      <Protobuf_ToolsCpu Condition=" '$(Protobuf_ToolsCpu)' == '' ">$(_Protobuf_ToolsCpu)</Protobuf_ToolsCpu>
+      <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' "
+           >$(Protobuf_PackagedToolsPath)bin\$(Protobuf_ToolsOs)\protoc.exe</Protobuf_ProtocFullPath>
+      <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' "
+           >$(Protobuf_PackagedToolsPath)bin/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/protoc</Protobuf_ProtocFullPath>
+    </PropertyGroup>
+
+    <Error Condition=" '$(DesignTimeBuild)' != 'true' and '$(PROTOBUF_PROTOC)' == ''
+                        and ( '$(Protobuf_ToolsOs)' == '' or '$(Protobuf_ToolsCpu)' == '' ) "
+      Text="Google.Protobuf.Tools cannot determine host OS and CPU.&#10;Use environment variables PROTOBUF_TOOLS_OS={linux|macosx|windows} and PROTOBUF_TOOLS_CPU={x86|x64} to try the closest match to your system.&#10;You may also set PROTOBUF_PROTOC to specify full path to the host-provided compiler (v3.5+ is required)." />
+  </Target>
+
+  <!--================================================================================
+                                     Proto compilation
+   =================================================================================-->
+
+  <!-- Extension points. -->
+  <Target Name="Protobuf_BeforeCompile" />
+  <Target Name="Protobuf_AfterCompile" />
+
+  <!-- Main compile sequence. Certain steps are gated by the value $(DesignTimeBuild),
+       so the sequence is good for either design time or build time. -->
+  <Target Name="Protobuf_Compile"
+          Condition=" '@(ProtoBuf)' != '' "
+          DependsOnTargets=" Protobuf_BeforeCompile;
+                             Protobuf_ResolvePlatform;
+                             _Protobuf_SelectFiles;
+                             Protobuf_PrepareCompile;
+                             _Protobuf_AugmentLanguageCompile;
+                             _Protobuf_CoreCompile;
+                             Protobuf_ReconcileOutputs;
+                             Protobuf_AfterCompile" />
+
+  <!-- Do proto compilation by default in a C# project. In other types, the user invoke
+       Protobuf_Compile directly where required. -->
+  <!-- TODO(kkm): Do shared compile in outer multitarget project? -->
+  <Target Name="_Protobuf_Compile_BeforeCsCompile"
+          BeforeTargets="BeforeCompile"
+          DependsOnTargets="Protobuf_Compile"
+          Condition=" '$(Language)' == 'C#' " />
+
+  <Target Name="_Protobuf_SelectFiles">
+    <!-- Guess .proto root for the files. Whenever the root is set for a file explicitly,
+         leave it as is. Otherwise, for files under the project directory, set the root
+         to "." for the project's directory, as it is the current when compiling; for the
+         files outside of project directory, use each .proto file's directory as the root. -->
+    <FindUnderPath Path="$(MSBuildProjectDirectory)"
+                   Files="@(ProtoBuf->WithMetadataValue('ProtoRoot',''))">
+      <Output TaskParameter="InPath" ItemName="_Protobuf_NoRootInProject"/>
+      <Output TaskParameter="OutOfPath" ItemName="_Protobuf_NoRootElsewhere"/>
+    </FindUnderPath>
+    <ItemGroup>
+      <!-- Files with explicit metadata. -->
+      <Protobuf_Compile Include="@(ProtoBuf->HasMetadata('ProtoRoot'))" />
+      <!-- In-project files will have ProtoRoot='.'. -->
+      <Protobuf_Compile Include="@(_Protobuf_NoRootInProject)">
+        <ProtoRoot>.</ProtoRoot>
+      </Protobuf_Compile>
+      <!-- Out-of-project files will have respective ProtoRoot='%(RelativeDir)'. -->
+      <Protobuf_Compile Include="@(_Protobuf_NoRootElsewhere)">
+        <ProtoRoot>%(RelativeDir)</ProtoRoot>
+      </Protobuf_Compile>
+      <!-- Remove files not for compile. -->
+      <Protobuf_Compile Remove="@(Protobuf_Compile)" Condition=" !%(ProtoCompile) " />
+      <!-- Ensure invariant Source=%(Identity). -->
+      <Protobuf_Compile>
+        <Source>%(Identity)</Source>
+      </Protobuf_Compile>
+    </ItemGroup>
+  </Target>
+
+  <!-- Extension point for non-C# project. Protobuf_Generator should be supported
+       by the ProtoCompile task, but we skip inferring expected outputs. All proto
+       files will be always recompiled with a warning, unless you add expectations
+       to the Protobuf_ExpectedOutputs collection.
+
+       All inferred ExpectedOutputs will be added to code compile (C#) in a C# project
+       by default. This is controlled per-proto by the CompileOutputs metadata. -->
+  <Target Name="Protobuf_PrepareCompile" Condition=" '@(Protobuf_Compile)' != '' ">
+    <!-- Predict expected names. -->
+    <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' "
+                          ProtoBuf="@(Protobuf_Compile)"
+                          Generator="$(Protobuf_Generator)">
+      <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" />
+    </ProtoCompilerOutputs>
+    <!-- Read any dependency files from previous compiles. -->
+    <ProtoReadDependencies Condition=" '$(Protobuf_DepFilesPath)' != '' and '$(DesignTimeBuild)' != 'true' "
+                           ProtoBuf="@(Protobuf_Compile)"
+                           ProtoDepDir="$(Protobuf_DepFilesPath)" >
+      <Output TaskParameter="Dependencies" ItemName="Protobuf_Dependencies" />
+    </ProtoReadDependencies>
+  </Target>
+
+  <!-- Add all expected outputs, and only these, to language compile.  -->
+  <Target Name="_Protobuf_AugmentLanguageCompile"
+          DependsOnTargets="_Protobuf_EnforceInvariants"
+          Condition=" '$(Language)' == 'C#' ">
+    <ItemGroup>
+      <_Protobuf_CodeCompile Include="@(Protobuf_ExpectedOutputs->Distinct())"
+         Condition=" '%(Source)' != '' and '@(Protobuf_Compile->WithMetadataValue('CompileOutputs', 'true'))' != '' " />
+      <Compile Include="@(_Protobuf_CodeCompile)" />
+    </ItemGroup>
+  </Target>
+
+  <!-- These invariants must be kept for compile up-to-date check to work. -->
+  <Target Name="_Protobuf_EnforceInvariants">
+    <!-- Enforce Source=Identity on proto files. The 'Source' metadata is used as a common
+         key to match dependencies/expected outputs in the lists for up-to-date checks. -->
+    <ItemGroup>
+      <Protobuf_Compile>
+        <Source>%(Identity)</Source>
+      </Protobuf_Compile>
+    </ItemGroup>
+
+    <!-- Remove possible output and dependency declarations that have no Source set, or those
+         not matching any proto marked for compilation. -->
+    <ItemGroup>
+      <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Protobuf_ExpectedOutputs.Source)' == '' " />
+      <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " />
+      <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Protobuf_Dependencies.Source)' == '' " />
+      <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " />
+    </ItemGroup>
+  </Target>
+
+  <!-- Gather files with and without known outputs, separately. -->
+  <Target Name="_Protobuf_GatherStaleFiles"
+          Condition=" '@(Protobuf_Compile)' != '' "
+          DependsOnTargets="_Protobuf_EnforceInvariants; _Protobuf_GatherStaleSimple; _Protobuf_GatherStaleBatched">
+    <ItemGroup>
+      <!-- Drop outputs from MSBuild inference (they won't have the '_Exec' metadata).  -->
+      <_Protobuf_OutOfDateProto Remove="@(_Protobuf_OutOfDateProto->WithMetadataValue('_Exec',''))" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_Protobuf_GatherStaleSimple">
+    <!-- Simple selection: always compile files that have no declared outputs (but warn below). -->
+    <ItemGroup>
+      <_Protobuf_OutOfDateProto Include="@(Protobuf_Compile)"
+                                Condition = " '%(Source)' != '' and '@(Protobuf_ExpectedOutputs)' == '' ">
+        <_Exec>true</_Exec>
+      </_Protobuf_OutOfDateProto>
+    </ItemGroup>
+
+    <!-- You are seeing this warning because there was no Protobuf_ExpectedOutputs items with
+         their Source attribute pointing to the proto files listed in the warning. Such files
+         will be recompiled on every build, as there is nothing to run up-to-date check against.
+         Set Protobuf_NoOrphanWarning to 'true' to suppress if this is what you want. -->
+    <Warning Condition=" '@(_Protobuf_OutOfDateProto)' != '' and '$(Protobuf_NoOrphanWarning)' != 'true' "
+             Text="The following files have no known outputs, and will be always recompiled as if out-of-date:&#10;@(_Protobuf_Orphans->'&#10;    %(Identity)', '')" />
+  </Target>
+
+  <Target Name="_Protobuf_GatherStaleBatched"
+          Inputs="@(Protobuf_Compile);%(Source);@(Protobuf_Dependencies);$(MSBuildAllProjects)"
+          Outputs="@(Protobuf_ExpectedOutputs)" >
+    <!-- The '_Exec' metadatum is set to distinguish really executed items from those MSBuild so
+         "helpfully" infers in a bucketed task. For the same reason, cannot use the intrinsic
+         ItemGroup task here. -->
+    <CreateItem Include="@(Protobuf_Compile)" AdditionalMetadata="_Exec=true">
+      <Output TaskParameter="Include" ItemName="_Protobuf_OutOfDateProto"/>
+    </CreateItem>
+  </Target>
+
+  <!-- Extension point: Plugins massage metadata into recognized metadata
+       values passed to the ProtoCompile task. -->
+  <Target Name="Protobuf_PrepareCompileOptions" Condition=" '@(Protobuf_Compile)' != '' ">
+    <ItemGroup>
+      <Protobuf_Compile>
+        <_OutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._OutputOptions);internal_access</_OutputOptions>
+      </Protobuf_Compile>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_Protobuf_CoreCompile"
+          Condition=" '$(DesignTimeBuild)' != 'true' "
+          DependsOnTargets="Protobuf_PrepareCompileOptions;_Protobuf_GatherStaleFiles">
+    <!-- Ensure output directories. -->
+    <MakeDir Directories="%(_Protobuf_OutOfDateProto.OutputDir)" />
+    <MakeDir Directories="%(_Protobuf_OutOfDateProto.GrpcOutputDir)" />
+    <MakeDir Directories="$(Protobuf_DepFilesPath)" />
+
+    <!-- Force output to the current directory if the user has set it to empty. -->
+    <ItemGroup>
+      <_Protobuf_OutOfDateProto>
+        <OutputDir Condition=" '%(OutputDir)' == '' ">.</OutputDir>
+      </_Protobuf_OutOfDateProto>
+    </ItemGroup>
+
+    <ProtoCompile Condition=" '@(_Protobuf_OutOfDateProto)' != '' "
+      ToolExe="$(Protobuf_ProtocFullPath)"
+      Generator="$(Protobuf_Generator)"
+      ProtoBuf="%(_Protobuf_OutOfDateProto.Source)"
+      ProtoPath="%(_Protobuf_OutOfDateProto.AdditionalImportDirs);$(Protobuf_StandardImportsPath);%(_Protobuf_OutOfDateProto.ProtoRoot)"
+      ProtoDepDir="$(Protobuf_DepFilesPath)"
+      OutputDir="%(_Protobuf_OutOfDateProto.OutputDir)"
+      OutputOptions="%(_Protobuf_OutOfDateProto._OutputOptions)"
+      GrpcPluginExe="%(_Protobuf_OutOfDateProto.GrpcPluginExe)"
+      GrpcOutputDir="%(_Protobuf_OutOfDateProto.GrpcOutputDir)"
+      GrpcOutputOptions="%(_Protobuf_OutOfDateProto._GrpcOutputOptions)"
+    >
+      <Output TaskParameter="GeneratedFiles" ItemName="_Protobuf_GeneratedFiles"/>
+    </ProtoCompile>
+
+    <!-- Compute files expected but not in fact produced by protoc. -->
+    <ItemGroup Condition=" '@(_Protobuf_OutOfDateProto)' != '' ">
+      <Protobuf_ExpectedNotGenerated Include="@(Protobuf_ExpectedOutputs)"
+                                     Condition=" '%(Source)' != '' and '@(_Protobuf_OutOfDateProto)' != '' " />
+      <Protobuf_ExpectedNotGenerated Remove="@(_Protobuf_GeneratedFiles)" />
+    </ItemGroup>
+  </Target>
+
+  <!-- Extension point. Plugins and/or unsupported projects may take special care of the
+       Protobuf_ExpectedNotGenerated list in BeforeTargets. We just silently create the
+       missing outputs so that out-of-date checks work (we do not add them to language
+       compile though). You can empty this collection in your Before targets to do nothing.
+       The target is not executed if the proto compiler is not executed. -->
+  <Target Name="Protobuf_ReconcileOutputs"
+          Condition=" '$(DesignTimeBuild)' != 'true' ">
+    <!-- Warn about unexpected/missing files outside object file directory only.
+         This should have happened because build was incorrectly customized. -->
+    <FindUnderPath Path="$(BaseIntermediateOutputPath)" Files="@(Protobuf_ExpectedNotGenerated)">
+      <Output TaskParameter="InPath" ItemName="_Protobuf_ExpectedNotGeneratedInTemp"/>
+      <Output TaskParameter="OutOfPath" ItemName="_Protobuf_ExpectedNotGeneratedElsewhere"/>
+    </FindUnderPath>
+
+    <!-- Prevent unnecessary recompilation by silently creating empty files. This probably
+         has happened because a proto file with an rpc service was processed by the gRPC
+         plugin, and the user did not set GrpcOutput to None. When we treat outputs as
+         transient, we can do it permissively. -->
+    <Touch Files="@(_Protobuf_ExpectedNotGeneratedInTemp)" AlwaysCreate="true" />
+
+    <!-- Also create empty files outside of the intermediate directory, if the user wants so. -->
+    <Touch Files="@(_Protobuf_ExpectedNotGeneratedElsewhere)" AlwaysCreate="true"
+           Condition=" '$(Protobuf_TouchMissingExpected)' == 'true' "/>
+
+    <!-- You are seeing this warning because there were some Protobuf_ExpectedOutputs items
+         (outside of the transient directory under obj/) not in fact produced by protoc. -->
+    <Warning Condition=" '@(_Protobuf_ExpectedNotGeneratedElsewhere)' != '' and $(Protobuf_NoWarnMissingExpected) != 'true' "
+             Text="Some expected protoc outputs were not generated.&#10;@(_Protobuf_ExpectedNotGeneratedElsewhere->'&#10;    %(Identity)', '')" />
+  </Target>
+
+  <!--================================================================================
+                                   Proto cleanup
+   =================================================================================-->
+
+  <!-- We fully support cleanup only in a C# project. If extending the build for other
+       generators/plugins, then mostly roll your own. -->
+
+  <!-- Extension points. -->
+  <Target Name="Protobuf_BeforeClean" />
+  <Target Name="Protobuf_AfterClean" />
+
+  <!-- Main cleanup sequence. -->
+  <Target Name="Protobuf_Clean"
+          Condition=" '@(ProtoBuf)' != '' "
+          DependsOnTargets=" Protobuf_BeforeClean;
+                             Protobuf_PrepareClean;
+                             _Protobuf_CoreClean;
+                             Protobuf_AfterClean" />
+
+  <!-- Do proto cleanup by default in a C# project. In other types, the user should
+       invoke Protobuf_Clean directly if required. -->
+  <Target Name="_Protobuf_Clean_AfterCsClean"
+          AfterTargets="CoreClean"
+          DependsOnTargets="Protobuf_Clean"
+          Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' " />
+
+  <!-- Extension point for non-C# project. ProtoCompilerOutputs is not invoked for
+       non-C# projects, since inferring protoc outputs is required, so this is a
+       no-op in other project types. In your extension target populate the
+       Protobuf_ExpectedOutputs with all possible output. An option is to include
+       all existing outputs using Include with a wildcard, if you know where to look.
+
+       Note this is like Protobuf_PrepareCompile, but uses @(Protobuf) regardless
+       of the Compile metadata, to remove all possible outputs. Plugins should err
+       on the side of overextending the Protobuf_ExpectedOutputs here.
+
+       All ExpectedOutputs will be removed. -->
+  <Target Name="Protobuf_PrepareClean" Condition=" '@(Protobuf)' != '' ">
+    <!-- Predict expected names. -->
+    <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' "
+                          ProtoBuf="@(Protobuf)"
+                          Generator="$(Protobuf_Generator)">
+      <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" />
+    </ProtoCompilerOutputs>
+  </Target>
+
+  <Target Name="_Protobuf_CoreClean">
+    <ItemGroup>
+      <_Protobuf_Protodep Include="$(Protobuf_DepFilesPath)*.protodep" />
+    </ItemGroup>
+    <Delete Files="@(Protobuf_ExpectedOutputs);@(_Protobuf_Protodep)" TreatErrorsAsWarnings="true" />
+  </Target>
+
+  <!--================================================================================
+                                  Design-time support
+   =================================================================================-->
+
+  <!-- Add all .proto files to the SourceFilesProjectOutputGroupOutput, so that
+       * Visual Studio triggers a build when any of them changed;
+       * The Pack target includes .proto files into the source package.  -->
+  <Target Name="_Protobuf_SourceFilesProjectOutputGroup"
+          BeforeTargets="SourceFilesProjectOutputGroup"
+          Condition=" '@(ProtoBuf)' != '' " >
+    <ItemGroup>
+      <SourceFilesProjectOutputGroupOutput Include="@(ProtoBuf->'%(FullPath)')" />
+    </ItemGroup>
+  </Target>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2c41fbcbd068fc69c5a475676661c1a50664c805
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml
@@ -0,0 +1,99 @@
+<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
+  <FileExtension Name=".proto"
+                 ContentType="ProtoFile" />
+
+  <ContentType Name="ProtoFile"
+               DisplayName="Protocol buffer definitions file"
+               ItemType="ProtoBuf" />
+
+  <ItemType Name="ProtoBuf"
+            DisplayName="Protobuf compiler" />
+
+  <Rule Name="ProtoBuf"
+        DisplayName="File Properties"
+        PageTemplate="generic"
+        Description="File Properties"
+        OverrideMode="Extend">
+    <Rule.DataSource>
+      <DataSource Persistence="ProjectFile" Label="Configuration" ItemType="ProtoBuf"
+                  HasConfigurationCondition="false" SourceOfDefaultValue="AfterContext" />
+    </Rule.DataSource>
+
+    <Rule.Categories>
+      <Category Name="Advanced" DisplayName="Advanced" />
+      <Category Name="Protobuf" DisplayName="Protobuf" />
+      <Category Name="Misc" DisplayName="Misc" />
+    </Rule.Categories>
+
+    <DynamicEnumProperty Name="{}{ItemType}" DisplayName="Build Action"  Category="Advanced"
+                   Description="How the file relates to the build and deployment processes."
+                   EnumProvider="ItemTypes" />
+
+    <StringProperty Name="Identity" Visible="false" ReadOnly="true">
+      <StringProperty.DataSource>
+        <DataSource Persistence="Intrinsic" ItemType="ProtoBuf"
+                    PersistedName="Identity" SourceOfDefaultValue="AfterContext" />
+      </StringProperty.DataSource>
+    </StringProperty>
+
+    <StringProperty Name="FullPath"
+                    DisplayName="Full Path"
+                    ReadOnly="true"
+                    Category="Misc"
+                    Description="Location of the file.">
+      <StringProperty.DataSource>
+        <DataSource Persistence="Intrinsic" ItemType="ProtoBuf"
+                    PersistedName="FullPath" SourceOfDefaultValue="AfterContext" />
+      </StringProperty.DataSource>
+    </StringProperty>
+
+    <StringProperty Name="FileNameAndExtension"
+                    DisplayName="File Name"
+                    ReadOnly="true"
+                    Category="Misc"
+                    Description="Name of the file or folder.">
+      <StringProperty.DataSource>
+        <DataSource Persistence="Intrinsic" ItemType="ProtoBuf"
+                    PersistedName="FileNameAndExtension" SourceOfDefaultValue="AfterContext" />
+      </StringProperty.DataSource>
+    </StringProperty>
+
+    <BoolProperty Name="Visible" Visible="false" Default="true" />
+
+    <StringProperty Name="DependentUpon" Visible="false">
+      <StringProperty.Metadata>
+        <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" />
+      </StringProperty.Metadata>
+    </StringProperty>
+
+    <StringProperty Name="Link" Visible="false">
+      <StringProperty.DataSource>
+        <DataSource SourceOfDefaultValue="AfterContext" />
+      </StringProperty.DataSource>
+      <StringProperty.Metadata>
+        <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" />
+      </StringProperty.Metadata>
+    </StringProperty>
+
+    <EnumProperty Name="Access" DisplayName="Class Access"
+                  Category="Protobuf"
+                  Description="Public or internal access modifier on generated classes.">
+      <EnumValue Name="Public" DisplayName="Public" IsDefault="true" />
+      <EnumValue Name="Internal" DisplayName="Internal" />
+      <EnumProperty.DataSource>
+        <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext"
+                    PersistenceStyle="Attribute" />
+      </EnumProperty.DataSource>
+    </EnumProperty>
+
+    <BoolProperty Name="ProtoCompile" DisplayName="Compile Protobuf"
+                  Category="Protobuf" Default="true"
+                  Description="Specifies if this file is compiled or only imported by other files.">
+      <BoolProperty.DataSource>
+        <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext"
+                    PersistenceStyle="Attribute" />
+      </BoolProperty.DataSource>
+    </BoolProperty>
+
+  </Rule>
+</ProjectSchemaDefinitions>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/README b/src/csharp/Grpc.Tools/build/_protobuf/README
new file mode 100644
index 0000000000000000000000000000000000000000..e6e358a218d5c6f193bf1daaeba0e525ce7267ea
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/README
@@ -0,0 +1 @@
+TODO(kkm): These file will go into Google.Protobuf.Tools/build after package split.
diff --git a/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props
new file mode 100644
index 0000000000000000000000000000000000000000..7f64ae91658f02be5c687b9b951018917bed323e
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+
+    <!-- Revision number of this package conventions (as if "API" version). -->
+    <Protobuf_ToolingRevision>1</Protobuf_ToolingRevision>
+
+    <!-- For a Visual Studio C++ native project we currently only resolve tools and import paths. -->
+    <Protobuf_ProtocFullPath>$(MSBuildThisFileDirectory)bin\windows\protoc.exe</Protobuf_ProtocFullPath>
+    <Protobuf_StandardImportsPath>$(MSBuildThisFileDirectory)bin\include\</Protobuf_StandardImportsPath>
+    <gRPC_PluginFileName>grpc_cpp_plugin</gRPC_PluginFileName>
+    <gRPC_PluginFullPath>$(MSBuildThisFileDirectory)bin\windows\grpc_cpp_plugin.exe</gRPC_PluginFullPath>
+  </PropertyGroup>
+</Project>
diff --git a/src/csharp/Grpc.sln b/src/csharp/Grpc.sln
index d9a7b8d556b850e3ab283bb6a227b88d6478b7eb..6c1b2e999807673b05208c43d7076e64e5dd7f24 100644
--- a/src/csharp/Grpc.sln
+++ b/src/csharp/Grpc.sln
@@ -39,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Reflection.Tests", "Gr
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Microbenchmarks", "Grpc.Microbenchmarks\Grpc.Microbenchmarks.csproj", "{84C17746-4727-4290-8E8B-A380793DAE1E}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Tools", "Grpc.Tools\Grpc.Tools.csproj", "{8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Tools.Tests", "Grpc.Tools.Tests\Grpc.Tools.Tests.csproj", "{AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -117,6 +121,14 @@ Global
 		{84C17746-4727-4290-8E8B-A380793DAE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{84C17746-4727-4290-8E8B-A380793DAE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{84C17746-4727-4290-8E8B-A380793DAE1E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/src/csharp/build_packages_dotnetcli.bat b/src/csharp/build_packages_dotnetcli.bat
index 394b859e0be591eeed0a543929b11c6ff5f965e3..cae253bc7b91745a1c091cd823e15bd7b4e47d16 100755
--- a/src/csharp/build_packages_dotnetcli.bat
+++ b/src/csharp/build_packages_dotnetcli.bat
@@ -45,10 +45,10 @@ xcopy /Y /I nativelibs\csharp_ext_windows_x64\grpc_csharp_ext.dll ..\..\cmake\bu
 %DOTNET% pack --configuration Release Grpc.Auth --output ..\..\..\artifacts || goto :error
 %DOTNET% pack --configuration Release Grpc.HealthCheck --output ..\..\..\artifacts || goto :error
 %DOTNET% pack --configuration Release Grpc.Reflection --output ..\..\..\artifacts || goto :error
+%DOTNET% pack --configuration Release Grpc.Tools --output ..\..\..\artifacts || goto :error
 
 %NUGET% pack Grpc.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts || goto :error
 %NUGET% pack Grpc.Core.NativeDebug.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts
-%NUGET% pack Grpc.Tools.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts
 
 @rem copy resulting nuget packages to artifacts directory
 xcopy /Y /I *.nupkg ..\..\artifacts\ || goto :error
diff --git a/src/csharp/build_packages_dotnetcli.sh b/src/csharp/build_packages_dotnetcli.sh
index 273d745f170b8e35ba4c9370808ef6261116eb14..8b81eba3aaa3924a64adb222ecbfb2fe3c9c2dfa 100755
--- a/src/csharp/build_packages_dotnetcli.sh
+++ b/src/csharp/build_packages_dotnetcli.sh
@@ -44,9 +44,9 @@ dotnet pack --configuration Release Grpc.Core.Testing --output ../../../artifact
 dotnet pack --configuration Release Grpc.Auth --output ../../../artifacts
 dotnet pack --configuration Release Grpc.HealthCheck --output ../../../artifacts
 dotnet pack --configuration Release Grpc.Reflection --output ../../../artifacts
+dotnet pack --configuration Release Grpc.Tools --output ../../../artifacts
 
 nuget pack Grpc.nuspec -Version "1.14.0-dev" -OutputDirectory ../../artifacts
 nuget pack Grpc.Core.NativeDebug.nuspec -Version "1.14.0-dev" -OutputDirectory ../../artifacts
-nuget pack Grpc.Tools.nuspec -Version "1.14.0-dev" -OutputDirectory ../../artifacts
 
 (cd ../../artifacts && zip csharp_nugets_dotnetcli.zip *.nupkg)
diff --git a/src/csharp/tests.json b/src/csharp/tests.json
index c2f243fe0ae7039ffdaa9839b20b18937e2f8680..483d1d7aada79bf088853e2e5f431678cc147c0b 100644
--- a/src/csharp/tests.json
+++ b/src/csharp/tests.json
@@ -62,5 +62,15 @@
   "Grpc.Reflection.Tests": [
     "Grpc.Reflection.Tests.ReflectionClientServerTest",
     "Grpc.Reflection.Tests.SymbolRegistryTest"
+  ],
+  "Grpc.Tools.Tests": [
+    "Grpc.Tools.Tests.CppGeneratorTests",
+    "Grpc.Tools.Tests.CSharpGeneratorTests",
+    "Grpc.Tools.Tests.GeneratorTests",
+    "Grpc.Tools.Tests.ProtoCompileBasicTests",
+    "Grpc.Tools.Tests.ProtoCompileCommandLineGeneratorTests",
+    "Grpc.Tools.Tests.ProtoCompileCommandLinePrinterTests",
+    "Grpc.Tools.Tests.ProtoToolsPlatformTaskTests",
+    "Grps.Tools.Tests.DepFileUtilTests"
   ]
 }
diff --git a/templates/src/csharp/build_packages_dotnetcli.bat.template b/templates/src/csharp/build_packages_dotnetcli.bat.template
index 45010fec7416da46cbc0c0e2e46f379869a04a50..cdadbba44f2f907e3aee28546cb6f8e0a2eb6418 100755
--- a/templates/src/csharp/build_packages_dotnetcli.bat.template
+++ b/templates/src/csharp/build_packages_dotnetcli.bat.template
@@ -47,10 +47,10 @@
   %%DOTNET% pack --configuration Release Grpc.Auth --output ..\..\..\artifacts || goto :error
   %%DOTNET% pack --configuration Release Grpc.HealthCheck --output ..\..\..\artifacts || goto :error
   %%DOTNET% pack --configuration Release Grpc.Reflection --output ..\..\..\artifacts || goto :error
+  %%DOTNET% pack --configuration Release Grpc.Tools --output ..\..\..\artifacts || goto :error
   
   %%NUGET% pack Grpc.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts || goto :error
   %%NUGET% pack Grpc.Core.NativeDebug.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts
-  %%NUGET% pack Grpc.Tools.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts
   
   @rem copy resulting nuget packages to artifacts directory
   xcopy /Y /I *.nupkg ..\..\artifacts\ || goto :error
diff --git a/templates/src/csharp/build_packages_dotnetcli.sh.template b/templates/src/csharp/build_packages_dotnetcli.sh.template
index 1172582ebde03e7313262afdc672af46cb737b09..5eba62efabf4b58aa3a999f201d21d0fcf556bf7 100755
--- a/templates/src/csharp/build_packages_dotnetcli.sh.template
+++ b/templates/src/csharp/build_packages_dotnetcli.sh.template
@@ -46,9 +46,9 @@
   dotnet pack --configuration Release Grpc.Auth --output ../../../artifacts
   dotnet pack --configuration Release Grpc.HealthCheck --output ../../../artifacts
   dotnet pack --configuration Release Grpc.Reflection --output ../../../artifacts
+  dotnet pack --configuration Release Grpc.Tools --output ../../../artifacts
   
   nuget pack Grpc.nuspec -Version "${settings.csharp_version}" -OutputDirectory ../../artifacts
   nuget pack Grpc.Core.NativeDebug.nuspec -Version "${settings.csharp_version}" -OutputDirectory ../../artifacts
-  nuget pack Grpc.Tools.nuspec -Version "${settings.csharp_version}" -OutputDirectory ../../artifacts
   
   (cd ../../artifacts && zip csharp_nugets_dotnetcli.zip *.nupkg)