如何在C#中将Assembly.CodeBase转换为文件系统路径?

71

我有一个项目,将模板存储在与DLL和EXE相邻的\Templates文件夹中。

我想在运行时确定此文件路径,但需要使用一种既适用于单元测试又适用于生产环境的技术(我不想在NUnit中禁用阴影复制!)

Assembly.Location不好用,因为在NUnit下运行时,它返回阴影复制程序集的路径。

Environment.CommandLine也有限制,因为在NUnit等情况下,它返回到NUnit而不是我的项目的路径。

Assembly.CodeBase看起来很有前途,但它是UNC路径:

file:///D:/projects/MyApp/MyApp/bin/debug/MyApp.exe

现在我可以使用字符串操作将其转换为本地文件系统路径,但我怀疑在.NET框架中有一种更干净的方法。 有人知道推荐的方法吗?

(在这种情况下,如果UNC路径不是file:/// URL,则抛出异常是完全可以接受的)


5个回答

122

你需要使用 System.Uri.LocalPath:

string localPath = new Uri("file:///D:/projects/MyApp/MyApp/bin/debug/MyApp.exe").LocalPath;
所以,如果您想要当前正在执行的程序集的原始位置:
string localPath = new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath;

LocalPath 包括程序集的文件名,例如:

D:\projects\MyApp\MyApp\bin\debug\MyApp.exe
如果你想获取程序集的目录,那么使用System.IO.Path.GetDirectoryName()函数:
string localDirectory = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);

这将会给你:

D:\projects\MyApp\MyApp\bin\debug

