Как я могу заставить однофайловое приложение .NET Core 3 найти файл appsettings.json?

Как следует настроить однофайловое приложение веб-API.Net Core 3.0 для поиска appsettings.json файл, который находится в том же каталоге, что и однофайловое приложение?

После запуска

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

Каталог выглядит так:

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

Однако при попытке запустить APPNAME.exe приводит к следующей ошибке

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

Я пробовал решения из аналогичного, но отдельного вопроса, а также из других вопросов о переполнении стека.

Я попытался передать следующее в SetBasePath()

  • Directory.GetCurrentDirectory()

  • environment.ContentRootPath

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

Каждый приводил к одной и той же ошибке.

Корень проблемы в том, что PublishSingleFile двоичный файл распаковывается и запускается из temp каталог.

В случае этого однофайлового приложения местоположение, которое оно искало appsettings.json был следующий каталог:

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

Все вышеперечисленные методы указывают на место, в которое файл был распакован, а не на то место, откуда он был запущен.

4 ответа

Решение

Я нашел вопрос на GitHub здесь под названиемPublishSingleFile excluding appsettings not working as expected.

Это указал на другой вопрос здесь под названиемsingle file publish: AppContext.BaseDirectory doesn't point to apphost directory

В нем решением было попробовать Process.GetCurrentProcess().MainModule.FileName

Следующий код настраивает приложение для просмотра каталога, из которого было запущено одноисполняемое приложение, а не места, в которое были извлечены двоичные файлы.

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

В GetBasePath() реализация:

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

Если вас устраивает использование файлов во время выполнения вне исполняемого файла, то вы можете просто пометить файлы, которые хотите извне, в csproj. Этот метод позволяет вносить изменения в реальном времени и тому подобное в известном месте.

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

Если это неприемлемо и должен иметь ТОЛЬКО один файл, я передаю путь, извлеченный из одного файла, в качестве корневого пути в настройках моего хоста. Это позволяет конфигурации и бритве (которую я добавлю после) находить свои файлы как обычно.

// 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);

Обратите внимание: чтобы действительно создать один файл без PDB, вам также понадобятся:

<DebugType>None</DebugType>

Мое приложение находится на.NET Core 3.1, публикуется в виде одного файла и работает как служба Windows (что может или не может повлиять на проблему).

Предлагаемое решение с Process.GetCurrentProcess().MainModule.FileName поскольку корень содержимого работает для меня, но только если я установил корень содержимого в нужном месте:

Это работает:

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

Это не работает:

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

Это область ответа (ов).

Во-первых, проголосуйте за ответ "RS", на который я ссылаюсь в этом ответе. Это было волшебство.

Короткий ответ - "используйте ответ RS И установите это значение во всех нужных местах". Я показываю 2 места для установки значений ниже.

Мое конкретное ДОПОЛНЕНИЕ (нигде не упомянутое):

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

Более длинный ответ:

Мне нужны были ответы выше, И у меня есть некоторые дополнения.

В моем выводе (я покажу код позже) вот разница между двумя приведенными выше ответами.

    GetBasePath='/mybuilddir/myOut'

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

где '/mybuilddir/myOut' было местом, где я опубликовал свой единственный файл.. в моем файле определения докеров.

GetBasePath НЕ работал при использовании PublishSingleFile

"realPath" был тем способом, которым я наконец заставил его работать. Ака, ответ выше.: Как мне получить однофайловое приложение.NET Core 3 для поиска файла appsettings.json?

и когда вы видите значение "realPath"... тогда все это имеет смысл. singleFile извлекается ~ где-то... и RS выяснил, где находится это место извлечения.

Я покажу свой Program.cs целиком, что даст всему контекст.

Обратите внимание, мне пришлось установить realPath в ДВУХ местах.

Я отметил важные вещи

/* IMPORTANT

Полный код ниже, который (снова) заимствован из ответа RS: Как мне получить однофайловое приложение.NET Core 3 для поиска файла appsettings.json?

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://docs.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;
        }
    }
}

и мое содержимое csproj toplayer:

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

и мой файл докера для кайфов:

# 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
Другие вопросы по тегам