Wednesday, February 5, 2020

Set the Working Directory in Visual Studio Code (or better yet, eliminate the requirement on the working directory)

In the last article, I showed what appeared to be a bug in the Visual Studio Code debugger and the .NET Core CLI (command-line interface). The issue stems from the fact that Visual Studio Code and Visual Studio 2019 use different  default values for "working directory" when debugging.

Default "Working Directory":
  • The Visual Studio 2019 debugger uses the executable directory
  • The Visual Studio Code debugger uses the project directory
The result is that an application that relies on the "current working directory" to find files will fail in strange ways. And that's just what the last article showed.
In Visual Studio Code, you can change the debugger / runner working directory in the "launch.json" file.
The code for this article is available on GitHub: jeremybytes-understanding-interfaces-core30. We are specifically using the completed/04-DynamicLoading-Plugin folder. To make things more interesting, some of the code samples use the "master" branch, and some use the "UsingWorkingDirectory" branch. These branches will be noted when code is shown.

Setting the Working Directory in Visual Studio Code
The specific application we are running is "PeopleViewer" -- a WPF application that uses a dynamically-loaded SQL data reader.

Here is the default "launch.json" file that was created by Visual Studio Code for the PeopleViewer project (from the launch.json file on the "UsingWorkingDirectory" branch):


This has a "cwd" setting which stands for "current working directory".

The default for this is the "${workspaceFolder}" which in this case means the project folder for the PeopleViewer application.

We can also see that the "program" setting references the output folder (workspace + bin/Debug/netcoreapp3.1/). This is the program that will be executed when running or debugging from Visual Studio Code.

Side note: .NET Core creates dlls for everything (including the desktop application that we have here). This dll can be executed from this directory on the command line using "dotnet .\PeopleViewer.dll". In addition, the compiler creates an executable ("PeopleViewer.exe") to make it easy to run as a separate application.

We can fix the issue by setting the working directory to be the same as the output folder:


Now the value is "${workspaceFolder}/bin/Debug/netcoreapp3.1". This will set the working directory to the same as our executable.

And if we run the application from Visual Studio Code (with or without debugging), it will work as expected.


SUCCESS!

Sort of. This doesn't eliminate all of the issues that we saw.

Issues from the .NET Core CLI
This doesn't change the problem that we saw in the last article when running the application from the .NET Core CLI.

We can open the command line to the project folder and type "dotnet run":


The application starts


But if we click the button, the application exits (with an unhandled exception).


This shows that we have the same problem with the working directory. The CLI tools do not use the "launch.json" file.

A better solution is to eliminate the reliance on the working directory.

Eliminating the Requirement on the Working Directory
This application uses a SQLite database which is in the "People.db" file on the file system. The application build process makes sure that the "People.db" file is copied into the output folder, so that "PeopleViewer.exe" and "People.db" are both in the same folder.

Here is the configuration for the SQLite EFCore DBContext (from the SQLReader.cs file on the "UsingWorkingDirectory" branch):


This uses the setting "Data Source=People.db". This tells the SQLite driver that we are using a file called "Person.db". The problem is that it looks for that file in the working directory.

Because it looks for the file in working directory, the application runs fine when executed from the application folder (either from File Explorer or from the command line). It also works in the Visual Studio 2019 debugger that uses the application folder as the working directory. And it will work from Visual Studio Code if we set the "cwd" value as shown above.

The Problem
The way that I discovered this was a problem was with Git. When I went to look at changes to the project, I noticed that there was a new "People.db" file in the project folder.


This is what led me to the discovery. The "People.db" file should not be in this folder; the source is somewhere else (an "AdditionalFiles" folder), and it is explicitly copied to the output folder.

So how did it get here?
If SQLite does not find the database file, it creates a new one.
Since this new file is empty, it explains the exception which says it cannot find the "People" table in the database.

Fixing the Issue
What we really want to do is fix the database connection so that it does not rely on the working directory.

A CSV data reader in this same project does not suffer from this problem even though it is also looking for a file (People.txt) in the same folder as the executable. This is because the CSV data reader is more explicit about the file location. Here is the configuration code (from the CSVReader.cs file on the master branch):


This uses the path "AppDomain.CurrentDomain.BaseDirectory" as the path to find the text file. This value is the same folder as the executable, so it works regardless of the current working directory.

To fix the SQL data reader, we can add the same path (from the SQLReader.cs file on the master branch):


Now we have "Data Source={AppDomain.CurrentDomain.BaseDirectory}People.db".

This adds the full directory to the "People.db" data source value, so SQLite will be able to find this file more reliably.

Now if we re-build everything (making sure to explicitly rebuild the "PersonReader.SQL" project so that the output .dll gets to the right folder), the application now runs fine from the .NET Core CLI.


"dotnet run" starts the application.


And if we click the "Dynamic Reader" button...


It works!

Awesomeness and Frustration
One thing that I really like about .NET Core is the set of options we have to build and run. We can use Visual Studio 2019; we can use Visual Studio 2019 for Mac, we can use Visual Studio Code, or we can use the .NET Core CLI.

My goal when building samples in .NET Core is for them to run as consistently as possible in these various development environments. This particular application will not work with Visual Studio 2019 for Mac (since it relies on WPF -- a Windows-only solution), but my website that generates mazes does work on macOS and Linux.

Finding the differences in the working directory among Visual Studio 2019, Visual Studio Code, and the .NET Core CLI was frustrating. I was sidetracked into figuring out why this was working in some places but not others. I "accidentally" ran across a clue by finding the unexpected "Person.db" file. Without that, I was completely lost on the cause. And that's quite a frustration.

But finding this problem also led to a more robust solution. Now that I know that SQLite relies on the current working directory, I can make sure that my connection strings are more specific. This is better overall.

I'll be sure to add more hurdles and solutions as I come across them. Hopefully this will be a help to someone else.

Happy Coding!

No comments:

Post a Comment