多角色 ASP.NET Core 主机如何规范 appsettings.{Environment}.json 发布过程
有些 ASP.NET Core 项目只有一套源码,却会发布成几个用途不同的主机。例如,一个主机提供完整的管理功能,另一个只保留公开 API;还可能有只运行定时任务的 Worker。它们连接的服务、监听端口和日志级别都不完全相同。
这类项目很容易积累出一批只有维护者看得懂的配置文件:
appsettings.json
appsettings.test.json
appsettings.host.json
appsettings.worker.json
appsettings.server-2.json文件名能提醒人,却不代表 ASP.NET Core 会加载它。ASP.NET Core Configuration 文档说明,WebApplication.CreateBuilder(args) 默认读取 appsettings.json 和当前环境对应的 appsettings.{Environment}.json。自定义后缀必须手工调用 AddJsonFile,否则它只是一个被复制到发布目录的普通 JSON 文件。
解决这个问题不需要再发明一套运行时配置系统。先把“环境”和“角色”分开,再决定角色配置何时进入发布物。
环境与角色是两个维度
ASP.NET Core 运行环境文档中的 Environment 描述应用运行在哪里:
Development:开发机;Staging:测试或预发布环境;Production:正式环境。
Admin、Api、Worker 不是 Environment。它们描述同一套程序以什么职责运行。
如果把两者混成 ProductionAdmin、ProductionApi 之类的自定义环境名,框架当然也能工作,但环境判断会逐渐变得别扭:IsProduction() 不再成立,第三方组件也未必认识这些名字。每增加一个部署环境,还要复制一轮角色组合。
更稳妥的分工是:
Environment 决定运行环境
Role 决定发布哪一种主机角色在发布时已经确定,就没有必要让运行中的进程再猜一次。一个 Admin 发布目录和一个 Api 发布目录,可以各自拥有同名的 appsettings.Production.json,内容不同并不冲突。
源码中的配置怎样命名
一种容易维护的命名方式是:
appsettings.Development.json
appsettings.Staging.json
appsettings.Production.Admin.json
appsettings.Production.Api.json开发和测试各自只有一个完整主机时,直接使用框架认识的环境文件。Production 有多个角色,所以源码中保留角色后缀,避免两个文件争用 appsettings.Production.json。
这里要接受一个看似不够整齐的事实:appsettings.Production.Admin.json 是发布源文件,不是运行时文件。它的名字是给维护者和发布脚本看的。Admin 主机发布后,它会变成标准的:
appsettings.Production.jsonApi 主机也一样,只是位于另一个发布目录。
是否还要保留 appsettings.json
如果几个环境确实有一小块稳定的公共配置,可以放在 appsettings.json,再让环境文件覆盖差异项。ASP.NET Core 会先加载基础文件,再加载当前环境文件。
旧项目的配置往往很大,注释和历史选项也很多。此时不要为了追求目录漂亮,立刻把完整配置拆成十几个片段。先规范文件名和加载路径,确认每个发布物读取正确;去重可以以后单独处理。没有可靠公共基线时,只保留完整的环境配置也可以。
Release 不等于 Production
按照 dotnet publish 命令文档,下面这条命令选择的是编译配置:
dotnet publish -c ReleaseRelease 控制优化、调试信息和条件编译,不会设置 IHostEnvironment.EnvironmentName。运行环境仍由 DOTNET_ENVIRONMENT、ASPNETCORE_ENVIRONMENT 或主机启动参数决定。两者恰好都常用 Development、Production 这些词,才容易被误认为一回事。
如果没有设置运行环境,ASP.NET Core 默认使用 Production。因此,只包含 appsettings.Production.json 的正式发布物可以直接启动。Docker Compose 中仍建议显式写出环境,打开编排文件就能看出它会加载哪套配置:
services:
web-host:
image: example/web-host:latest
environment:
ASPNETCORE_ENVIRONMENT: Production不要同时给 DOTNET_ENVIRONMENT 和 ASPNETCORE_ENVIRONMENT 设置不同值。不同 Hosting API 和 .NET 版本对两者的优先级有差异,同时存在只会增加排障成本。
launchSettings.json 只管本地启动
本地开发可以在 Properties/launchSettings.json 中指定:
{
"profiles": {
"WebHost": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}这个文件供 IDE、dotnet run 和 dotnet watch 使用,不会随 dotnet publish 部署到服务器。生产环境不能依赖它,测试服务器也需要在自己的启动方式中明确设置 Staging。
发布时把角色配置变成标准环境文件
假设脚本位于仓库根目录,后端项目位于 src/Server。下面的脚本一次发布一个角色:
param(
[Parameter(Mandatory)]
[ValidateSet("Admin", "Api")]
[string]$Role
)
$ErrorActionPreference = "Stop"
$rootDirectory = $PSScriptRoot
$publishDirectory = Join-Path $rootDirectory "dist/$($Role.ToLowerInvariant())"
Push-Location $rootDirectory
try
{
Remove-Item $publishDirectory -Recurse -Force -ErrorAction Ignore
Set-Location "src/Server"
dotnet publish -c Release -o $publishDirectory -t:Rebuild
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed: $LASTEXITCODE" }
$roleConfig = "appsettings.Production.$Role.json"
if (-not (Test-Path $roleConfig -PathType Leaf))
{
throw "Role configuration was not found: $roleConfig"
}
# dotnet publish 会复制项目中的 appsettings*.json。
# 发布物只保留当前角色真正会读取的配置。
Get-ChildItem -LiteralPath $publishDirectory -Filter "appsettings*.json" |
Remove-Item -Force
Copy-Item $roleConfig "$publishDirectory/appsettings.Production.json"
}
finally
{
Pop-Location
}这里用 throw 而不是 exit:失败应该终止发布脚本,但不该顺手关闭调用者的 PowerShell 会话。角色配置不存在时也必须在删除发布物中的配置文件之前报错。关于目录恢复和原生命令退出码,可参考用工作目录简化 PowerShell 发布脚本。
分别执行:
.\build-host.ps1 -Role Admin
.\build-host.ps1 -Role Api得到两个独立发布目录:
dist/
├── admin/
│ ├── WebHost.dll
│ └── appsettings.Production.json
└── api/
├── WebHost.dll
└── appsettings.Production.json删除发布目录中的全部 appsettings*.json 再复制,不是多余步骤。Web SDK 通常会把项目中的配置文件带到发布目录;如果把 Development、Staging 和所有 Production 角色配置一起放进镜像,进程虽然只读取其中一部分,部署包却泄露了其他环境的设置,也让人工排查变得含糊。
如果项目保留了公共的 appsettings.json,清理时应留下它,只删除带环境和角色后缀的文件:
Get-ChildItem $publishDirectory -Filter "appsettings.*.json" |
Remove-Item -Force随后再复制选中的 appsettings.Production.json。
为什么发布时要 Rebuild
如果不同角色只有配置不同,一次正常的 dotnet publish 就够了。若角色还通过条件编译裁剪 Controller、后台任务或中间件,同一个工作区连续发布多个角色时应执行 -t:Rebuild,避免第二次发布复用前一个角色留下的中间产物。
条件编译符号描述的是代码差异,不负责选择配置文件。发布脚本应同时完成这两件事,但不要把它们混成一个隐式规则:
编译参数 -> 决定程序集包含哪些代码
配置复制 -> 决定该发布目录使用哪些设置少量易变值用环境变量覆盖
角色配置确定后,JWT 签名密钥、数据库密码等值仍可能需要独立轮换。没必要因此复制一份新的 JSON。默认配置提供程序会在 JSON 之后读取环境变量,因此环境变量可以覆盖文件中的同名键。
层级键在环境变量中使用双下划线:
services:
web-host:
environment:
ASPNETCORE_ENVIRONMENT: Production
Authentication__JwtBearer__SecurityKey: ${JWT_SECURITY_KEY}对应的 JSON 路径是:
Authentication:JwtBearer:SecurityKey环境变量适合覆盖少数会轮换或由部署平台管理的值,不适合把一份几百行的结构化配置全部摊平。还要注意,环境变量本身不是加密存储;它解决的是覆盖顺序和部署注入,不是密钥保管问题。
Windows 与 Docker 使用同一套发布规则
配置规范不应依赖容器。Docker、Windows 服务和直接运行可执行文件的区别,只在于它们如何设置 Environment:
- Docker Compose 在
environment中指定Production; - Windows 服务可以在服务包装器或宿主配置中设置,也可以依赖默认的
Production; - 本地开发由
launchSettings.json指定Development; - 测试服务器的启动入口明确指定
Staging。
发布物内部始终遵守标准文件名。这样把应用从 Windows 移进 Linux 容器时,不需要修改 Program.cs 来寻找一份历史命名的 JSON。
发布后的检查
先检查目录,不必启动应用:
Get-ChildItem "dist/admin" -Filter "appsettings*.json"
Get-Content "dist/admin/appsettings.Production.json" |
ConvertFrom-Json |
Out-Null正式角色的发布目录应只有预期的基础配置和一个 appsettings.Production.json,JSON 也必须能被解析。
启动后再确认日志中的 Hosting environment。需要进一步核对时,可以在启动阶段记录角色名称、配置版本或不敏感的服务地址,但不要打印连接字符串、令牌和完整配置对象。
最后检查这些容易漏掉的地方:
- 容器或服务声明的 Environment 与发布文件名一致;
- Admin 和 Api 发布目录中的配置内容没有复制反;
- 环境变量覆盖使用双下划线,并且没有被空值意外替换;
- 发布目录没有留下其他环境或角色的配置文件。
这套规则的判断标准很直接:开发者看到源码文件名,知道它属于哪个环境和角色;运维打开任意发布目录,只会看到该进程真正使用的标准环境配置。
如果同一个发布物还要装入 Angular 前端,可以继续使用Angular 与 ASP.NET Core 发布物合并为单一 Docker 镜像中的制品组装方式。配置选择应在前端文件复制之前完成,任何一步失败都不能继续构建镜像。