📟.NET 8 專案 MSB3030「無法複製 appsettings.Development.json」:publish 巢狀遞迴的完整排錯與修正指南

 

重點摘要

  • 你的 dotnet run/build 報錯 MSB3030:找不到要複製的 appsettings.Development.json

  • 真正原因是:專案把 publish/(甚至 out/)目錄也當成專案來源來「再發佈一次」,形成 publish\publish-...\publish-...無限巢狀與「來源檔不存在」。

  • 兩步就好:

    1. 清掉殘留:刪除 bin/obj/publish/

    2. 改 .csproj:加 DefaultItemExcludes 排除 publish/out,並「白名單」只處理根目錄的 appsettings*.json,同時把 bin/obj/publish/out 從各 Items 移除。


問題現象(錯誤訊息)

你的主控台長這樣(節錄重點):

error MSB3030: 無法複製檔案 C:\...\publish\publish-...\publish-...\appsettings.Development.json, 因為找不到檔案。

注意到那串超長路徑嗎?publish\publish-...\publish-... 一層套一層,就是自我遞迴的典型症狀。


為什麼會發生?

ASP.NET Core 的 Microsoft.NET.Sdk.Web 會「自動收錄」很多檔案(Default Items)。
如果你或某個外部共用設定(例如 Directory.Build.targets/props)有這類萬用字元:

  • **\*.json**\*.*

  • 或自訂 <Copy>/AfterPublishpublish 當「來源」 再複製一次

就可能把 上一次發佈產生的 publish/ 當作這次建置的來源,造成:

  1. publish 裡面又包含 publish(無限巢狀)

  2. 尋找「來源檔」時,路徑越疊越長、實際又不存在 → MSB3030


最快解法(保險且通用)

1) 先清乾淨

這次改用 CMD 指令 與跨平台清理,與上一版不同寫法


:: Windows CMD rmdir /s /q .\bin rmdir /s /q .\obj rmdir /s /q .\publish :: 或搭配官方清理 dotnet clean

2) 修改 Net.csproj(重點設定)

新版寫法:同樣達到「排除 publish/out、白名單 appsettings」目標,但採用 Update + Exclude 與更完整的排除清單


<Project
Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <!-- ⛔ 直接把 publish/out 加入預設排除,避免被 Default Items 收進來 --> <DefaultItemExcludes> $(DefaultItemExcludes); **\publish\**;**\out\**;**\dist\**;**\.next\** </DefaultItemExcludes> </PropertyGroup> <ItemGroup> <PackageReference Include="EPPlus" Version="7.6.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.13" /> <PackageReference Include="Oracle.EntityFrameworkCore" Version="9.23.60" /> <PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.7.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> </ItemGroup> <!-- ✅ 白名單:只針對專案根目錄的 appsettings*.json --> <ItemGroup> <None Update="appsettings*.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </None> </ItemGroup> <!-- 🧹 雙保險:移除可能被外部 targets 或萬用字元加入的項目 --> <ItemGroup> <Content Remove="bin\**\*.*;obj\**\*.*;publish\**\*.*;out\**\*.*;**\node_modules\**" /> <None Remove="bin\**\*.*;obj\**\*.*;publish\**\*.*;out\**\*.*;**\node_modules\**" /> <Compile Remove="publish\**\*.*;out\**\*.*" /> <EmbeddedResource Remove="publish\**\*.*;out\**\*.*" /> <!-- 針對可能參與 Copy 的 Items 做額外清空 --> <ResolvedFileToPublish Remove="publish\**\*.*;out\**\*.*" /> <NoneWithTargetPath Remove="publish\**\*.*;out\**\*.*" /> <ContentWithTargetPath Remove="publish\**\*.*;out\**\*.*" /> </ItemGroup> <!-- 🔒(可選)Release 發佈時排除 Development 設定檔 --> <ItemGroup Condition="'$(Configuration)'=='Release'"> <None Update="appsettings.Development.json"> <CopyToPublishDirectory>Never</CopyToPublishDirectory> </None> </ItemGroup> </Project>

3) 再建置 / 執行

這次示範加上 --no-restore(若前面已 restore)與 --urls,避免重複步驟


dotnet build dotnet run --no-restore --urls
"http://0.0.0.0:8080"

進階:想抓出真正「元兇」?(建議一次做)

改用 dotnet msbuild 的參數形式,輸出不同檔名,便於歸檔


dotnet msbuild -bl:build-diag.binlog -v:diag

MSBuild Structured Log Viewer 打開 build-diag.binlog,搜尋:

  • Copy 任務

  • 超長的 publish-...appsettings.Development.json 路徑
    往上展開,看是哪個 Item/Target 把 publish/ 收進來。


參考:如果真的想「時間戳備份」發佈結果

全新示範:改以 robocopy 較快的備份方式,且路徑在專案外,杜絕遞迴


