How do I find which directory my .NET Core console application was started in or is running from?
I got a great question emailed to me today. And while I could find the answer and email them back, I also have a limited number of keystrokes. Plus, every question that moves knowledge forward is a gift, and I don't want to ignore a gift. So instead, I've email my new friend a link to this blog!
Here is their question:
The tl;dr question: how do I find which directory the app was started in?
I have a CLI app in .NET Core 3 that is supposed to sit in your apps folder you have in your path, do something when you run it, then exit.
Unfortunately, it needs a single line of configuration (an API key), which store in a text file. I want to keep the app portable (ie. avoid going into other directories), so I want to store the config file right next to the executable.
And since I want it to be small and easily distributed, I set up .NET to merge and prune the EXE on build. (I think I got the idea from your blog btw, thanks! :) It is a simple app that does a single task, so I figure it should be one EXE, not 50 megabytes in 80 files.
And there the problem lies: If the config file is right next to the exe, I need to know where the exe is located. BUT, it seems that when I have the entire app built into a single EXE like this, .NET actually extracts the embedded DLL to some temporary location in my filesystem, then runs it from there. Every method for finding the startup assembly's location I have found, either by googling or exploring with reflection while it runs, only gives me the location in the temp directory, not the one where the app was actually launched from. The app then attempts to load the config file from this temp directory, which obviously fails.
Let's break this problem down:
- .NET 3.1 LTS (long term support) has a cool feature where you can ship a single EXE with no dependencies. You ship "myapp.exe" and it just works. One file, no install.
- When you run the EXE in .NET 3.1, the system "unzips" the app and its dependencies into a temp folder.
- If you have a config file like myapp.exe.config, you'd like the user to keep that file NEXT TO THE EXE. That way you have two files next to each other and it's simple.
- But how do you find this config file if you got started from a random temp folder?
Here's how things work in .NET Core 3.1:
using System;
using System.Diagnostics;
using System.IO;
namespace testdir
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Launched from {Environment.CurrentDirectory}");
Console.WriteLine($"Physical location {AppDomain.CurrentDomain.BaseDirectory}");
Console.WriteLine($"AppContext.BaseDir {AppContext.BaseDirectory}");
Console.WriteLine($"Runtime Call {Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName)}");
}
}
}
And here's the output of this app when started from elsewhere. Look carefully at the paths:
~\Desktop> .\testdir\bin\Debug\netcoreapp3.1\win-x64\publish\testdir.exe
Launched from C:\Users\scott\Desktop
Physical location C:\Users\scott\AppData\Local\Temp\.net\testdir\30gxvy1b.yq3\
AppContext.BaseDir C:\Users\scott\AppData\Local\Temp\.net\testdir\30gxvy1b.yq3\
Runtime Call C:\Users\scott\Desktop\testdir\bin\Debug\netcoreapp3.1\win-x64\publish
You'll note that when on .NET 3.1 if you want to get your "original birth location" you have to do a little runtime dance. Starting with your current process, then digging into the MainModules's filename, then getting that file's Directory. You'll want to catch that at startup as it's not a super cheap call you want to make all the time.
How does it work in .NET Core 5 (> preview 7)? Well, because the assemblies are embedded and loaded from memory in 5, so like any other in-memory assembly, they don't have a location. Any use of Assembly.Location is going to be suspect.
NOTE: There is some work happening on an analyzer that would flag weird usage when you're using Single File publish, so you won't have to remember any of this. Additionally, if you are on .NET 5 and you want to have the .NET 3.1 temp file behavior, you can use a compatibility property called IncludeAllContentInSingleFile.
Here's the same app, running in .NET 5 as a single EXE and unfolding directly into memory (no temp files):
Launched from C:\Users\scott\Desktop
Physical location C:\Users\scott\Desktop\testdir\bin\Debug\net5.0\win-x64\publish\
AppContext.BaseDir C:\Users\scott\Desktop\testdir\bin\Debug\net5.0\win-x64\publish\
Runtime Call C:\Users\scott\Desktop\testdir\bin\Debug\net5.0\win-x64\publish
You'll note here that you get the behavior you expect with each call.
Here's the answer then:
- You should use
AppContext.BaseDirectory
on .NET 5 to get the truth - You can use the runtime calls (and cache them) with MainModule (the last line in the example) for .NET 3.1.
Hope this helps!
Sponsor: Never miss a bug — send alerts to Slack or email, and find problems before your customers do, using Seq 2020.1.
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
The direction moved for .NET 5 might be a slower starting for large projects (if the entire runtime is bundled with it) but at least sounds more reliable (I don't know that it is slower, just assumed it was if it was extracting the entire set into memory every time from the container).
you end some actually advanced papers. https://write-essayforme.com/
and located that it's really informative. I am gonna be careful for brussels.
I will appreciate if you proceed this in future.
Many people will likely be benefited out of your writing. Cheers!
Comments are closed.