如何获取.NET Core 3单文件应用程序以查找appsettings.json文件?

时间:2019-10-09 15:31:32

标签: c# .net-core asp.net-core-webapi asp.net-core-3.0

应如何配置单文件.Net Core 3.0 Web API应用程序,以查找与构建单文件应用程序相同的目录中的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()
...

我尝试了similar, but distinct question的解决方案以及其他Stack Overflow问题。

我试图将以下内容传递给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 个答案:

答案 0 :(得分:6)

我在GitHub here上发现了一个标题为PublishSingleFile excluding appsettings not working as expected的问题。

这指向另一个名为single file publish: AppContext.BaseDirectory doesn't point to apphost directory的问题here

其中,一种解决方案是尝试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);
}

答案 1 :(得分:5)

如果可以在运行时在可执行文件之外使用文件,则可以在csproj中标记所需的文件。这种方法允许在已知位置进行实时更改等。

hstore

如果这是不可接受的,并且必须仅包含一个文件,则在主机设置中将单文件提取的路径作为根路径传递。这样一来,配置和剃刀(我将在其后添加)就可以正常查找其文件。

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

注意,要真正制作一个文件而没有PDB,您还需要:

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

答案 2 :(得分:2)

我的应用程序在.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>();
    });

答案 3 :(得分:-1)

这是“背负式”答案区域。

首先,通过我在此答案中引用的“ RS”对上述答案进行投票。那是魔术。

最简单的答案是“使用RS的答案并在所有正确的位置设置该值”。我在下面显示了两个用于设置值的地方。

我的特定添加项(在其他任何地方都没有提及)是:

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

更长的答案是:

我需要以上答案,并且还有一些补充。

在我的输出中(稍后我将显示代码),这是上面两个答案之间的区别。

    GetBasePath='/mybuilddir/myOut'

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

'/ mybuilddir / myOut'是我在docker定义文件中发布单个文件的位置。

使用PublishSingleFile时,GetBasePath不起作用

“ realPath”是我最终使它工作的方式。又名,以上答案。 :How can I get my .NET Core 3 single file app to find the appsettings.json file?

,当您看到“ realPath”的值时...那么一切都有意义。 singleFile正在某个位置被提取出来...。RS找出了提取位置所在的地方。

我将展示我的整个Program.cs,它将为所有内容提供上下文。

请注意,我必须在两个地方设置“ realPath”。

我用

标记了重要的事情

/* IMPORTANT

下面的完整代码(再次)是从RS的答案中借用的: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://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;
        }
    }
}

和我的toplayer csproj内容:

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

和我的docker文件踢:

# 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