37

How should a single-file .Net Core 3.0 Web API application be configured to look for the appsettings.json file that is in the same directory that the single-file application is built to?

After running

dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true

The directory looks like this:

XX/XX/XXXX  XX:XX PM    <DIR>          .
XX/XX/XXXX  XX:XX PM    <DIR>          ..
XX/XX/XXXX  XX:XX PM               134 appsettings.json
XX/XX/XXXX  XX:XX PM        92,899,983 APPNAME.exe
XX/XX/XXXX  XX:XX PM               541 web.config
               3 File(s)     92,900,658 bytes

However, attempting to run APPNAME.exe results in the following error

An exception occurred, System.IO.FileNotFoundException: The configuration file 'appsettings.json' was not found and is not optional. The physical path is 'C:\Users\USERNAME\AppData\Local\Temp\.net\APPNAME\kyl3yc02.5zs\appsettings.json'.
   at Microsoft.Extensions.Configuration.FileConfigurationProvider.HandleException(ExceptionDispatchInfo info)
   at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load(Boolean reload)
   at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load()
   at Microsoft.Extensions.Configuration.ConfigurationRoot..ctor(IList`1 providers)
   at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.BuildCommonServices(AggregateException& hostingStartupErrors)
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
...

I tried solutions from a similar, but distinct question, as well as other Stack Overflow questions.

I attempted to pass the following to SetBasePath()

  • Directory.GetCurrentDirectory()

  • environment.ContentRootPath

  • Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)

Each led to the same error.

The root of the issue is that the PublishSingleFile binary is unzipped and run from a temp directory.

In the case of this single file app, the location it was looking appsettings.json was the following directory:

C:\Users\USERNAME\AppData\Local\Temp\.net\APPNAME\kyl3yc02.5zs

All of the above methods point to the place that the file is unzipped to, which is different than the place it was run from.

Jason Yandell
  • 931
  • 1
  • 7
  • 9

4 Answers4

46

I found an issue on GitHub here titled PublishSingleFile excluding appsettings not working as expected.

That pointed to another issue here titled single file publish: AppContext.BaseDirectory doesn't point to apphost directory

In it, a solution was to try Process.GetCurrentProcess().MainModule.FileName

The following code configured the application to look at the directory that the single-executable application was run from, rather than the place that the binaries were extracted to.

config.SetBasePath(GetBasePath());
config.AddJsonFile("appsettings.json", false);

The GetBasePath() implementation:

private string GetBasePath()
{
    using var processModule = Process.GetCurrentProcess().MainModule;
    return Path.GetDirectoryName(processModule?.FileName);
}
mason
  • 31,774
  • 10
  • 77
  • 121
Jason Yandell
  • 931
  • 1
  • 7
  • 9
  • 1
    This answer, plus @ronald-swaine below is perfect. The appsettings are excluded from the bundled exe, and the publishing task places the appsettings files alongside the bundled exe. – Aaron Hudon Feb 03 '20 at 22:56
  • I had the same error. My problem was actually at the deployment stage - I have changed appSettings.json to appsettings.json but Github commit did not care about that case change (needed to change local settings, just something to check) – user685590 Aug 25 '20 at 03:50
  • 1
    I've been fighting this for a few days now. Thanks for your solution. I do feel it's a bit of an odd workaround for something that should "just work". And everything behaves much differently in .NET 5 vs .NET Core 3.1. This: Path.Combine(AppContext.BaseDirectory, Assembly.GetExecutingAssembly().ManifestModule.Name) works in the VS2019 preview in debug, but after publishing to a single file (no trimming, no Ready-To-Run) it returns " System.IO.FileNotFoundException: The system cannot find the file specified. (0x80070002)". Seems something is missing to me... – John Baughman Sep 03 '20 at 20:36
  • This needs to be fixed. We should not write the code to determine also the Environment and read up/aggregate each appsettings json files. Not good. This is not a good work around. Must look up and update or create a new GitHub issue at MS. – hB0 Sep 29 '20 at 13:45
  • ever changing behavior of .net core system call, changes btw sub-versions of net core https://github.com/dotnet/runtime/issues/3704#issuecomment-607103702 meaning this runtime is not-usable under shared environment setup unlike full .net fw version (which does not break main fw apis/services) – hB0 Oct 01 '20 at 20:39
  • MS: "This issue is fixed in .net5" - So .NET 3.1 is let down? REF: https://github.com/dotnet/runtime/issues/3704#issuecomment-651210020 – hB0 Oct 06 '20 at 04:38
  • 1
    Thanks for that solution. Much Appreciated. You put me out of my misery!! A simple click on a checkbox to turn on Single File turned into a week of arsing around trying to find out why my app worked perfectly in visual studio but couldn't find its appsettings when deployed and run from a different folder.. Gahhh!! – Vida Oct 24 '22 at 11:50
19

If you're okay with having files used at runtime outside of the executable, then you could just flag the files you want outside, in csproj. This method allows for live changes and such in a known location.

<ItemGroup>
    <None Include="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      <CopyToPublishDirectory>Always</CopyToPublishDirectory>
      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
    </None>
    <None Include="appsettings.Development.json;appsettings.QA.json;appsettings.Production.json;">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      <CopyToPublishDirectory>Always</CopyToPublishDirectory>
      <DependentUpon>appsettings.json</DependentUpon>
      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
    </None>
  </ItemGroup>

  <ItemGroup>
    <None Include="Views\Test.cshtml">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
    </None>
  </ItemGroup>

If this is not acceptable, and must have ONLY a single file, I pass the single-file-extracted path as the root path in my host setup. This allows configuration, and razor (which I add after), to find its files as normal.

// when using single file exe, the hosts config loader defaults to GetCurrentDirectory
            // which is where the exe is, not where the bundle (with appsettings) has been extracted.
            // when running in debug (from output folder) there is effectively no difference
            var realPath = Directory.GetParent(System.Reflection.Assembly.GetExecutingAssembly().Location).FullName;

            var host = Host.CreateDefaultBuilder(args).UseContentRoot(realPath);

Note, to truly make a single file, and no PDB, you'll also need:

<DebugType>None</DebugType>
  • How does Views\Test.cshtml in your example get on the target computer? I have image and dictionary files that I need to open based on Culture, – Paul Cohen Nov 09 '19 at 07:43
  • @PaulCohen I would typically deploy all files from the published output location using SCP. With only the changes in csproj (first example) any APIs that are using the default root content directory will need to have their files available in the working directory. It sounds like you need to use the 2nd example, to get the real path of the extracted content, so that way you can access the images from a full path, or by having the content root set correctly so that ~/... paths to images can be used in razor. – Ronald Swaine Nov 13 '19 at 15:01
  • My background is in embedded applications so a single binary is usually burned into rom. What I am realizing is there the Exe that I share in some kind of self extracting “zip like” file. All my data files are in there and when the application is run everything is extracted to a temp directly. If I find that I can find my data files. It also means I need some logic so I can Debug in VS where the data is somewhere else. – Paul Cohen Nov 13 '19 at 23:37
  • 1
    I have this setup currently, and it randomly stopped finding the files. – Going-gone Jan 27 '20 at 22:28
  • "var realPath" You rock the suburbs. I've added an answer to expound on where to set the value.......but wow..this was a lifesaver. Thanks. – granadaCoder Jul 23 '20 at 21:51
  • 1
    First solution, keeping appsettings file outside is what I use and it does not work as expected. When you have single file, it extracts on to a temp location under User profile in windows, and there it also has appsettings file extracted as well as to the original location; but the run-path at exec time is set to the temp directory's location; and therefore kills the purpose. SingleFile is just a cosmetic change; rather I would avoid it at the server environment. – hB0 Oct 01 '20 at 20:26
2

My application is on .NET Core 3.1, is published as a single file and runs as a Windows Service (which may or may not have an impact on the issue).

The proposed solution with Process.GetCurrentProcess().MainModule.FileName as the content root works for me, but only if I set the content root in the right place:

This works:

Host.CreateDefaultBuilder(args)
    .UseWindowsService()
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseContentRoot(...);
        webBuilder.UseStartup<Startup>();
    });

This does not work:

Host.CreateDefaultBuilder(args)
    .UseWindowsService()
    .UseContentRoot(...)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    });

0

this is the "piggy back" answer(s) area.

First, upvote the above answer by "RS" that I reference in this answer. That was the magic.

Short answer is "use RS's answer AND set that value in all the right places.". I show the 2 places to SET the values below.

My specific ADDITION (not mentioned anywhere else) is:

            IConfigurationBuilder builder = new ConfigurationBuilder()
            /* IMPORTANT line below */
                    .SetBasePath(realPath)

Longer answer is:

I needed the above answers AND I have some additions.

In my output (i'll show code later), here is the difference between the 2 answers above.

    GetBasePath='/mybuilddir/myOut'

  
  realPath='/var/tmp/.net/MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne/jhvc5zwc.g25'

where '/mybuilddir/myOut' was the location here I published my single file..in my docker definition file.

GetBasePath did NOT work when using PublishSingleFile

"realPath" was the way I finally got it to work. Aka, the answer above. : How can I get my .NET Core 3 single file app to find the appsettings.json file?

and when you see the value of "realPath"...then it all makes sense. the singleFile is being extracted ~somewhere....and RS figured out the magic sauce on where that extraction place is.

I will show my entire Program.cs, that will give context to everything.

Note, I had to set "realPath" in TWO places.

I marked the important things with

/* IMPORTANT

Full code below, which (again) borrows from RS's answer : How can I get my .NET Core 3 single file app to find the appsettings.json file?

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using Serilog;

namespace MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne
{
    public static class Program
    {
        public static async Task<int> Main(string[] args)
        {
            /* easy concrete logger that uses a file for demos */
            Serilog.ILogger lgr = new Serilog.LoggerConfiguration()
                .WriteTo.Console()
                .WriteTo.File("MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne.log.txt", rollingInterval: Serilog.RollingInterval.Day)
                .CreateLogger();

            try
            {
                /* look at the Project-Properties/Debug(Tab) for this environment variable */
                string environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
                Console.WriteLine(string.Format("ASPNETCORE_ENVIRONMENT='{0}'", environmentName));
                Console.WriteLine(string.Empty);

                string basePath = Directory.GetCurrentDirectory();
                basePath = GetBasePath();

                Console.WriteLine(string.Format("GetBasePath='{0}'", basePath));
                Console.WriteLine(string.Empty);

                // when using single file exe, the hosts config loader defaults to GetCurrentDirectory
                // which is where the exe is, not where the bundle (with appsettings) has been extracted.
                // when running in debug (from output folder) there is effectively no difference
                /* IMPORTANT 3 lines below */
                string realPath = Directory.GetParent(System.Reflection.Assembly.GetExecutingAssembly().Location).FullName;
                Console.WriteLine(string.Format("realPath='{0}'", realPath));
                Console.WriteLine(string.Empty);


                IConfigurationBuilder builder = new ConfigurationBuilder()
                /* IMPORTANT line below */
                        .SetBasePath(realPath)
                        .AddJsonFile("appsettings.json")
                        .AddJsonFile($"appsettings.{environmentName}.json", true, true)
                        .AddEnvironmentVariables();

                IConfigurationRoot configuration = builder.Build();


                IHost host = Host.CreateDefaultBuilder(args)
                /* IMPORTANT line below */
                      .UseContentRoot(realPath)
                    .UseSystemd()
                    .ConfigureServices((hostContext, services) => AppendDi(services, configuration, lgr)).Build();

                await host.StartAsync();

                await host.WaitForShutdownAsync();
            }
            catch (Exception ex)
            {
                string flattenMsg = GenerateFullFlatMessage(ex, true);
                Console.WriteLine(flattenMsg);
            }

            Console.WriteLine("Press ENTER to exit");
            Console.ReadLine();

            return 0;
        }

        private static string GetBasePath()
        {
            using var processModule = System.Diagnostics.Process.GetCurrentProcess().MainModule;
            return Path.GetDirectoryName(processModule?.FileName);
        }

        private static string GenerateFullFlatMessage(Exception ex)
        {
            return GenerateFullFlatMessage(ex, false);
        }

        private static void AppendDi(IServiceCollection servColl, IConfiguration configuration, Serilog.ILogger lgr)
        {
            servColl
                .AddSingleton(lgr)
                .AddLogging();

            servColl.AddHostedService<TimedHostedService>(); /* from https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio and/or https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/fundamentals/host/hosted-services/samples/3.x/BackgroundTasksSample/Services/TimedHostedService.cs */

            servColl.AddLogging(blder =>
            {
                blder.AddConsole().SetMinimumLevel(LogLevel.Trace);
                blder.SetMinimumLevel(LogLevel.Trace);
                blder.AddSerilog(logger: lgr, dispose: true);
            });

            Console.WriteLine("Using UseInMemoryDatabase");
            servColl.AddDbContext<WorkerServiceExampleOneDbContext>(options => options.UseInMemoryDatabase(databaseName: "WorkerServiceExampleOneInMemoryDatabase"));
        }

        private static string GenerateFullFlatMessage(Exception ex, bool showStackTrace)
        {
            string returnValue;

            StringBuilder sb = new StringBuilder();
            Exception nestedEx = ex;

            while (nestedEx != null)
            {
                if (!string.IsNullOrEmpty(nestedEx.Message))
                {
                    sb.Append(nestedEx.Message + System.Environment.NewLine);
                }

                if (showStackTrace && !string.IsNullOrEmpty(nestedEx.StackTrace))
                {
                    sb.Append(nestedEx.StackTrace + System.Environment.NewLine);
                }

                if (ex is AggregateException)
                {
                    AggregateException ae = ex as AggregateException;

                    foreach (Exception aeflatEx in ae.Flatten().InnerExceptions)
                    {
                        if (!string.IsNullOrEmpty(aeflatEx.Message))
                        {
                            sb.Append(aeflatEx.Message + System.Environment.NewLine);
                        }

                        if (showStackTrace && !string.IsNullOrEmpty(aeflatEx.StackTrace))
                        {
                            sb.Append(aeflatEx.StackTrace + System.Environment.NewLine);
                        }
                    }
                }

                nestedEx = nestedEx.InnerException;
            }

            returnValue = sb.ToString();

            return returnValue;
        }
    }
}

and my toplayer csproj contents:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <!-- allows one line of code to get a txt file logger #simple #notForProduction -->
    <PackageReference Include="Serilog" Version="2.9.0" />
    <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
    <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
    <PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.6" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
    <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="3.1.6" />
  </ItemGroup>



  <ItemGroup>
    <None Update="appsettings.Development.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>



</Project>

and my docker file for kicks:

# See https://hub.docker.com/_/microsoft-dotnet-core-sdk/
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS buildImage
WORKDIR /mybuilddir


# Copy sln and csprojs and restore as distinct layers
COPY ./src/Solutions/MyCompany.MyExamples.WorkerServiceExampleOne.sln ./src/Solutions/

COPY ./src/ConsoleOne/*.csproj ./src/ConsoleOne/


RUN dotnet restore ./src/Solutions/MyCompany.MyExamples.WorkerServiceExampleOne.sln

COPY ./src ./src



RUN dotnet publish "./src/ConsoleOne/MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne.csproj" -c Release -o myOut -r linux-x64 /p:PublishSingleFile=true /p:DebugType=None  --framework netcoreapp3.1

# See https://hub.docker.com/_/microsoft-dotnet-core-runtime/
FROM mcr.microsoft.com/dotnet/core/runtime:3.1 AS runtime
WORKDIR /myrundir
COPY --from=buildImage /mybuilddir/myOut ./

# this line is wrong for  PublishSingleFile  ### ENTRYPOINT ["dotnet", "MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne.dll"]

#below is probably right...i was still working on this at time of posting this answer
 ./myOut/MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne
granadaCoder
  • 26,328
  • 10
  • 113
  • 146