<Target
Name="ArchivePublishOutput" AfterTargets="Publish"> <PropertyGroup> <UtcStamp>$([System.DateTime]::UtcNow.ToString(yyyyMMdd_HHmmss))</UtcStamp> <ArchiveRoot>$(SolutionDir)artifacts\$(MSBuildProjectName)\publish-$(UtcStamp)\</ArchiveRoot> </PropertyGroup> <MakeDir Directories="$(ArchiveRoot)" /> <!-- Windows 最快:robocopy,/E 遞迴、關閉多數輸出,避免干擾 CI 日誌 --> <Exec Command="robocopy &quot;$(PublishDir)&quot; &quot;$(ArchiveRoot)&quot; /E /NFL /NDL /NJH /NJS /NP" /> </Target>
非 Windows 可改:
<Exec Command="dotnet tool run copyfiles &quot;$(PublishDir)**/*&quot; &quot;$(ArchiveRoot)&quot;" />
或使用 PowerShell:
<Exec Command="powershell -NoProfile -Command &quot;Copy-Item -Recurse -Force '$(PublishDir)*' '$(ArchiveRoot)'&quot;" />


FAQ:常見疑問

Q1:為什麼 dotnet publish 先成功,後面 dotnet run 反而失敗?
A:第一次發佈把產物丟到 publish/。但下一次建置把 publish/ 當成來源又收進來,就會形成巢狀遞迴與找不到檔案 → MSB3030。

Q2:我可以用萬用字元 **/*.json 嗎?
可以,但一定要排除 bin/obj/publish/out/node_modules,例如以下 全新範例(與前述不同寫法):

<ItemGroup> <Content Update="**\*.json" Exclude="bin\**;obj\**;publish\**;out\**;**\node_modules\**;**\dist\**" /> </ItemGroup>

Q3:appsettings.Development.json 需要發佈到正式環境嗎?
通常不需要。這裡也示範了另一種條件式寫法(不同於前例):

<ItemGroup Condition="'$(Configuration)'=='Release'"> <None Update="appsettings.Development.json"> <CopyToPublishDirectory>Never</CopyToPublishDirectory> </None> </ItemGroup>

Q4:只 Clean 可以解決嗎?
請同時刪除 publish/,不然殘留的巢狀內容下次仍可能被收進來。


順手把 Nullability 警告也降噪一下(全新示範)

1) Model 的必填字串給預設值或 required

public sealed class PlanRow { public required string ApsPlanNo { get; init; } = string.Empty; // .NET 7+ 支援 required public string? Customer { get; set; } public int? Week { get; set; } }

2) ToDictionary 的 Key 不能是 string?(替代策略:為 null 指定替代鍵)

var map = rows .GroupBy(r => r.Customer ?? "__NULL__") // 為 null 指定替代鍵 .ToDictionary(g => g.Key, g => g.Count());

3) Controller 參數加保護(改用 BindRequired 或手動驗證)

using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; [ApiController] [Route("api/[controller]")] public class JobsController : ControllerBase { [HttpGet] public IActionResult GetJobs([FromQuery, BindRequired] string apsVersion) { if (string.IsNullOrWhiteSpace(apsVersion)) return BadRequest("Query 'apsVersion' is required."); // ... 查詢與回傳 return Ok(new { version = apsVersion }); } }

4) Minimal API 版本(另一種完全不同的寫法)

using System.ComponentModel.DataAnnotations; var app = WebApplication.CreateBuilder(args).Build(); app.MapGet("/api/jobs", ([Required] string apsVersion) => { if (string.IsNullOrWhiteSpace(apsVersion)) return Results.BadRequest("apsVersion is required."); return Results.Ok(new { version = apsVersion }); }); app.Run();

最後的檢查清單(不少團隊就靠這張)

  • .csproj 已加 <DefaultItemExcludes>…;**\publish\**;**\out\**</DefaultItemExcludes>

  • 已把 bin/obj/publish/outContent/None/Compile/EmbeddedResource 等 Items 移除

  • 僅白名單處理 appsettings*.json(根目錄)

  • 刪除過 bin/obj/publish/ 後重新建置

  • 沒有任何 <Copy>/自訂 Target 把 publish/ 當成「來源」

  • 若要時間戳備份,目的地在專案外的 artifacts/,不是 publish/

  • ToDictionary 的 key 不是 string?;Model 必填字串已初始化

留言

這個網誌中的熱門文章

🔍Vue.js 專案錯誤排查:解決 numericFields is not defined 與合併儲存格邏輯最佳化

🛠【ASP.NET Core + Oracle】解決 ORA-00904 "FALSE": 無效的 ID 錯誤與資料欄位動態插入顯示問題

🔎EF Core 連 Oracle 出現 ORA-00600 [kpp_concatq:2] 的完整排錯指南(含 EF Core ToString/CultureInfo 錯誤)