Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Environment ApplicationDirectory #41341

Open
eerhardt opened this issue Aug 25, 2020 · 11 comments
Open

Add Environment ApplicationDirectory #41341

eerhardt opened this issue Aug 25, 2020 · 11 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime
Milestone

Comments

@eerhardt
Copy link
Member

eerhardt commented Aug 25, 2020

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:

Gets the pathname of the base directory that the assembly resolver uses to probe for assemblies.

There are scenarios where AppContext.BaseDirectory doesn't return the directory where the application lives, see #40828 (comment) for example - PublishSingleFile in 3.0. Since AppContext.BaseDirectory is defined this way, it makes it unusable for the above purposes.

Proposed API

namespace System
{
    public partial class Environment
    {
        // Returns the directory path of the application.
        public string? ApplicationDirectory { get; }
    }
}

Usage Examples

string configurationFile = Path.Combine(Environment.ApplicationDirectory, "appSettings.json");
@eerhardt eerhardt added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Aug 25, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Meta untriaged New issue has not been triaged by the area owner labels Aug 25, 2020
@eerhardt eerhardt changed the title Environment GetAppDirectory Add Environment ApplicationDirectory Aug 25, 2020
@ericstj ericstj removed the untriaged New issue has not been triaged by the area owner label Aug 26, 2020
@ericstj ericstj added this to the Future milestone Aug 26, 2020
@umbarov
Copy link

umbarov commented Sep 3, 2020

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

@mklement0
Copy link

mklement0 commented Oct 16, 2020

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)).

@eerhardt
Copy link
Member Author

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.

@tannergooding
Copy link
Member

@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)?

@tannergooding
Copy link
Member

Do you know how we would implement this differently from Assembly.GetEntryAssembly().Location?

@jkotas
Copy link
Member

jkotas commented Sep 8, 2022

There are scenarios where AppContext.BaseDirectory doesn't return the directory where the application lives, see #40828 (comment) 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.

@eerhardt
Copy link
Member Author

eerhardt commented Sep 8, 2022

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:

public static string RuntimeIdentifier =>
s_runtimeIdentifier ??= AppContext.GetData("RUNTIME_IDENTIFIER") as string ?? "unknown";

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?

@jkotas
Copy link
Member

jkotas commented Sep 8, 2022

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?

@eerhardt
Copy link
Member Author

eerhardt commented Sep 9, 2022

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.

@jkotas
Copy link
Member

jkotas commented Sep 9, 2022

Sounds good to me.

@dqwork
Copy link

dqwork commented Jul 25, 2024

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. Running the single file exe for windows (from bin/Release/win-x64/publish)
  2. Running the WhereAmI.dll (from bin/Release/win-x64) using dotnet (ie dotnet WhereAmI.dll)
  3. Running the single file produced for linux (from bin/Release/linux-x64/publish) on a linux machine
  4. Running the WhereAmI.dll (from bin/Release/linux-x64) using dotnet (ie dotnet WhereAmI.dll) on a linux machine

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime
Projects
None yet
Development

No branches or pull requests

8 participants