Scott Hanselman

How do I find which directory my .NET Core console application was started in or is running from?

July 28, '20 Comments [4] Posted in DotNetCore
Sponsored By

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

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb
Friday, July 31, 2020 1:48:44 PM UTC
I'm glad to see the IncludeAllContentInSingleFile flag. It's really annoying moving to single file and losing all the stuff you normally expect to be in the bin directory.
Sam
Tuesday, August 04, 2020 2:19:49 AM UTC
Thank you for clarifying, I got this working a week ago with Environment.CurrentDirectory which meant I always had to launch my app from app folder.
Byron Adams
Tuesday, August 04, 2020 4:28:34 PM UTC
I can not wait for .NET 5. It will be great.
Thursday, August 06, 2020 3:04:54 PM UTC
In 3.1 the single exe thing was problematic in that it would get unzipped to a temp area, if anything like the virus checker or something clears a portion or even a single file out of that folder your program will either not work or begin to have unexpected runtime errors when assemblies are accessed that don't exist there anymore. Clicking on the original EXE doesn't "re-unzip" the files. It makes it very unreliable.

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

Chris Poland
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.