Quantcast
Channel: Scott Hanselman's Blog
Viewing all articles
Browse latest Browse all 1148

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

$
0
0

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.



© 2020 Scott Hanselman. All rights reserved.
     

Viewing all articles
Browse latest Browse all 1148

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>