8
请注意,当目录名中包含 "#" 字符时,该方法将无法正常工作。 - Drakarah
5
关键在于使用 Uri.LocalPath - 我们之前使用了 Uri.AbsolutePath,看起来是好使的,直到路径名中有空格时出现了错误! - Martin Ba
1
实际上,如果您的路径包含例如%20的字符,即使使用 LocalPath,也无法正常工作。(这可能是可以接受的...包含任何%xx转义字符的本地路径相当罕见。请参见此其他问题的评论:https://dev59.com/EHVD5IYBdhLWcg3wNIzc#u52cEYcBWogLw_1byeyD) - Martin Ba
1
注意:即使使用了EscapedCodeBase,对于某些用例,本答案仍可能不正确。请参见下面@MartinBa的答案以了解原因,并在您更看重正确性而非开发便利性时使用他的解决方案。 - Pakman
真正的宝石。由于某种原因,Assembly.GetCallingAssembly().CodeBase开始返回不同的结果,或者Path.GetDirectoryName()失去了解析该结果的能力。非常奇怪,但至少有这个解决方案。 - ouflak
最好使用new Uri(assembly.EscapedCodeBase).LocalPath,但即使如此也可能会失败,因为EscapedCodeBase不正确地将路径Space( )(h#)(p%20){[a&],t@,p%,+}转义为Space(%20)(h%23)(p%20)%7B%5Ba%26%5D,t@,p%,+%7D.,,这会导致%20在路径中被更改为一个空格,从而变成了Space( )(h#)(p ){[a&],t@,p%,+}., - Qwertie

30
Assembly.CodeBase看起来很有前途,但它是一个UNC路径:
请注意,它是类似于文件URI,而不是UNC路径
你可以通过手动进行字符串操作来解决这个问题。真的。
尝试在以下目录中找到所有其他你能在SO上找到的方法(逐字逐句):
C:\Test\Space( )(h#)(p%20){[a&],t@,p%,+}.,\Release

这是一个有效的,虽然有些不寻常的Windows路径。(有些人的路径中可能会有这些字符之一,而且你肯定希望你的方法对所有这些情况都能正常工作,对吧?)
可用的代码库(我们不需要Location,对吧?)属性然后是(在我的Win7上使用.NET 4):
assembly.CodeBase -> file:///C:/Test/Space( )(h#)(p%20){[a&],t@,p%,+}.,/Release

assembly.EscapedCodeBase -> file:///C:/Test/Space(%20)(h%23)(p%20)%7B%5Ba%26%5D,t@,p%,+%7D.,/Release

你会注意到:
  • CodeBase根本没有被转义,它只是常规的本地路径前缀加上file:///,并且反斜杠被替换掉。因此,将其传递给System.Uri不起作用的
  • EscapedCodeBase并没有完全转义(我不知道这是否是一个错误,或者这是否是URI scheme的不足之处):
    • 注意空格字符( )被转换为%20
    • 但是%20序列被转换为%20!(百分号%根本没有被转义)
    • 没有人能够从这个混乱的形式中重建原始形式!
对于本地文件(这对于CodeBase的东西来说真的是我关心的全部),因为如果文件不是本地的,你可能想要使用.Location,以下对我来说是有效的(请注意,它也不是最漂亮的):
    public static string GetAssemblyFullPath(Assembly assembly)
    {
        string codeBasePseudoUrl = assembly.CodeBase; // "pseudo" because it is not properly escaped
        if (codeBasePseudoUrl != null) {
            const string filePrefix3 = @"file:///";
            if (codeBasePseudoUrl.StartsWith(filePrefix3)) {
                string sPath = codeBasePseudoUrl.Substring(filePrefix3.Length);
                string bsPath = sPath.Replace('/', '\\');
                Console.WriteLine("bsPath: " + bsPath);
                string fp = Path.GetFullPath(bsPath);
                Console.WriteLine("fp: " + fp);
                return fp;
            }
        }
        System.Diagnostics.Debug.Assert(false, "CodeBase evaluation failed! - Using Location as fallback.");
        return Path.GetFullPath(assembly.Location);
    }

我相信人们可以提出更好的解决方案,可能甚至可以提出一个解决方案,如果CodeBase属性是一个本地路径,那么可以进行正确的URL编码/解码。但是,考虑到我们只需要去掉file:///就可以解决问题,我认为这个解决方案已经足够好了,尽管确实很丑陋。

1
如果是UNC路径,则为file://server/share/path/to/assembly.dll(只有2个斜杠)。 - Joshua
@Joshua - 你的意思是说一个\\server\...路径只编码了两个斜杠,而本地驱动器的路径有三个吗? - Martin Ba
@Joshua - 你知道 \\?\UNC\server\share 路径是如何编码的吗? - Martin Ba
1
这很棒,但你特别要用反斜杠替换正斜杠吗?这不会在其他平台上使用mono时引起问题吗? - BjarkeCK
1
@BjarkeCK 你可以使用 Path.DirectorySeparatorChar 来解决这个问题。 - brz
显示剩余2条评论

5
这应该可以运行:
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
Assembly asm = Assembly.GetCallingAssembly();
String path = Path.GetDirectoryName(new Uri(asm.EscapedCodeBase).LocalPath);

string strLog4NetConfigPath = System.IO.Path.Combine(path, "log4net.config");

我使用这个来使dll库能够使用独立的log4net.config文件记录日志。


4

还有一种解决方案,包括复杂路径:

    public static string GetPath(this Assembly assembly)
    {
        return Path.GetDirectoryName(assembly.GetFileName());
    }

    public static string GetFileName(this Assembly assembly)
    {
        return assembly.CodeBase.GetPathFromUri();
    }

    public static string GetPathFromUri(this string uriString)
    {
        var uri = new Uri(Uri.EscapeUriString(uriString));
        return String.Format("{0}{1}", Uri.UnescapeDataString(uri.PathAndQuery), Uri.UnescapeDataString(uri.Fragment));
    }

并且测试:

    [Test]
    public void GetPathFromUriTest()
    {
        Assert.AreEqual(@"C:/Test/Space( )(h#)(p%20){[a&],t@,p%,+}.,/Release", @"file:///C:/Test/Space( )(h#)(p%20){[a&],t@,p%,+}.,/Release".GetPathFromUri());
        Assert.AreEqual(@"C:/Test/Space( )(h#)(p%20){[a&],t@,p%,+}.,/Release",  @"file://C:/Test/Space( )(h#)(p%20){[a&],t@,p%,+}.,/Release".GetPathFromUri());
    }

    [Test]
    public void AssemblyPathTest()
    {
        var asm = Assembly.GetExecutingAssembly();

        var path = asm.GetPath();
        var file = asm.GetFileName();

        Assert.IsNotEmpty(path);
        Assert.IsNotEmpty(file);

        Assert.That(File     .Exists(file));
        Assert.That(Directory.Exists(path));
    }

不错!但我更喜欢反斜杠,所以我会通过Path.GetFullPathGetPathFromUri的结果传递。 - tm1
为了支持网络路径,您需要考虑Uri.Host。 - tm1

2

既然您在问题中标记了NUnit,那么您也可以使用AssemblyHelper.GetDirectoryName来获取执行程序集的原始目录:

using System.Reflection;
using NUnit.Framework.Internal;
...
string path = AssemblyHelper.GetDirectoryName(Assembly.GetExecutingAssembly())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接