How can I convert Assembly.CodeBase into a filesystem path in C#?
Asked Answered
S

5

72

I have a project that stores templates in a \Templates folder next to the DLLs and EXE.

I want to determine this file path at runtime, but using a technique that will work inside a unit test as well as in production (and I don't want to disable shadow-copying in NUnit!)

Assembly.Location is no good because it returns the shadow-copied assembly's path when running under NUnit.

Environment.CommandLine is also of limited use because in NUnit et al it returns the path to NUnit, not to my project.

Assembly.CodeBase looks promising, but it's a UNC path:

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

Now I could turn this into a local filesystem path using string manipulation, but I suspect there's a cleaner way of doing it buried in the .NET framework somewhere. Anyone know a recommended way of doing this?

(Throwing an exception if the UNC path is not a file:/// URL is absolutely fine in this context)

Shown answered 5/11, 2010 at 15:56 Comment(1)
possible duplicate of Is there a .NET Framework method for converting file URIs to paths with drive letters?Uninterested
V
123

You need to use System.Uri.LocalPath:

string localPath = new Uri("file:///D:/projects/MyApp/MyApp/bin/debug/MyApp.exe").LocalPath;

So if you want the original location of the currently executing assembly:

string localPath = new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath;

The LocalPath includes the assembly's file name, e.g.,

D:\projects\MyApp\MyApp\bin\debug\MyApp.exe

If you want the assembly's directory, then use System.IO.Path.GetDirectoryName():

string localDirectory = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);

which will give you:

D:\projects\MyApp\MyApp\bin\debug
Veneering answered 5/11, 2010 at 16:12 Comment(6)
Note that this does not work when a directory has a '#' in its nameSubstituent
Key here is to use Uri.LocalPath - we had a bug were we used Uri.AbsolutePath, which seems to work until you have a space in the path name!Dioptase
And actually, if you have a path that contains %20for example, even LocalPath wont work. (Which might be acceptable .... local paths containing any %xxescape are rather rare. See this comment on other question: #53297Dioptase
Note: Even with EscapedCodeBase, this answer can be incorrect for some use cases. See @MartinBa's answer below for why and use his solution if you value correctness over ease of development.Swag
A real gem. For some reason Assembly.GetCallingAssembly().CodeBase started returning a different result, or Path.GetDirectoryName() lost the ability to parse that result. Very strange, but atleast there is this solution.Baudoin
It is safer to use new Uri(assembly.EscapedCodeBase).LocalPath, but even this can fail because EscapedCodeBase improperly escapes the path Space( )(h#)(p%20){[a&],t@,p%,+} as Space(%20)(h%23)(p%20)%7B%5Ba%26%5D,t@,p%,+%7D., which unescapes to Space( )(h#)(p ){[a&],t@,p%,+}., (the %20 in the path was changed to a space.)Saxtuba
D
30

Assembly.CodeBase looks promising, but it's a UNC path:

Do note that it is something approximating a file uri, not an UNC path.


You solve this by doing string manipulation by hand. Seriously.

Try all other methods you can find on SO with the following directory (verbatim):

C:\Test\Space( )(h#)(p%20){[a&],t@,p%,+}.,\Release

This is a valid, if somewhat unusual, Windows path. (Some people will have either one of these characters in there paths, and you would want you method to work for all of those, right?)

The available code base (we do not want Location, right?) properties are then (on my Win7 with .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

You will note:

  • CodeBase is not escaped at all, it's just the regular local path prefixed with file:/// and the backslashes replaced. As such, it does not work to feed this to System.Uri.
  • EscapedCodeBase is not escaped completely (I do not know if this is a bug or if this is a shortcoming of the URI scheme):
    • Note how the space character ( ) translates to %20
    • but the %20 sequence also translates to %20! (percent % is not escaped at all)
    • No one can rebuild the original from this mangled form!

For local files (And that's really all I'd care about for the CodeBasestuff, because if the file ain't local, you probably want to use .Location anyway, the following works for me (note that it isn't the prettiest either:

    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);
    }

I am sure one can come up with better solutions, probably one could even come up with a solution that does proper URL en-/decoding of the CodeBase property if it's a local path, but given that one can just strip off the file:/// and be done with it, I'd say this solution stands as good enough, if certainly really ugly.

Dioptase answered 4/2, 2015 at 10:45 Comment(7)
If it's a UNC path it's file://server/share/path/to/assembly.dll (only 2 slashes).Medicaid
@Medicaid - you mean to say that a \\server\... path is encoded with only two slashes, instead of the 3 when having a local drive?Dioptase
@Medicaid - do you know how \\?\UNC\server\share paths are encoded?Dioptase
My testing failed to get a \\?\UNC form to be accepted at all; maybe it's not valid for the underlying API call.Medicaid
That is fantastic, but any reason in particular you would replace / to \ ? Wouldn't that only cause issues with mono on other platforms?Samaritan
@Samaritan no specific reason for '/' vs '\'. My code was for Windows only. Shouldn't a sane Mono Path.GetFullPath implementation handle BS correctly?Dioptase
@Samaritan you could use Path.DirectorySeparatorChar to mitigate that issue.Incunabula
M
5

This should work:

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");

I am using this to be able to log from within the dll libraries using a standalone log4net.config file.

Madame answered 12/6, 2013 at 8:8 Comment(0)
O
4

One more solution, including complex paths:

    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));
    }

and tests:

    [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));
    }
Overstay answered 17/12, 2016 at 21:30 Comment(2)
Nice! But I'd prefer backslashes, so I'd pass the result from GetPathFromUri through Path.GetFullPath.Richards
You'll need to take the Uri.Host into account in order to support network paths.Richards
P
2

Since you tagged this question NUnit, you can also use AssemblyHelper.GetDirectoryName to get the original directory of the executing assembly:

using System.Reflection;
using NUnit.Framework.Internal;
...
string path = AssemblyHelper.GetDirectoryName(Assembly.GetExecutingAssembly())
Pitiable answered 12/3, 2016 at 12:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.