diff --git a/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs
index e89a8f4b5d3..4da62f5773e 100644
--- a/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs
+++ b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs
@@ -67,6 +67,34 @@ namespace Grpc.Tools.Tests
             Assert.AreNotEqual(unsame1, unsame2);
         }
 
+        [Test]
+        public void GetOutputDirWithHash_IsSane()
+        {
+            StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}$",
+                DepFileUtil.GetOutputDirWithHash("out", "foo.proto"));
+            StringAssert.IsMatch(@"^[a-f0-9]{16}$",
+                DepFileUtil.GetOutputDirWithHash("", "foo.proto"));
+        }
+
+        [Test]
+        public void GetOutputDirWithHash_HashesDir()
+        {
+            string PickHash(string fname) => DepFileUtil.GetOutputDirWithHash("", fname);
+
+            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
 
diff --git a/src/csharp/Grpc.Tools/DepFileUtil.cs b/src/csharp/Grpc.Tools/DepFileUtil.cs
index 440d3d535c8..15ffed24546 100644
--- a/src/csharp/Grpc.Tools/DepFileUtil.cs
+++ b/src/csharp/Grpc.Tools/DepFileUtil.cs
@@ -138,6 +138,24 @@ namespace Grpc.Tools
             return result.ToArray();
         }
 
+        /// <summary>
+        /// Construct the directory hash from a relative file name
+        /// </summary>
+        /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
+        /// <returns>
+        /// Directory hash based on the file name, e. g. "deadbeef12345678"
+        /// </returns>
+        private static string GetDirectoryHash(string proto)
+        {
+            string dirname = Path.GetDirectoryName(proto);
+            if (Platform.IsFsCaseInsensitive)
+            {
+                dirname = dirname.ToLowerInvariant();
+            }
+
+            return HashString64Hex(dirname);
+        }
+
         /// <summary>
         /// Construct relative dependency file name from directory hash and file name
         /// </summary>
@@ -145,7 +163,7 @@ namespace Grpc.Tools
         /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
         /// <returns>
         /// Full relative path to the dependency file, e. g.
-        /// "out/deadbeef12345678_file.protodep"
+        /// "out/deadbeef12345678/file.protodep"
         /// </returns>
         /// <remarks>
         /// Since a project may contain proto files with the same filename but in different
@@ -158,21 +176,45 @@ namespace Grpc.Tools
         /// project and solution directories, which are also some level deep from the root.
         /// Instead of creating long and unwieldy names for these proto sources, we cache
         /// the full path of the name without the filename, and append the filename to it,
-        /// as in e. g. "foo/file.proto" will yield the name "deadbeef12345678_file", where
-        /// "deadbeef12345678" is a presumed hash value of the string "foo/". This allows
+        /// as in e. g. "foo/file.proto" will yield the name "deadbeef12345678/file", where
+        /// "deadbeef12345678" is a presumed hash value of the string "foo". This allows
         /// the file names be short, unique (up to a hash collision), and still allowing
         /// the user to guess their provenance.
         /// </remarks>
         public static string GetDepFilenameForProto(string protoDepDir, string proto)
         {
-            string dirname = Path.GetDirectoryName(proto);
-            if (Platform.IsFsCaseInsensitive)
-            {
-                dirname = dirname.ToLowerInvariant();
-            }
-            string dirhash = HashString64Hex(dirname);
+            string outdir = GetOutputDirWithHash(protoDepDir, proto);
             string filename = Path.GetFileNameWithoutExtension(proto);
-            return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep");
+            return Path.Combine(outdir, $"{filename}.protodep");
+        }
+
+        /// <summary>
+        /// Construct relative output directory with directory hash
+        /// </summary>
+        /// <param name="outputDir">Relative path to the output directory, e. g. "out"</param>
+        /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
+        /// <returns>
+        /// Full relative path to the directory, e. g. "out/deadbeef12345678"
+        /// </returns>
+        /// <remarks>
+        /// Since a project may contain proto files with the same filename but in different
+        /// directories, a unique directory for the generated files is constructed based on the
+        /// proto file names directory. The directory path can be arbitrary, for example,
+        /// it can be outside of the project, or an absolute path including a drive letter,
+        /// or a UNC network path. A name constructed from such a path by, for example,
+        /// replacing disallowed name characters with an underscore, may well be over
+        /// filesystem's allowed path length, since it will be located under the project
+        /// and solution directories, which are also some level deep from the root.
+        /// Instead of creating long and unwieldy names for these proto sources, we cache
+        /// the full path of the name without the filename, as in e. g. "foo/file.proto"
+        /// will yield the name "deadbeef12345678", where that is a presumed hash value
+        /// of the string "foo". This allows the path to be short, unique (up to a hash
+        /// collision), and still allowing the user to guess their provenance.
+        /// </remarks>
+        public static string GetOutputDirWithHash(string outputDir, string proto)
+        {
+            var dirhash = GetDirectoryHash(proto);
+            return Path.Combine(outputDir, dirhash);
         }
 
         // Get a 64-bit hash for a directory string. We treat it as if it were
