JsonSettings
JsonSettings copied to clipboard
Trying to get the executing path via Assembly.GetEntryAssembly().Location throws an ArgumentException in bundled assemblies.
As the title describes, trying to get the executing path via Assembly.GetEntryAssembly().Location throws an ArgumentException in bundled assemblies, because Location returns an empty string.
See: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location?#remarks
This happened to my project because i'm using the new "single file" feature of net 5 that generates a single bundled assembly file by merging all the libraries into the exe.

As you can see Location is empty and even CodeBase throws an exception, the same happens with Assembly.GetExecutingAssembly().Location.
For getting the executing directory is suggested to use AppContext.BaseDirectory.
Just changed ExecutingDirectory to this:
public static DirectoryInfo `ExecutingDirectory` => new DirectoryInfo(Path.GetDirectoryName(AppContext.BaseDirectory));
ExecutingExe is no more needed, but you can get it via Path.Combine(AppContext.BaseDirectory, Assembly.GetEntryAssembly().ManifestModule.ScopeName).
See: https://github.com/dotnet/corert/issues/5467#issuecomment-369202524
To debug the issue is a bit tricky: 1 Publish inside a new net 5 Wpf app, select Debug in configuration, then check "produce single file".

2 Copy the generated files with all the pdb files inside the debug folder (Debug/net5.0...). 3 Run debug.
I have made a comparison about this before First of all, using Assembly Method is not good because of the different values it has in .NET Framework, .NET Core and .Net 5 and single file mode (This test is written in a DLL library and run in a WPF program in .Net Framework, .Net Core 3.x and .Net 5 in Single File and portable mode)
note: GetProccess is a native method
| SDK | Method | Portable | Publish | Status |
|---|---|---|---|---|
| .Net 4.5 | GetCallingAssembly | bin\Debug\net45\WpfApp12.exe | :heavy_check_mark: | |
| .Net 4.5 | GetEntryAssembly | bin\Debug\net45\WpfApp12.exe | :heavy_check_mark: | |
| .Net 4.5 | GetExecutablePath | bin\Debug\net45\WpfApp12.exe | :heavy_check_mark: | |
| .Net 4.5 | GetExecutingAssembly | bin\Debug\net45\WpfCustomControlLibrary1.dll | :x: | |
| .Net 4.5 | GetProccess | bin\Debug\net45\WpfApp12.exe | :heavy_check_mark: |
bin\Debug\netcoreapp3.1\
| SDK | Method | Portable | Publish | Status |
|---|---|---|---|---|
| Core 3.1 | GetCallingAssembly | WpfApp12.dll | qshmjn44.1xl\WpfApp12.dll | :x: |
| Core 3.1 | GetEntryAssembly | WpfApp12.dll | qshmjn44.1xl\WpfApp12.dll | :x: |
| Core 3.1 | GetExecutablePath | WpfApp12.exe | publish\WpfApp12.exe | :heavy_check_mark: |
| Core 3.1 | GetExecutingAssembly | WpfCustomControlLibrary1.dll | qshmjn44.1xl\WpfCustomControlLibrary1.dll | :x: |
| Core 3.1 | GetProccess | WpfApp12.exe | publish\WpfApp12.exe | :heavy_check_mark: |
bin\Debug\net5.0-windows\
| SDK | Method | Portable | Publish | Status |
|---|---|---|---|---|
| .Net 5 | GetCallingAssembly | WpfApp12.dll | :x: | |
| .Net 5 | GetEntryAssembly | WpfApp12.dll | :x: | |
| .Net 5 | GetExecutablePath | WpfApp12.exe | publish\WpfApp12.exe | :heavy_check_mark: |
| .Net 5 | GetExecutingAssembly | WpfCustomControlLibrary1.dll | :x: | |
| .Net 5 | GetProccess | WpfApp12.exe | publish\WpfApp12.exe | :heavy_check_mark: |
Also, using the AppContext.BaseDirectory is not very convenient because it gives different values this is .Net 3
CurrentDirectory : C:\Users\Mahdi\source\repos\net3.1\net3.1\bin\Release\netcoreapp3.1\publish
AppDomain : C:\Users\Mahdi\AppData\Local\Temp\.net\net3.1\vy4mio3s.mik\
AppContext : C:\Users\Mahdi\AppData\Local\Temp\.net\net3.1\vy4mio3s.mik\
and this is .Net 5
CurrentDirectory : C:\Users\Mahdi\source\repos\net3.1\net5\bin\Release\net5.0-windows\publish
AppDomain : C:\Users\Mahdi\source\repos\net3.1\net5\bin\Release\net5.0-windows\publish\
AppContext : C:\Users\Mahdi\source\repos\net3.1\net5\bin\Release\net5.0-windows\publish\
The best solution I have found is to use the native method which gives the same value in all modes and frameworks
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
static extern uint GetModuleFileName(IntPtr hModule, System.Text.StringBuilder lpFilename, int nSize);
static readonly int MAX_PATH = 255;
public static string GetExecutablePath() {
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) {
var sb = new System.Text.StringBuilder(MAX_PATH);
GetModuleFileName(IntPtr.Zero, sb, MAX_PATH);
return sb.ToString();
}
else {
return System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
}
}
This solves it for Windows, I do wonder about single-file on Linux.
I did not test but it should work for linux
System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
We have 3 ways to resolve the executing exe's path so we can resolve paths like "justfile.json".
If All fail, we fallback to Current Directory provided by Environment.CurrentDirectory