为什么我可以从控制台应用程序打开一个路径很长(>259 个字符)的文件,但不能从 PowerPoint VSTO 加载项打开?

Why can I open a file with a long (>259 characters) path from a console app, but not from a PowerPoint VSTO add-in?

我们有一个 PowerPoint 加载项,使用 Visual Studio Office 工具 (VSTO) 在 C# 和 VS2019 中构建,目标是 .NET Framework 4.7.2。有时此加载项需要打开用户指定的文件,它使用通常的 System.IO.File.Open API 来执行此操作。我们最近收到了一位客户的错误报告,该客户无法打开其中一个文件。他们包括一个堆栈跟踪,看起来像这样:

System.IO.PathTooLongException: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
   at System.IO.PathHelper.GetFullPathName()
   at System.IO.Path.LegacyNormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength)
   at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.Open(String path, FileMode mode, FileAccess access, FileShare share)

原来他们文件的路径超过了旧的 259 个字符的限制。令人恼火的是,我们甚至无法通过使用文件的“短”路径来解决这个问题(即,每个路径组件都替换为旧的 8.3 版本)。该路径远低于限制,但当我们尝试打开它时出现相同的异常,但堆栈跟踪略有不同:

System.IO.PathTooLongException: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
   at System.IO.PathHelper.TryExpandShortFileName()
   at System.IO.Path.LegacyNormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength, Boolean expandShortPaths)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength)
   at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.Open(String path, FileMode mode, FileAccess access, FileShare share)

所以看起来,为了打开文件,它首先将短名称扩展为长名称,然后抱怨它太长了。当文件可以使用其短名称完美打开时似乎没有必要,但是没关系 - 我们只需要另一个解决方案。

接下来我尝试使用带有 \?\“原始”路径前缀的完整路径,这在过去总是让我出狱(尽管在 C++ 代码中使用 Windows API直接)。仍然不高兴 - 我得到了与使用完整路径无前缀时完全相同的堆栈跟踪的相同异常。

经过一些谷歌搜索后,我发现 this page documenting the AppContextSwitchOverrides element in app.config - 特别引起我兴趣的开关是 Switch.System.IO.UseLegacyPathHandlingSwitch.System.IO.BlockLongPaths。我在加载项启动期间检查了这些开关的值(使用 AppContext.TryGetSwitch),并且都设置为 true,所以我想我会尝试将它们都关闭。然而,将元素添加到我的 app.config 并没有奏效(可能是因为加载项不是它自己的应用程序,而是托管在 PowerPoint 中)。因此,在尝试打开任何文件之前,我在加载项中使用了以下代码片段:

AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);

然后我尝试使用我上面提到的每个路径(完整的、简短的和带有 \?\ 前缀的完整路径)打开文件并且...我得到了与以前完全相同的异常,完全相同每种情况下都有相同的堆栈跟踪。

对于那些还没有失去生存意志的人来说,这就是我的 ThisAddIn.cs class 的相关部分此时的样子:

using System;
using System.Diagnostics;
using System.IO;

static void TryOpen(string path, string desc)
{
  try
  {
    using (var strm = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None))
    {
    }
  }
  catch (IOException ex)
  {
    Debug.WriteLine($"Opening {desc} path failed with exception: {ex}");
  }
}

private void ThisAddIn_Startup(object sender, EventArgs e)
{
  AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
  AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
  var shortPath = @"D:\LONGDI~1\LONGDI~1\LONGDI~1\LONGDI~1\LONGDI~1\test.txt";
  var longPath = @"D:\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\test.txt";
  var prefixedPath = @"\?\D:\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\Long directory name that will be repeated a few times\test.txt";
  TryOpen(longPath, "long");
  TryOpen(shortPath, "short");
  TryOpen(prefixedPath, "prefixed");
}

我还尝试在针对 .NET Framework 4.7.2 的控制台应用程序中使用相同的代码,“长”和“短”路径也在那里失败,但“前缀”路径工作得很好。事实上,在控制台应用程序中,我可以省略 AppContext.SetSwitch() 调用,并且我得到完全相同的行为(“前缀”是唯一有效的行为)。

我确实找到了解决方案,但我讨厌它。如果我总是使用 \?\ 前缀并通过 P/Invoke 使用 Windows CreateFile() API,这会给我一个文件句柄,我可以将其传递给 FileStream构造函数。我可以做到这一点,但我必须在打开文件的任何地方更新代码,确保新代码在错误情况下以及在“快乐路径”上的行为方式相同。这是可行的,但如果可能的话,我宁愿避免几天的工作,而且相同的代码在控制台应用程序中运行良好的事实让我认为必须有一种方法可以让它在没有所有这些的情况下工作。

所以我的问题是:有没有其他人遇到过这个问题,有没有办法在 VSTO PowerPoint 加载项中 运行 时哄骗 System.IO.File.Open() 接受长路径?我对在控制台应用程序中获得的相同级别的功能很好(即能够通过添加 \?\ 前缀使其工作)而且我可以在控制台应用程序中使其工作这一事实让我觉得必须有某种方法可以做到这一点,我目前正在失踪。

更新:我放弃了。我接受了 Eugene 的回答,因为它可能对某些人有用 - 如果您正在构建内部应用程序并且您可以完全控制目标环境,那么为 Office 可执行文件部署应用程序配置文件可能是合适的。不过,我无法发布能够做到这一点的商业应用程序。我将查看提供长路径支持的可用 .NET 库,或者如果 none 对我有用,则使用 P/Invoke 滚动我自己的库。

最简单的方法是使用主机应用程序的配置文件(即在本例中,名为 POWERPNT.EXE.config 的文件,放置在与 POWERPNT.EXE 相同的目录中),其内容如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false;Switch.System.IO.BlockLongPaths=false" />
  </runtime>
</configuration>

您可以在 How to deal with files with a name longer than 259 characters? 讨论帖中找到可能的解决方案。