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

Как следует настроить однофайловое веб-приложение .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

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


person Jason Yandell    schedule 09.10.2019    source источник


Ответы (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);
}
person Jason Yandell    schedule 09.10.2019
comment
Этот ответ плюс @ ronald-swaine ниже идеален. Параметры приложения исключаются из связанного исполняемого файла, а задача публикации помещает файлы параметров приложения вместе с прилагаемым исполняемым файлом. - person Aaron Hudon; 04.02.2020
comment
У меня была такая же ошибка. Моя проблема была фактически на этапе развертывания - я изменил appSettings.json на appsettings.json, но коммит Github не заботился об этом изменении случая (необходимо изменить локальные настройки, просто что-то проверить) - person user685590; 25.08.2020
comment
Я борюсь с этим уже несколько дней. Спасибо за ваше решение. Я действительно считаю, что это немного странный обходной путь для чего-то, что должно просто работать. И все ведет себя по-разному в .NET 5 и .NET Core 3.1. Это: Path.Combine (AppContext.BaseDirectory, Assembly.GetExecutingAssembly (). ManifestModule.Name) работает в предварительном просмотре VS2019 при отладке, но после публикации в один файл (без обрезки, без готовности к запуску) он возвращает System. IO.FileNotFoundException: система не может найти указанный файл. (0x80070002). Кажется, мне чего-то не хватает ... - person John Baughman; 03.09.2020
comment
Это нужно исправить. Мы не должны писать код для определения также среды и чтения / агрегирования каждого json-файла настроек приложения. Нехорошо. Это не лучший обходной путь. Необходимо найти и обновить или создать новую проблему GitHub на MS. - person hB0; 29.09.2020
comment
постоянно изменяющееся поведение системного вызова .net core, меняет между собой подверсии сетевого ядра github.com/dotnet/runtime/issues/3704#issuecomment-607103702 означает, что эта среда выполнения не может использоваться при настройке общей среды, в отличие от полной версии .net fw (которая не нарушает основные apis / services fw) - person hB0; 01.10.2020
comment
МС: Эта проблема исправлена ​​в .net5 - Значит, .NET 3.1 не работает? ССЫЛКА: github.com/dotnet/runtime/issues/3704#issuecomment-651210020 < / а> - person hB0; 06.10.2020

Если вас устраивает использование файлов во время выполнения вне исполняемого файла, то вы можете просто пометить файлы, которые хотите извне, в 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>
person Ronald Swaine    schedule 28.10.2019
comment
Как Views \ Test.cshtml в вашем примере попадает на целевой компьютер? У меня есть файлы изображений и словарей, которые мне нужно открыть на основе культуры, - person Paul Cohen; 09.11.2019
comment
@PaulCohen Я обычно развертывал все файлы из опубликованного места вывода с помощью SCP. При наличии только изменений в csproj (первый пример) для любых API-интерфейсов, использующих корневой каталог содержимого по умолчанию, файлы должны быть доступны в рабочем каталоге. Похоже, вам нужно использовать второй пример, чтобы получить реальный путь извлеченного контента, чтобы вы могли получить доступ к изображениям с полного пути или правильно настроив корень контента, чтобы пути ~ / ... к изображениям можно использовать в бритве. - person Ronald Swaine; 13.11.2019
comment
Мой опыт работы со встроенными приложениями, поэтому один двоичный файл обычно записывается в ROM. Я понимаю, что есть Exe, который я использую в каком-то самораспаковывающемся «zip-подобном» файле. Все мои файлы данных находятся там, и когда приложение запускается, все сразу извлекается во временную папку. Если я найду это, я смогу найти свои файлы данных. Это также означает, что мне нужна некоторая логика, чтобы я мог отлаживать в VS, где данные находятся где-то еще. - person Paul Cohen; 14.11.2019
comment
В настоящее время у меня есть эта настройка, и она случайно перестала находить файлы. - person Going-gone; 28.01.2020
comment
var realPath Вы качаете пригород. Я добавил ответ, чтобы разъяснить, где установить значение ....... но ничего себе ... это было спасением. Спасибо. - person granadaCoder; 24.07.2020
comment
Первое решение - хранить файл appsettings снаружи - это то, что я использую, и оно работает не так, как ожидалось. Когда у вас есть один файл, он извлекается во временное место в профиле пользователя в Windows, и там же извлекается файл appsettings, а также в исходное местоположение; но путь выполнения во время выполнения устанавливается в местоположение временного каталога; и поэтому убивает цель. SingleFile - это просто косметическое изменение; скорее я бы избегал этого в серверной среде. - person hB0; 01.10.2020

Мое приложение находится на .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>();
    });

person Wolfgang Gallo    schedule 05.05.2020

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

Во-первых, проголосуйте за приведенный выше ответ от 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
person granadaCoder    schedule 23.07.2020