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