diff --git a/src/csharp/Grpc.Tools/GeneratorServices.cs b/src/csharp/Grpc.Tools/GeneratorServices.cs
index 903dd3dacd9..4e2d2f748f5 100644
--- a/src/csharp/Grpc.Tools/GeneratorServices.cs
+++ b/src/csharp/Grpc.Tools/GeneratorServices.cs
@@ -67,19 +67,20 @@ namespace Grpc.Tools
         {
             bool doGrpc = GrpcOutputPossible(protoItem);
             var outputs = new string[doGrpc ? 2 : 1];
-            string basename = Path.GetFileNameWithoutExtension(protoItem.ItemSpec);
+            var itemSpec = protoItem.ItemSpec;
+            string basename = Path.GetFileNameWithoutExtension(itemSpec);
 
-            string outdir = protoItem.GetMetadata(Metadata.OutputDir);
+            string outdir = DepFileUtil.GetOutputDirWithHash(protoItem.GetMetadata(Metadata.OutputDir), itemSpec);
             string filename = LowerUnderscoreToUpperCamelProtocWay(basename);
             outputs[0] = Path.Combine(outdir, filename) + ".cs";
 
             if (doGrpc)
             {
-                // Override outdir if kGrpcOutputDir present, default to proto output.
+                // Override outdir if GrpcOutputDir present, default to proto output.
                 string grpcdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
+                grpcdir = grpcdir == "" ? outdir : DepFileUtil.GetOutputDirWithHash(grpcdir, itemSpec);
                 filename = LowerUnderscoreToUpperCamelGrpcWay(basename);
-                outputs[1] = Path.Combine(
-                    grpcdir != "" ? grpcdir : outdir, filename) + "Grpc.cs";
+                outputs[1] = Path.Combine(grpcdir, filename) + "Grpc.cs";
             }
             return outputs;
         }
diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs
index 36a0ea36cee..8625c2a5aab 100644
--- a/src/csharp/Grpc.Tools/ProtoCompile.cs
+++ b/src/csharp/Grpc.Tools/ProtoCompile.cs
@@ -18,6 +18,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Text;
 using System.Text.RegularExpressions;
 using Microsoft.Build.Framework;
@@ -413,16 +414,21 @@ namespace Grpc.Tools
         // Called by the base ToolTask to get response file contents.
         protected override string GenerateResponseFileCommands()
         {
+            var outDir = TrimEndSlash(MaybeEnhanceOutputDir(OutputDir, Protobuf));
+            var grpcOutDir = TrimEndSlash(MaybeEnhanceOutputDir(GrpcOutputDir, Protobuf));
+
             var cmd = new ProtocResponseFileBuilder();
-            cmd.AddSwitchMaybe(Generator + "_out", TrimEndSlash(OutputDir));
+            cmd.AddSwitchMaybe(Generator + "_out", outDir);
             cmd.AddSwitchMaybe(Generator + "_opt", OutputOptions);
             cmd.AddSwitchMaybe("plugin=protoc-gen-grpc", GrpcPluginExe);
-            cmd.AddSwitchMaybe("grpc_out", TrimEndSlash(GrpcOutputDir));
+            cmd.AddSwitchMaybe("grpc_out", grpcOutDir);
             cmd.AddSwitchMaybe("grpc_opt", GrpcOutputOptions);
             if (ProtoPath != null)
             {
                 foreach (string path in ProtoPath)
+                {
                     cmd.AddSwitchMaybe("proto_path", TrimEndSlash(path));
+                }
             }
             cmd.AddSwitchMaybe("dependency_out", DependencyOut);
             cmd.AddSwitchMaybe("error_format", "msvs");
@@ -433,6 +439,18 @@ namespace Grpc.Tools
             return cmd.ToString();
         }
 
+        // If possible, disambiguate output dir by adding a hash of the proto file's path
+        static string MaybeEnhanceOutputDir(string outputDir, ITaskItem[] protobufs)
+        {
+            if (protobufs.Length != 1)
+            {
+                return outputDir;
+            }
+
+            var protoFile = protobufs[0].ItemSpec;
+            return DepFileUtil.GetOutputDirWithHash(outputDir, protoFile);
+        }
+
         // Protoc cannot digest trailing slashes in directory names,
         // curiously under Linux, but not in Windows.
         static string TrimEndSlash(string dir)