Open eerhardt opened 4 years ago
Maybe a short version: AppDirectory
? Like appSettings.json
, System.AppContext
System.AppDomain
. For app
consistency.
public string? AppDirectory { get; }
...
string configFile = Path.Combine(Environment.AppDirectory, "appSettings.json");
I suspect that is the plan anyway, but just to spell it out (correct me, if I'm wrong):
The return value will be the full, physical path - with symlinks, if any, resolved - of the directory in which the executable is located, so that, say, an executable /path/to/exe
that is symlinked to /usr/bin/exe
and invoked as such still reports /path/to
.
[Applies only to regular, application-specific executables - see next comment]
(Expressed in terms of the upcoming Environment.ProcessPath
property, which returns the executable's full, physical file path: Path.GetDirectoryName(Environment.ProcessPath)
).
The return value will be the full, physical path - with symlinks, if any, resolved - of the directory in which the executable is located, so that, say, an executable /path/to/exe that is symlinked to /usr/bin/exe and invoked as such still reports /path/to.
I would assume all symlinks would be followed, yes.
(Expressed in terms of the upcoming Environment.ProcessPath property, which returns the executable's full, physical file path: Path.GetDirectoryName(Environment.ProcessPath).
No, this wouldn't be the case for all scenarios. One easy example is when you use dotnet.exe
to load an application. dotnet.exe bin\Debug\net5.0\MyApp.dll
. Your proposal would have Environment.ApplicationDirectory
return the location of the dotnet.exe
executable. But the intention is for this API to return the full path of where MyApp.dll
is located.
Other scenarios where your above definition is incorrect could potentially be Mobile/Xamarin scenarios.
My thought is that the Environment.ApplicationDirectory
is the full, physical file path to where the logical .NET "EntryPoint" assembly is located on disk. Even if the actual "EntryPoint" assembly is packaged in some sort of aggregate package - like it is in "single file" applications. If the "EntryPoint" assembly wasn't loaded from some physical file (for example Blazor WASM), then Environment.ApplicationDirectory
returns null
.
@eerhardt, so this basically gets the path to the folder containing the file that has the managed Main
method (whether that remains implemented in IL or compiled down to native directly for AOT scenarios)?
Do you know how we would implement this differently from Assembly.GetEntryAssembly().Location
?
There are scenarios where AppContext.BaseDirectory doesn't return the directory where the application lives, see https://github.com/dotnet/runtime/issues/40828#issuecomment-676519769 for example
We have deprecated the "expand to temp single-file" publishing scheme that had this problem. The default single-file experience does not have this problem anymore. AppContext.BaseDirectory
returns the directory where the application lives.
Do you know how we would implement this differently from Assembly.GetEntryAssembly().Location?
This implementation would not work for single-file where GetEntryAssembly().Location
is empty string.
Do you know how we would implement this differently from Assembly.GetEntryAssembly().Location?
My thinking was that it would be an AppContext property that was passed from the host. Similar to how RuntimeInformation.RuntimeIdentifier
is implemented:
We definitely wouldn't want to use Assembly.Location, because we want this API to work in single-file and NativeAOT.
so this basically gets the path to the folder containing the file that has the managed Main method (whether that remains implemented in IL or compiled down to native directly for AOT scenarios)?
I think yes (more or less). For "mobile" scenarios or some other app scenarios, it might not be exactly that definition.
The default single-file experience does not have this problem anymore. AppContext.BaseDirectory returns the directory where the application lives.
Can we update documentation then? That way in the future we can guarantee that's what AppContext.BaseDirectory
does?
My thinking was that it would be an AppContext property that was passed from the host
How would the hosts that we maintain compute the value? What are the cases where this would be different from AppContext.BaseDirectory
?
Can we update documentation then?
The documentation has a text to clarify this "In .NET 5 and later versions, for bundled assemblies, the value returned is the containing directory of the host executable." What else would you like to see the documentation to say?
What else would you like to see the documentation to say?
I would like to see the Summary
change:
- Gets the file path of the base directory that the assembly resolver uses to probe for assemblies.
+ Gets the file path of the base directory of the application.
That way in the future when we have some new app model (shadow-copy? another extract assemblies thing?) that probes for assemblies in a different directory than where the application lives, we ensure that AppContext.BaseDirectory
points to where consumers expect it to point to. The reason given why it wasn't fixed for the 3.x extract-to-temp single file app model was because the documentation says the API is for the assembly resolver. This broke a LOT of things that expected to use this API for reading files in the application directory. I'm trying to prevent this from happening again in the future.
Sounds good to me.
Just wanted to add to this conversation and provide an easy to repro example.
I just wanted to share the results of some testing I did and hopefully add some to this issue... or get told what I'm doing wrong thats causing this behavior :).
This is going to be pretty long, because my company won't let me post a gist...
Created a new dotnet 8 console app in Visual Studio 2022 (called WhereAmI)
WhereAmI.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>false</SelfContained>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<IncludeNativeLibrariesInSingleFile>true</IncludeNativeLibrariesInSingleFile>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
</PropertyGroup>
</Project>
Program.cs
using System.Diagnostics;
using System.Reflection;
using System;
namespace WhereAmI
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Running on {Environment.OSVersion.Platform}");
Console.WriteLine($"Dotnt Details:");
Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
var appContextBaseDir = AppContext.BaseDirectory;
var envProcessPath = Environment.ProcessPath;
var execAssembly = Assembly.GetExecutingAssembly()?.Location;
var entryAssembly = Assembly.GetEntryAssembly()?.Location;
var processPath = Process.GetCurrentProcess().MainModule.FileName;
Console.WriteLine($"AppContext.BaseDirectory: {appContextBaseDir}");
Console.WriteLine($"Environment.ProcessPath: {envProcessPath}");
Console.WriteLine($"Executing Assembly Path: {execAssembly}");
Console.WriteLine($"Entry Assembly Path: {entryAssembly}");
Console.WriteLine($"Current Process Filename: {processPath}");
}
}
}
I built the project using dotnet build -c Release
I then ran
dotnet publish -r win-x64
and
dotnet publish -r linux-x64
Below are the results from testing
1. Windows Single File Exe
.\WhereAmI.exe
Running on Win32NT
Dotnt Details:
.NET 8.0.3
AppContext.BaseDirectory: C:\Users\******\AppData\Local\Temp\.net\WhereAmI\vh36hwyJjwSNejBA4Srlnov7e8lMgjc=\
Environment.ProcessPath: C:\Users\******\source\repos\WhereAmI\WhereAmI\bin\Release\win-x64\publish\WhereAmI.exe
Executing Assembly Path: C:\Users\******\AppData\Local\Temp\.net\WhereAmI\vh36hwyJjwSNejBA4Srlnov7e8lMgjc=\WhereAmI.dll
Entry Assembly Path: C:\Users\******\AppData\Local\Temp\.net\WhereAmI\vh36hwyJjwSNejBA4Srlnov7e8lMgjc=\WhereAmI.dll
Current Process Filename: C:\Users\******\source\repos\WhereAmI\WhereAmI\bin\Release\win-x64\publish\WhereAmI.exe
2. Windows run with dotnet
dotnet WhereAmI.dll
Running on Win32NT
Dotnt Details:
.NET 8.0.3
AppContext.BaseDirectory: C:\Users\******\source\repos\WhereAmI\WhereAmI\bin\Release\win-x64\
Environment.ProcessPath: c:\program files\dotnet\dotnet.exe
Executing Assembly Path: C:\Users\******\source\repos\WhereAmI\WhereAmI\bin\Release\win-x64\WhereAmI.dll
Entry Assembly Path: C:\Users\******\source\repos\WhereAmI\WhereAmI\bin\Release\win-x64\WhereAmI.dll
Current Process Filename: c:\program files\dotnet\dotnet.exe
3. Linux Single File
./WhereAmI
Running on Unix
Dotnt Details:
.NET 8.0.4
AppContext.BaseDirectory: /home/******/.net/WhereAmI/SoSD6MD9IYup9vfwv0YpqXytL2Zw2Fc=/
Environment.ProcessPath: /home/******/dotnet-testing/WhereAmI
Executing Assembly Path: /home/******/.net/WhereAmI/SoSD6MD9IYup9vfwv0YpqXytL2Zw2Fc=/WhereAmI.dll
Entry Assembly Path: /home/******/.net/WhereAmI/SoSD6MD9IYup9vfwv0YpqXytL2Zw2Fc=/WhereAmI.dll
Current Process Filename: /home/******/dotnet-testing/WhereAmI
4. Linux Run With dotnet
dotnet WhereAmI.dll
Running on Unix
Dotnt Details:
.NET 8.0.4
AppContext.BaseDirectory: /home/******/dotnet-testing/non-contained/
Environment.ProcessPath: /home/******/.dotnet/dotnet
Executing Assembly Path: /home/******/dotnet-testing/non-contained/WhereAmI.dll
Entry Assembly Path: /home/******/dotnet-testing/non-contained/WhereAmI.dll
Current Process Filename: /home/******/.dotnet/dotnet
As you can see from the results here there is no consistent approach to getting the location of the executable. Running in different ways gives different results making it quite awkward to reliably get the location and load things like config files, images or any other external, run time loaded resource.
Hope this was helpful in someway
Background and Motivation
The path of the current application is often needed to load files in your application directory. For example,
appSettings.json
configuration files, or image files.This API is needed more than before now that we support single-file publishing and assemblies do not have physical file paths anymore. See https://github.com/dotnet/designs/blob/master/accepted/2020/form-factors.md#single-file for details.
We have
AppContext.BaseDirectory
, but that API is documented as:There are scenarios where
AppContext.BaseDirectory
doesn't return the directory where the application lives, see https://github.com/dotnet/runtime/issues/40828#issuecomment-676519769 for example - PublishSingleFile in 3.0. SinceAppContext.BaseDirectory
is defined this way, it makes it unusable for the above purposes.Proposed API
Usage Examples