<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>John Wu&#39;s Blog</title>
  
  <subtitle>簡單不想寫，太難不會寫。</subtitle>
  <link href="https://blog.johnwu.cc/atom.xml" rel="self"/>
  
  <link href="https://blog.johnwu.cc/"/>
  <updated>2026-04-17T09:06:57.517Z</updated>
  <id>https://blog.johnwu.cc/</id>
  
  <author>
    <name>John Wu</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>AI Coding Agent 實戰工作坊 — 中階場 (Claude)</title>
    <link href="https://blog.johnwu.cc/article/ai-coding-agent-workshop-02-intermediate-claude.html"/>
    <id>https://blog.johnwu.cc/article/ai-coding-agent-workshop-02-intermediate-claude.html</id>
    <published>2026-04-17T10:00:00.000Z</published>
    <updated>2026-04-17T09:06:57.517Z</updated>
    
    <content type="html"><![CDATA[<p>本篇分享 AI Coding Agent 實戰工作坊中階場的內容，以 Claude Code 作為主要示範工具，帶大家從「會用 AI」進一步到「設計 AI 的工作環境」。中階場的核心不是背更多術語，而是<strong>學會判斷結果不穩時，該補哪一層</strong>。</p><span id="more"></span><iframe width="560" height="315" src="https://www.youtube.com/embed/b5kWQNVJ4HM" title="AI Coding Agent 實戰工作坊 — 中階場 (Claude)" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe><h2 id="簡報下載"><a href="#簡報下載" class="headerlink" title="簡報下載"></a>簡報下載</h2><ul><li><a href="/files/20260417-AI-Workshop-02-intermediate.pdf">AI Coding Agent 實戰工作坊 — 中階場 (Claude) (PDF)</a></li></ul><h2 id="從單次協作到可重複流程"><a href="#從單次協作到可重複流程" class="headerlink" title="從單次協作到可重複流程"></a>從單次協作到可重複流程</h2><p>初階場學的是「完成一次可驗收的人機協作」，中階場要往前一步：</p><table><thead><tr><th align="left">角度</th><th align="left">你在做什麼</th></tr></thead><tbody><tr><td align="left"><strong>單次協作</strong></td><td align="left">把需求講清楚，讓 AI 幫你完成一次任務</td></tr><tr><td align="left"><strong>可重複流程</strong></td><td align="left">設計規則、流程、工具，讓 AI 能穩定完成很多次任務</td></tr></tbody></table><p>你不只是想做出一道菜，而是開始設計整間廚房怎麼穩定運作。同樣一道菜，有人每次都靠運氣，有人靠制度穩定出餐。</p><h2 id="AI-工作環境的三大支柱"><a href="#AI-工作環境的三大支柱" class="headerlink" title="AI 工作環境的三大支柱"></a>AI 工作環境的三大支柱</h2><p>中階場的核心框架是 Prompt &#x2F; Context &#x2F; Harness，三者缺一不可：</p><table><thead><tr><th align="left">支柱</th><th align="left">餐廳比喻</th><th align="left">白話</th></tr></thead><tbody><tr><td align="left"><strong>Prompt</strong></td><td align="left">你怎麼點餐</td><td align="left">你給 AI 的指令、限制、輸出要求</td></tr><tr><td align="left"><strong>Context</strong></td><td align="left">菜單、廚房規矩、食材資訊</td><td align="left">AI 取得到的專案知識與外部資訊</td></tr><tr><td align="left"><strong>Harness</strong></td><td align="left">廚房制度、權限、安全檢查</td><td align="left">AI 可以怎麼做、做到哪裡、何時該停下來</td></tr></tbody></table><h3 id="為什麼同一句需求，結果還是會飄？"><a href="#為什麼同一句需求，結果還是會飄？" class="headerlink" title="為什麼同一句需求，結果還是會飄？"></a>為什麼同一句需求，結果還是會飄？</h3><p>通常不是單一原因：</p><ol><li><strong>Prompt 不夠清楚</strong>：任務、限制、輸出格式沒有講明</li><li><strong>Context 不夠完整</strong>：AI 不知道 repo 規範、現有 code、外部資料</li><li><strong>Harness 不夠穩</strong>：沒有檢查點、權限邊界或 workflow 約束</li></ol><p><strong>所以要學的不是「寫超長 prompt」，而是判斷這次要補的是哪一層。</strong></p><h2 id="Prompt-—-你怎麼點餐"><a href="#Prompt-—-你怎麼點餐" class="headerlink" title="Prompt — 你怎麼點餐"></a>Prompt — 你怎麼點餐</h2><p>不是每件事都要用同樣鬆緊度：</p><table><thead><tr><th align="left">任務類型</th><th align="left">建議鬆緊度</th></tr></thead><tbody><tr><td align="left">資料庫遷移</td><td align="left"><strong>嚴格</strong>（指定做法、限制風險）</td></tr><tr><td align="left">API 實作</td><td align="left"><strong>中等</strong>（講清規格，保留部分空間）</td></tr><tr><td align="left">Code Review</td><td align="left"><strong>中等到寬鬆</strong>（給目標與觀察角度）</td></tr><tr><td align="left">寫文件</td><td align="left"><strong>寬鬆</strong>（講清受眾與格式）</td></tr></tbody></table><blockquote><p>越危險的操作，prompt 越要明確；越探索的任務，可以留多一點空間。</p></blockquote><h2 id="Context-—-廚房知道什麼"><a href="#Context-—-廚房知道什麼" class="headerlink" title="Context — 廚房知道什麼"></a>Context — 廚房知道什麼</h2><p>如果 Prompt 是點餐，那 Context 就是：這是一家什麼餐廳、廚房有什麼規矩、現在有哪些食材、有沒有外部系統可以查資料。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">常見 context = 對話內容 + 相關檔案 + 專案指引檔 + 需要時取得的外部資訊</span><br></pre></td></tr></table></figure><p>⚠ context 不只影響品質，也影響成本與效率——塞太多不相關的東西，判斷會變差、資源也會浪費。</p><h3 id="專案指引檔是廚房規矩"><a href="#專案指引檔是廚房規矩" class="headerlink" title="專案指引檔是廚房規矩"></a>專案指引檔是廚房規矩</h3><table><thead><tr><th align="left">概念</th><th align="left">Claude Code</th><th align="left">Codex</th><th align="left">Gemini</th></tr></thead><tbody><tr><td align="left">專案指引檔</td><td align="left"><code>CLAUDE.md</code></td><td align="left"><code>AGENTS.md</code></td><td align="left"><code>GEMINI.md</code></td></tr><tr><td align="left">作用</td><td align="left">告訴 AI 專案身份、規範、工作方式</td><td align="left">同類型概念</td><td align="left">同類型概念</td></tr></tbody></table><p>你是在跟 AI 說：「這是我們家的廚房，我們平常就是這樣做事」。寫一次，之後每次對話都比較容易站在同一套規則上。</p><h3 id="專案指引檔怎麼寫才有用？"><a href="#專案指引檔怎麼寫才有用？" class="headerlink" title="專案指引檔怎麼寫才有用？"></a>專案指引檔怎麼寫才有用？</h3><table><thead><tr><th align="left">要寫</th><th align="left">不要寫</th></tr></thead><tbody><tr><td align="left">規則（約束怎麼工作、什麼不能做）</td><td align="left">介紹（項目是什麼、目錄結構）</td></tr><tr><td align="left">團隊&#x2F;倉庫獨有的限制</td><td align="left">模型本來就會的常識</td></tr><tr><td align="left">長期有效的流程</td><td align="left">臨時任務（應寫成 Skill 或 Hook）</td></tr><tr><td align="left">反面例子「不要這樣做」</td><td align="left">正面套話「保持優雅」</td></tr></tbody></table><blockquote><p>每一行都佔全局上下文。不寫就會出錯的才寫進去，同樣錯誤出現兩次再更新。</p></blockquote><p>⚠️ 有研究指出：AI 自己生的指引檔，任務成功率反而<strong>下降</strong>；人寫但塞太多，也幾乎沒幫助。<strong>只寫最小必要規則，效果最好。</strong></p><h3 id="Prompt-其實是組裝出來的"><a href="#Prompt-其實是組裝出來的" class="headerlink" title="Prompt 其實是組裝出來的"></a>Prompt 其實是組裝出來的</h3><p>你寫的不只是一份 CLAUDE.md，而是可組合的模組：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CLAUDE.md → role → policy → stack → skills → templates</span><br><span class="line">━━━━━━━━━━━━━━━━━━━━━━━━━   ━━━━━━━━━━━━━━━━━━━━━━━━━━</span><br><span class="line">     每次都載入（核心）                 按需選用</span><br></pre></td></tr></table></figure><ul><li><strong>核心模組</strong>：身份、規則 — 每次對話都帶著</li><li><strong>Skills</strong>：怎麼做 — 任務有固定 SOP 時載入</li><li><strong>Templates</strong>：結構化的資訊格式 — 任務背景、報告、提案都是模板</li></ul><h2 id="Harness-—-廚房怎麼被管住"><a href="#Harness-—-廚房怎麼被管住" class="headerlink" title="Harness — 廚房怎麼被管住"></a>Harness — 廚房怎麼被管住</h2><table><thead><tr><th align="left">機制</th><th align="left">餐廳比喻</th><th align="left">說明</th></tr></thead><tbody><tr><td align="left"><strong>權限 &#x2F; approval</strong></td><td align="left">哪些事要店長批准</td><td align="left">高風險操作先確認</td></tr><tr><td align="left"><strong>Hooks &#x2F; 自動檢查</strong></td><td align="left">出餐前先過品管</td><td align="left">在特定時機自動跑檢查或流程</td></tr><tr><td align="left"><strong>Workflow 約束</strong></td><td align="left">廚房標準流程</td><td align="left">先看 code、再改、最後驗證</td></tr></tbody></table><p>例如在 <code>.claude/settings.json</code> 設一個 hook，每次 AI 改完檔案自動跑 lint：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;hooks&quot;: &#123;</span><br><span class="line">    &quot;PostToolUse&quot;: [&#123;</span><br><span class="line">      &quot;matcher&quot;: &quot;Edit&quot;,</span><br><span class="line">      &quot;hooks&quot;: [&#123; &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;pnpm lint --fix&quot; &#125;]</span><br><span class="line">    &#125;]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>Harness 像安全帶 — 不是綁手綁腳，而是讓你敢放手讓 AI 跑。</p></blockquote><h3 id="Harness-的兩個面向"><a href="#Harness-的兩個面向" class="headerlink" title="Harness 的兩個面向"></a>Harness 的兩個面向</h3><table><thead><tr><th align="left">面向</th><th align="left">做什麼</th></tr></thead><tbody><tr><td align="left"><strong>自動護欄</strong></td><td align="left">hooks、權限、檢查點，防止出錯</td></tr><tr><td align="left"><strong>反覆調校</strong></td><td align="left">根據結果回頭修正 Prompts &amp; Skills</td></tr></tbody></table><p>只有護欄沒有調校，規則會僵化；只有調校沒有護欄，每次都靠人盯。兩者搭配才完整。</p><h2 id="Skills-MCP-Plugins，分別在做什麼？"><a href="#Skills-MCP-Plugins，分別在做什麼？" class="headerlink" title="Skills &#x2F; MCP &#x2F; Plugins，分別在做什麼？"></a>Skills &#x2F; MCP &#x2F; Plugins，分別在做什麼？</h2><table><thead><tr><th align="left">概念</th><th align="left">白話</th></tr></thead><tbody><tr><td align="left"><strong>Prompt</strong></td><td align="left">你怎麼下指令</td></tr><tr><td align="left"><strong>CLAUDE.md &#x2F; AGENTS.md</strong></td><td align="left">專案背景與做事方式</td></tr><tr><td align="left"><strong>Skills</strong></td><td align="left">讓 AI 按步驟完成一種任務</td></tr><tr><td align="left"><strong>MCP</strong></td><td align="left">讓 AI 連到外部工具或資料</td></tr><tr><td align="left"><strong>Plugins</strong></td><td align="left">把常用能力打包成可重用組合</td></tr></tbody></table><h3 id="Skills-—-把做事方法變成-SOP"><a href="#Skills-—-把做事方法變成-SOP" class="headerlink" title="Skills — 把做事方法變成 SOP"></a>Skills — 把做事方法變成 SOP</h3><p>Skill 把常見任務的流程寫好：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">模糊需求 → brainstorming → plan → implementation → verification</span><br></pre></td></tr></table></figure><blockquote><p>Skill 不只是 prompt 模板，而是可重複套用的工作流程。</p></blockquote><h3 id="MCP-—-讓廚房自己去拿資訊"><a href="#MCP-—-讓廚房自己去拿資訊" class="headerlink" title="MCP — 讓廚房自己去拿資訊"></a>MCP — 讓廚房自己去拿資訊</h3><p>MCP 讓 AI 在需要時自己去查資料或做操作，例如讀 issue、查資料庫、看團隊討論脈絡、讀檔案。關鍵不是「接了多少系統」，而是：<strong>AI 能不能在對的時候，自己拿到它需要的資訊</strong>。</p><h2 id="Live-Demo：從-0-到-1-做一個-RWD-任務管理系統"><a href="#Live-Demo：從-0-到-1-做一個-RWD-任務管理系統" class="headerlink" title="Live Demo：從 0 到 1 做一個 RWD 任務管理系統"></a>Live Demo：從 0 到 1 做一個 RWD 任務管理系統</h2><p>Demo 的主軸是讓 AI 按流程做出產品雛形，對應三大支柱：</p><ul><li><strong>Prompt &#x2F; Context</strong>：先定角色與規則</li><li><strong>Context → Spec</strong>：用 brainstorming 把需求收斂</li><li><strong>Harness</strong>：用 plan、回饋迴圈推進與修正</li></ul><h3 id="1-先建立-Role-Skill-系統"><a href="#1-先建立-Role-Skill-系統" class="headerlink" title="1. 先建立 Role &#x2F; Skill 系統"></a>1. 先建立 Role &#x2F; Skill 系統</h3><p>在專案中建立三個 Role Prompts：</p><ul><li><strong>Frontend</strong>：專注 UI 結構、RWD、互動與元件拆分</li><li><strong>Backend</strong>：專注 API、資料結構、驗證與錯誤處理</li><li><strong>QA</strong>：專注測試案例、邊界條件與風險提醒</li></ul><p>每個角色再配一個 Skill：</p><ul><li><strong>Frontend skill</strong>：先定頁面結構，再定 breakpoint，再拆元件</li><li><strong>Backend skill</strong>：先定 schema，再定 API，再補 validation</li><li><strong>QA skill</strong>：先寫 test plan，再補 edge cases 與 E2E checklist</li></ul><h3 id="2-建立約束條件"><a href="#2-建立約束條件" class="headerlink" title="2. 建立約束條件"></a>2. 建立約束條件</h3><p>用 <code>/init</code> 產生 CLAUDE.md，再把規則寫進去：</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/init 這個專案每次執行任務時，只帶一個角色與對應 Skill，Skill 最多 3 個</span><br></pre></td></tr></table></figure><table><thead><tr><th align="left">情境</th><th align="left">沒約束</th><th align="left">有約束</th></tr></thead><tbody><tr><td align="left">角色混用</td><td align="left">前後端混扮，風格不一致</td><td align="left">一次一個角色，職責清楚</td></tr><tr><td align="left">Skill 爆量</td><td align="left">太多 SOP 互相衝突</td><td align="left">最多 3 個，聚焦當前任務</td></tr><tr><td align="left">規則遺失</td><td align="left">每次對話都要重新交代</td><td align="left">寫進 CLAUDE.md，自動生效</td></tr></tbody></table><h3 id="3-Brainstorm-把需求收斂成-Design-Spec"><a href="#3-Brainstorm-把需求收斂成-Design-Spec" class="headerlink" title="3. Brainstorm 把需求收斂成 Design Spec"></a>3. Brainstorm 把需求收斂成 Design Spec</h3><p>安裝 <a href="https://github.com/obra/superpowers">Superpowers</a> 後：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/superpowers:brainstorm 我想做一個 RWD 的任務管理系統</span><br></pre></td></tr></table></figure><p>一句模糊需求會被逐步澄清成可落地規格，決定架構方案與技術棧。<strong>關鍵不是直接寫 code，而是把需求先收斂成可以分工與驗收的 spec。</strong></p><h3 id="4-從-Plan-到逐步實作"><a href="#4-從-Plan-到逐步實作" class="headerlink" title="4. 從 Plan 到逐步實作"></a>4. 從 Plan 到逐步實作</h3><p>先把 spec 變成可執行計畫：</p><ul><li>把 spec 拆出 N 個 tasks</li><li>依序讓 AI 實作每個 task</li><li>最後用 build &#x2F; test 當驗收標準</li></ul><p>執行順序：後端 schema + API → 前端元件 + 頁面 → QA 補測試案例與驗收。</p><h3 id="5-根據回饋即時修正"><a href="#5-根據回饋即時修正" class="headerlink" title="5. 根據回饋即時修正"></a>5. 根據回饋即時修正</h3><p>請 AI 修改完自動啟動 backend 與 frontend，用瀏覽器實際操作。發現問題時，判斷該補哪一層：</p><table><thead><tr><th align="left">問題</th><th align="left">該補什麼</th><th align="left">修正方式</th></tr></thead><tbody><tr><td align="left">手機版任務卡片排版跑掉</td><td align="left"><strong>Context</strong>（規範）</td><td align="left">調整 flex &#x2F; media query</td></tr><tr><td align="left">新增任務後 API 回 404</td><td align="left"><strong>Prompt</strong>（路由細節）</td><td align="left">檢查路由與 controller</td></tr><tr><td align="left">篩選「已完成」仍顯示全部</td><td align="left"><strong>Harness</strong>（測試檢查點）</td><td align="left">確認 query 與 filter</td></tr></tbody></table><blockquote><p>成品不會一次到位，關鍵是判斷該補哪一層，再回饋給 AI。</p></blockquote><h3 id="6-用-OpenSpec-提出需求變更"><a href="#6-用-OpenSpec-提出需求變更" class="headerlink" title="6. 用 OpenSpec 提出需求變更"></a>6. 用 OpenSpec 提出需求變更</h3><p><a href="https://github.com/Fission-AI/OpenSpec">OpenSpec</a> 是需求、設計、實作、驗收的變更帳本：</p><ol><li><code>openspec init</code> 啟用</li><li><code>/opsx:explore</code>：提出需求</li><li><code>/opsx:propose</code>：建立需求文件及分工規劃</li><li><code>/opsx:apply</code>：依照 spec 實作</li><li><code>/opsx:archive</code>：驗收完成後歸檔</li></ol><blockquote><p>OpenSpec 不是文件倉庫，而是讓每次變更都有脈絡可追。</p></blockquote><h2 id="實務提醒：成本與-context-意識"><a href="#實務提醒：成本與-context-意識" class="headerlink" title="實務提醒：成本與 context 意識"></a>實務提醒：成本與 context 意識</h2><p>前面提過，context 不只影響品質，也影響成本。這些情況通常都比較耗資源：</p><table><thead><tr><th align="left">行為</th><th align="left">常見影響</th><th align="left">建議</th></tr></thead><tbody><tr><td align="left">超長對話</td><td align="left">context 越來越重</td><td align="left">適時開新對話，保持主題乾淨</td></tr><tr><td align="left">一次塞太多不相關資訊</td><td align="left">判斷品質下降</td><td align="left">只給真正相關的內容</td></tr><tr><td align="left">反覆試錯但不收斂</td><td align="left">花時間也花額度</td><td align="left">退一步先補 context 或規則</td></tr><tr><td align="left">不必要的多工具串接</td><td align="left">複雜度上升</td><td align="left">先解決核心問題，再加工具</td></tr></tbody></table><blockquote><p>省資源的方法通常不是少問，而是讓每次提問更有結構。</p></blockquote><h2 id="三個-Takeaway"><a href="#三個-Takeaway" class="headerlink" title="三個 Takeaway"></a>三個 Takeaway</h2><ol><li><strong>Prompt + Context + Harness 三者缺一不可</strong> — 好的結果，通常不是靠單一神 prompt，而是三層一起補</li><li><strong>CLAUDE.md &#x2F; AGENTS.md 是團隊共享的廚房規矩</strong> — 寫一次，之後每次合作都不用重新對齊規矩</li><li><strong>Skills + MCP 讓 AI 從聊天工具變成工作環境的一部分</strong> — 不只會回答，還能按流程做事、在需要時自己拿資訊</li></ol><blockquote><p>中階的核心不是「更會用 AI」，而是「更會設計 AI 怎麼工作」。</p></blockquote><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://claude.com/claude-code">Claude Code</a> - Anthropic</li><li><a href="https://github.com/obra/superpowers">Superpowers</a> - 可重用技能包</li><li><a href="https://github.com/Fission-AI/OpenSpec">OpenSpec</a> - Spec 驅動開發</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本篇分享 AI Coding Agent 實戰工作坊中階場的內容，以 Claude Code 作為主要示範工具，帶大家從「會用 AI」進一步到「設計 AI 的工作環境」。中階場的核心不是背更多術語，而是&lt;strong&gt;學會判斷結果不穩時，該補哪一層&lt;/strong&gt;。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="https://blog.johnwu.cc/categories/ai/"/>
    
    
    <category term="Vibe Coding" scheme="https://blog.johnwu.cc/tags/vibe-coding/"/>
    
    <category term="Prompt Engineering" scheme="https://blog.johnwu.cc/tags/prompt-engineering/"/>
    
    <category term="Context Engineering" scheme="https://blog.johnwu.cc/tags/context-engineering/"/>
    
    <category term="Superpowers" scheme="https://blog.johnwu.cc/tags/superpowers/"/>
    
    <category term="OpenSpec" scheme="https://blog.johnwu.cc/tags/openspec/"/>
    
    <category term="AI 工作坊" scheme="https://blog.johnwu.cc/tags/ai-%E5%B7%A5%E4%BD%9C%E5%9D%8A/"/>
    
    <category term="AI Agent" scheme="https://blog.johnwu.cc/tags/ai-agent/"/>
    
    <category term="Claude Code" scheme="https://blog.johnwu.cc/tags/claude-code/"/>
    
    <category term="Harness Engineering" scheme="https://blog.johnwu.cc/tags/harness-engineering/"/>
    
    <category term="Skills" scheme="https://blog.johnwu.cc/tags/skills/"/>
    
  </entry>
  
  <entry>
    <title>AI Coding Agent 實戰工作坊 — 初階場 (Codex)</title>
    <link href="https://blog.johnwu.cc/article/ai-coding-agent-workshop-01-beginner-codex.html"/>
    <id>https://blog.johnwu.cc/article/ai-coding-agent-workshop-01-beginner-codex.html</id>
    <published>2026-04-15T10:00:00.000Z</published>
    <updated>2026-04-17T09:06:54.903Z</updated>
    
    <content type="html"><![CDATA[<p>本篇分享 AI Coding Agent 實戰工作坊初階場的內容，以 Codex 作為主要示範工具，帶大家從零開始體驗 AI 輔助開發的完整流程。初階場的核心目標不是背名詞，而是<strong>完成一次可驗收的人機協作</strong>。</p><span id="more"></span><iframe width="560" height="315" src="https://www.youtube.com/embed/pwbbTtk3K1Y" title="AI Coding Agent 實戰工作坊 — 初階場 (Codex)" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe><h2 id="簡報下載"><a href="#簡報下載" class="headerlink" title="簡報下載"></a>簡報下載</h2><ul><li><a href="/files/20260415-AI-Workshop-01-beginner.pdf">AI Coding Agent 實戰工作坊 — 初階場 (Codex) (PDF)</a></li></ul><h2 id="三個心智模型轉換"><a href="#三個心智模型轉換" class="headerlink" title="三個心智模型轉換"></a>三個心智模型轉換</h2><p>在開始使用 AI 工具之前，先調整三個思維方式：</p><table><thead><tr><th align="left">以前</th><th align="left">現在</th></tr></thead><tbody><tr><td align="left"><strong>搜尋</strong> — Google、Stack Overflow 找答案</td><td align="left"><strong>對話</strong> — 直接問 AI，它給你完整答案</td></tr><tr><td align="left"><strong>複製貼上</strong> — 找到 code 改改看能不能用</td><td align="left"><strong>描述意圖</strong> — 說你要什麼，AI 量身打造</td></tr><tr><td align="left"><strong>獨自開發</strong> — 自己想、自己寫</td><td align="left"><strong>人機協作</strong> — 你做決策，AI 做執行</td></tr></tbody></table><p>這就是所謂的 <strong>Vibe Coding</strong>：不再逐行手寫，而是用自然語言「說出你要什麼」，AI 生成第一版，你負責驗收和迭代。</p><h2 id="AI-開發的六個層次"><a href="#AI-開發的六個層次" class="headerlink" title="AI 開發的六個層次"></a>AI 開發的六個層次</h2><p>工作坊用「開餐廳」的比喻來串聯所有觀念：</p><table><thead><tr><th align="left">你說的話</th><th align="left">餐廳比喻</th><th align="left">對應概念</th></tr></thead><tbody><tr><td align="left">「我想吃辣的」</td><td align="left">先做一版試吃再調整</td><td align="left"><strong>Vibe Coding</strong></td></tr><tr><td align="left">「宮保雞丁，不要花生，少油」</td><td align="left">講越具體，出餐越準</td><td align="left"><strong>Prompts</strong></td></tr><tr><td align="left">「這是川菜館，大火快炒」</td><td align="left">廚房規矩寫好</td><td align="left"><strong>Project Instructions</strong></td></tr><tr><td align="left">食譜 + 檢查點 + 工具清單</td><td align="left">標準作業流程</td><td align="left"><strong>Skills</strong></td></tr><tr><td align="left">接上供應商、倉庫、外送系統</td><td align="left">對外接口</td><td align="left"><strong>MCP</strong></td></tr></tbody></table><h3 id="Prompts-—-你當下怎麼開口"><a href="#Prompts-—-你當下怎麼開口" class="headerlink" title="Prompts — 你當下怎麼開口"></a>Prompts — 你當下怎麼開口</h3><p>你對 AI 說的每一句話都是 Prompt。描述越具體，AI 的結果越接近你要的。</p><p>同一個需求，不同說法差很多：</p><table><thead><tr><th align="left"></th><th align="left">Prompt</th><th align="left">預期結果</th></tr></thead><tbody><tr><td align="left">❌</td><td align="left">「改好一點」</td><td align="left">AI 不知道要改什麼</td></tr><tr><td align="left">⚠️</td><td align="left">「UI 醜醜的，幫我改」</td><td align="left">AI 隨便改，可能不是你要的</td></tr><tr><td align="left">✅</td><td align="left">「把背景改成草地主題，地鼠換成卡通風格，按鈕加圓角和陰影，被打到時要有特效，游標變錘子」</td><td align="left">AI 精準執行</td></tr></tbody></table><h3 id="Project-Instructions-—-讓-AI-先了解你的專案"><a href="#Project-Instructions-—-讓-AI-先了解你的專案" class="headerlink" title="Project Instructions — 讓 AI 先了解你的專案"></a>Project Instructions — 讓 AI 先了解你的專案</h3><p>在專案根目錄放一個專案指引檔（Codex 用 <code>AGENTS.md</code>），AI 每次開工都會自動讀取，不用每次重講。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># 打地鼠遊戲</span></span><br><span class="line"></span><br><span class="line"><span class="section">## 技術棧</span></span><br><span class="line">HTML + CSS + vanilla JavaScript，不用任何框架</span><br><span class="line"></span><br><span class="line"><span class="section">## 規則</span></span><br><span class="line"><span class="bullet">-</span> 所有文字使用繁體中文</span><br><span class="line"><span class="bullet">-</span> CSS 使用卡通風格，圓角 + 明亮配色</span><br><span class="line"><span class="bullet">-</span> 每次修改後要能直接開 index.html 測試</span><br></pre></td></tr></table></figure><blockquote><p>寫一次，全團隊受益。</p></blockquote><h3 id="Skills-—-讓-AI-按照流程做事"><a href="#Skills-—-讓-AI-按照流程做事" class="headerlink" title="Skills — 讓 AI 按照流程做事"></a>Skills — 讓 AI 按照流程做事</h3><p>把常見工作流程寫成可重用的能力，讓 AI 在需要時套用。例如建立一個 <code>code-review</code> Skill，每次做收尾檢查時自動執行：</p><ol><li>檢查有沒有 <code>console.log</code> 殘留</li><li>確認所有文字都是繁體中文</li><li>測試遊戲能不能正常開啟</li><li>列出這次改了哪些檔案</li></ol><p>不用每次重複交代，寫一次就能重複使用。進一步還可以安裝 <a href="https://github.com/obra/superpowers">Superpowers</a> 這類整套技能包，直接取得 brainstorming、TDD、code review 等常見 workflow。</p><h3 id="MCP-—-讓-AI-連接外部工具與資料"><a href="#MCP-—-讓-AI-連接外部工具與資料" class="headerlink" title="MCP — 讓 AI 連接外部工具與資料"></a>MCP — 讓 AI 連接外部工具與資料</h3><p>讓 AI 不只看你貼的內容，還能連到外部系統查資料、讀文件、做操作。例如：</p><ul><li>連 GitLab → 讀 Issue、MR</li><li>連 JIRA → 讀 Ticket</li><li>連 Telegram → 收發訊息</li></ul><p>AI 從「只會聊天」進一步變成「能查、能讀、能動手做事」。</p><h2 id="補充觀念"><a href="#補充觀念" class="headerlink" title="補充觀念"></a>補充觀念</h2><h3 id="Context-—-AI-看到的全貌"><a href="#Context-—-AI-看到的全貌" class="headerlink" title="Context — AI 看到的全貌"></a>Context — AI 看到的全貌</h3><p>每次你跟 AI 對話，工具會整理相關的 Context 給它：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">你的 prompt + 對話歷史 + 相關檔案內容</span><br><span class="line">+ 專案指引檔（如 AGENTS.md）</span><br><span class="line">+ 需要時載入的 Skills</span><br><span class="line">+ 呼叫 MCP 後取得的結果</span><br></pre></td></tr></table></figure><p>Context 越大通常越慢、越貴；對話越長，前面的重點越可能被稀釋。所以：<strong>講重點、必要時開新對話、不要把什麼都塞進去</strong>。</p><h3 id="Models-—-AI-的法術強度"><a href="#Models-—-AI-的法術強度" class="headerlink" title="Models — AI 的法術強度"></a>Models — AI 的法術強度</h3><p>不同模型像遊戲裡不同等級的法術：</p><table><thead><tr><th align="left">法術等級</th><th align="left">感覺</th><th align="left">適合場景</th></tr></thead><tbody><tr><td align="left"><strong>小招級</strong></td><td align="left">最快、最省</td><td align="left">整理文案、格式修正、簡單問答</td></tr><tr><td align="left"><strong>主力技</strong></td><td align="left">速度和品質平衡</td><td align="left">日常 feature 開發、大部分工作</td></tr><tr><td align="left"><strong>大招級</strong></td><td align="left">處理難題比較穩</td><td align="left">卡很久的 bug、架構設計、複雜重構</td></tr></tbody></table><p>原則很簡單：<strong>簡單事別亂開大招，殺雞不用牛刀</strong>。同一個模型也有不同的推理程度（蓄力時間），蓄力越久答案通常越完整，但也更慢更貴，不是每次都要拉滿。</p><h3 id="Token-—-AI-的魔力值"><a href="#Token-—-AI-的魔力值" class="headerlink" title="Token — AI 的魔力值"></a>Token — AI 的魔力值</h3><p>Token 是 AI 處理資訊的計量單位。你講得越多、AI 回得越多、對話越長，消耗就越大。強模型、長對話、重工具使用通常都更耗資源。所以：<strong>講重點，少廢話，卡住就開新對話</strong>。</p><h2 id="實作心法"><a href="#實作心法" class="headerlink" title="實作心法"></a>實作心法</h2><h3 id="選題三判準"><a href="#選題三判準" class="headerlink" title="選題三判準"></a>選題三判準</h3><ul><li><strong>小</strong> — 單一功能、單一 bug、單一整理任務</li><li><strong>清楚</strong> — 你能用一句話說清楚要它做什麼</li><li><strong>可驗收</strong> — 能用測試、畫面、或 checklist 判斷結果</li></ul><h3 id="開口模板"><a href="#開口模板" class="headerlink" title="開口模板"></a>開口模板</h3><p><strong>工程師：</strong>「這是我的小任務：____。請你先做第一版，如果需要修改程式碼就直接改，最後告訴我怎麼驗收。」</p><p><strong>QA：</strong>「這是我要測的功能：____。請幫我整理成 test checklist 或 bug report，格式要清楚、可直接拿去用。」</p><p><strong>BA：</strong>「這是我的需求描述：____。請幫我整理成 user story、acceptance criteria，並指出哪裡還不夠清楚。」</p><h3 id="卡住時怎麼接著問"><a href="#卡住時怎麼接著問" class="headerlink" title="卡住時怎麼接著問"></a>卡住時怎麼接著問</h3><ul><li>「測試失敗，錯誤如下，請修到通過：____」</li><li>「結果跟我預期不同，差異是：____」</li><li>「先不要一次做完，請拆成 3 個步驟」</li><li>「先告訴我你準備怎麼改，再開始動手」</li></ul><h2 id="安全提醒"><a href="#安全提醒" class="headerlink" title="安全提醒"></a>安全提醒</h2><p>AI 不是保險箱，以下內容<strong>不要貼給 AI</strong>：</p><table><thead><tr><th align="left">類型</th><th align="left">範例</th><th align="left">風險</th></tr></thead><tbody><tr><td align="left">機密程式碼</td><td align="left">核心演算法、未公開功能</td><td align="left">敏感資訊送進第三方服務</td></tr><tr><td align="left">客戶資料</td><td align="left">個資、交易紀錄</td><td align="left">違反隱私法規</td></tr><tr><td align="left">認證資訊</td><td align="left">API key、密碼、token</td><td align="left">外洩即可被利用</td></tr><tr><td align="left">內部文件</td><td align="left">未公開的商業策略</td><td align="left">競爭資訊外洩</td></tr></tbody></table><blockquote><p>是否可使用，依公司政策、工具設定與資料分級規範為準。不確定能不能貼？先用公開或模擬資料。</p></blockquote><h2 id="三個-Takeaway"><a href="#三個-Takeaway" class="headerlink" title="三個 Takeaway"></a>三個 Takeaway</h2><ol><li><strong>AI 是你的 Pair Programmer</strong> — AI 是協作者，不是代做者；你要負責驗收</li><li><strong>AI 不只靠 Prompts</strong> — 不是越長越好，而是越清楚越好；先講目標、限制、驗收</li><li><strong>AI 會犯錯</strong> — 卡住時不要自己硬修；把錯誤和預期差異丟回 AI</li></ol><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://chatgpt.com/codex">Codex</a> - OpenAI</li><li><a href="https://github.com/obra/superpowers">Superpowers</a> - 可重用技能包</li><li><a href="https://github.com/Fission-AI/OpenSpec">OpenSpec</a> - Spec 驅動開發</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本篇分享 AI Coding Agent 實戰工作坊初階場的內容，以 Codex 作為主要示範工具，帶大家從零開始體驗 AI 輔助開發的完整流程。初階場的核心目標不是背名詞，而是&lt;strong&gt;完成一次可驗收的人機協作&lt;/strong&gt;。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="https://blog.johnwu.cc/categories/ai/"/>
    
    
    <category term="Vibe Coding" scheme="https://blog.johnwu.cc/tags/vibe-coding/"/>
    
    <category term="Codex" scheme="https://blog.johnwu.cc/tags/codex/"/>
    
    <category term="Prompt Engineering" scheme="https://blog.johnwu.cc/tags/prompt-engineering/"/>
    
    <category term="Context Engineering" scheme="https://blog.johnwu.cc/tags/context-engineering/"/>
    
    <category term="Superpowers" scheme="https://blog.johnwu.cc/tags/superpowers/"/>
    
    <category term="OpenSpec" scheme="https://blog.johnwu.cc/tags/openspec/"/>
    
    <category term="AI 工作坊" scheme="https://blog.johnwu.cc/tags/ai-%E5%B7%A5%E4%BD%9C%E5%9D%8A/"/>
    
    <category term="AI Agent" scheme="https://blog.johnwu.cc/tags/ai-agent/"/>
    
  </entry>
  
  <entry>
    <title>數學練習小程式</title>
    <link href="https://blog.johnwu.cc/article/math-exercise.html"/>
    <id>https://blog.johnwu.cc/article/math-exercise.html</id>
    <published>2020-12-25T08:17:00.000Z</published>
    <updated>2026-02-03T04:09:36.654Z</updated>
    
    <content type="html"><![CDATA[<p><img src="/images/b/57.png" alt="數學練習小程式 執行結果"></p><p>小學二年級的女兒學到乘法時，隨手寫的練習程式。由於工作關係，三年多沒碰前端，順便練習寫一下 ReactJS；這是我第一次寫 ReactJS，寫得不好的地方請多指教。</p><p>這是免費的線上九九乘法<a href="https://math-exercise.johnwu.cc/">數學練習小程式</a>，程式碼也公開在 GitHub 上給需要的人自取。  </p><span id="more"></span><h2 id="更新內容"><a href="#更新內容" class="headerlink" title="更新內容"></a>更新內容</h2><ul><li>2021&#x2F;01<ul><li>舒爾特方格表<ul><li>數字, 注音, 英文大小寫, 日文平假名片假名</li></ul></li><li>填充題作答方式</li><li>自訂答題限時</li></ul></li><li>2020&#x2F;12 <ul><li>首發 9 x 9 乘法練習</li><li>新增 19 x 19 乘法練習</li><li>新增加法練習</li><li>新增減法練習</li><li>新增時鐘練習</li></ul></li></ul><h3 id="更新計畫"><a href="#更新計畫" class="headerlink" title="更新計畫"></a>更新計畫</h3><p>之後會隨著女兒及兒子的課業內容不定期更新，如果有改善建議歡迎在下面留言處跟我說。<br>或是有能力開發的人，也歡迎發 MR 給我，底下有 GitHub 連結。 </p><h2 id="操作截圖"><a href="#操作截圖" class="headerlink" title="操作截圖"></a>操作截圖</h2><p>由左到右為：  </p><ul><li>主選單畫面</li><li>9 x 9 乘法選單畫面</li><li>9 x 9 乘法選擇題作答畫面</li></ul><p><img src="/images/b/57.png" alt="數學練習小程式 - 主選單畫面">  </p><p>由左到右為：  </p><ul><li>9 x 9 乘法作答逾時畫面</li><li>9 x 9 乘法填充題作答畫面</li><li>9 x 9 乘法作答完成畫面</li></ul><p><img src="/images/b/58.png" alt="數學練習小程式 - 作答畫面及作答完成畫面">  </p><p>由左到右為：  </p><ul><li>5 x 5 舒爾特方格表畫面</li><li>時鐘練習作答畫面</li><li>時鐘練習作答完成畫面</li></ul><p><img src="/images/b/59.png" alt="數學練習小程式 - 時間作答畫面">  </p><h2 id="程式碼下載"><a href="#程式碼下載" class="headerlink" title="程式碼下載"></a>程式碼下載</h2><p><a href="https://github.com/johnwu1114/math-exercise">GitHub - math-exercise</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;/images/b/57.png&quot; alt=&quot;數學練習小程式 執行結果&quot;&gt;&lt;/p&gt;
&lt;p&gt;小學二年級的女兒學到乘法時，隨手寫的練習程式。
由於工作關係，三年多沒碰前端，順便練習寫一下 ReactJS；這是我第一次寫 ReactJS，寫得不好的地方請多指教。&lt;/p&gt;
&lt;p&gt;這是免費的線上九九乘法&lt;a href=&quot;https://math-exercise.johnwu.cc/&quot;&gt;數學練習小程式&lt;/a&gt;，程式碼也公開在 GitHub 上給需要的人自取。  &lt;/p&gt;</summary>
    
    
    
    <category term="隨手作品" scheme="https://blog.johnwu.cc/categories/%E9%9A%A8%E6%89%8B%E4%BD%9C%E5%93%81/"/>
    
    
    <category term="ReactJS" scheme="https://blog.johnwu.cc/tags/reactjs/"/>
    
  </entry>
  
  <entry>
    <title>.NET Core - 在 Mac 開發階段發生 ObjectDisposedException</title>
    <link href="https://blog.johnwu.cc/article/dotnet-core-mac-objectdisposedexception.html"/>
    <id>https://blog.johnwu.cc/article/dotnet-core-mac-objectdisposedexception.html</id>
    <published>2020-01-16T08:06:00.000Z</published>
    <updated>2026-02-03T05:13:46.039Z</updated>
    
    <content type="html"><![CDATA[<p>在 Mac 使用 Rider 開發時，突然遇到執行測試失敗，顯示的錯誤訊息如下：  </p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">System.ObjectDisposedException: Cannot access a disposed object.</span><br><span class="line">Object name: &#x27;IServiceProvider&#x27;.</span><br></pre></td></tr></table></figure><p>前一刻才剛跑過所有的測試，突然間就死一片，如圖：  </p><p><img src="/images/b/54.png" alt=".NET Core - System.ObjectDisposedException: Cannot access a disposed object"></p><span id="more"></span><p>主要原因是本機整合測試時，為了要使用 <code>IServiceProvider</code>，所以建立出 <code>IHostBuilder</code> 實體。<br>但很不幸的是這個 <code>Host</code> 死在背景，而且站著資源放不掉，所以重跑測試時一直無法 Build 出新的 Host，導致 <code>IServiceProvider</code> 不能被使用。</p><p>可透過以下指令找出在背景的 dotnet process，在強制刪除，如下：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 找到死在背景的 dotnet process</span></span><br><span class="line">ps x | grep <span class="string">&quot;dotnet exec&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 強制停止該 dotnet process</span></span><br><span class="line"><span class="built_in">kill</span> -9 &lt;pid&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 整合以上兩行</span></span><br><span class="line"><span class="built_in">kill</span> -9 $(ps -ax | grep <span class="string">&quot;dotnet exec&quot;</span> | awk <span class="string">&#x27;&#123; print $1 &#125;&#x27;</span>) 2&gt; /dev/null</span><br></pre></td></tr></table></figure><p><img src="/images/b/55.png" alt=".NET Core - 開發階段無法取用 Disposed Object - kill dotnet process"></p><p>刪除後又回復正常了。  </p><p><img src="/images/b/56.png" alt=".NET Core - 開發階段無法取用 Disposed Object Success"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在 Mac 使用 Rider 開發時，突然遇到執行測試失敗，顯示的錯誤訊息如下：  &lt;/p&gt;
&lt;figure class=&quot;highlight txt&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;System.ObjectDisposedException: Cannot access a disposed object.&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Object name: &amp;#x27;IServiceProvider&amp;#x27;.&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;前一刻才剛跑過所有的測試，突然間就死一片，如圖：  &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/b/54.png&quot; alt=&quot;.NET Core - System.ObjectDisposedException: Cannot access a disposed object&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term=".NET Core" scheme="https://blog.johnwu.cc/categories/net-core/"/>
    
    
    <category term=".NET Core" scheme="https://blog.johnwu.cc/tags/net-core/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - 注入多個相同的介面 (Interface)</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-di-same-interface.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-di-same-interface.html</id>
    <published>2019-10-29T15:52:00.000Z</published>
    <updated>2026-02-03T05:13:46.014Z</updated>
    
    <content type="html"><![CDATA[<p>通常在使用 ASP.NET Core 依賴注入 (Dependency Injection, DI) 都是一個介面對應一個實作類別。<br>若有多個類別時做相同的介面時，注入的方式就會有點變化。<br>本篇將介紹 ASP.NET Core 依賴注入多個相同的介面 (Interface)  </p><span id="more"></span><h2 id="前置準備"><a href="#前置準備" class="headerlink" title="前置準備"></a>前置準備</h2><p>以下介面作為範例：</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="built_in">enum</span> WalletType</span><br><span class="line">&#123;</span><br><span class="line">    Alipay,</span><br><span class="line">    CreditCard,</span><br><span class="line">    LinePay</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">IWalletService</span></span><br><span class="line">&#123;</span><br><span class="line">    WalletType WalletType &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">Debit</span>(<span class="params"><span class="built_in">decimal</span> amount</span>)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">AlipayService</span> : <span class="title">IWalletService</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> WalletType WalletType &#123; <span class="keyword">get</span>; &#125; = WalletType.Alipay;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Debit</span>(<span class="params"><span class="built_in">decimal</span> amount</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 從支付寶扣錢</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CreditCardService</span> : <span class="title">IWalletService</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> WalletType WalletType &#123; <span class="keyword">get</span>; &#125; = WalletType.CreditCard;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Debit</span>(<span class="params"><span class="built_in">decimal</span> amount</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 從信用卡扣錢</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">LinePayService</span> : <span class="title">IWalletService</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> WalletType WalletType &#123; <span class="keyword">get</span>; &#125; = WalletType.LinePay;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Debit</span>(<span class="params"><span class="built_in">decimal</span> amount</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 從 Line Pay 扣錢</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="服務註冊"><a href="#服務註冊" class="headerlink" title="服務註冊"></a>服務註冊</h2><p>用相同的介面，註冊不同的實作類別：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        services.AddMvc();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 註冊 Services</span></span><br><span class="line">        services.AddSingleton&lt;IWalletService, AlipayService&gt;();</span><br><span class="line">        services.AddSingleton&lt;IWalletService, CreditCardService&gt;();</span><br><span class="line">        services.AddSingleton&lt;IWalletService, LinePayService&gt;();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Service-Injection"><a href="#Service-Injection" class="headerlink" title="Service Injection"></a>Service Injection</h2><p>在 Constructor Injection 時用 <code>IEnumerable&lt;T&gt;</code> 注入，便可取得相同介面的全部實例，再依照使用情境選擇要用的實例。範例如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System.Collections.Generic;</span><br><span class="line"><span class="keyword">using</span> System.Linq;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Mvc;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite.Controllers</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">ShoppingCartController</span> : <span class="title">Controller</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">readonly</span> IEnumerable&lt;IWalletService&gt; _walletServices;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">ShoppingCartController</span>(<span class="params">IEnumerable&lt;IWalletService&gt; walletServices</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            _walletServices = walletServices;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> IActionResult <span class="title">Checkout</span>(<span class="params">WalletType walletType, <span class="built_in">decimal</span> amount</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">var</span> walletService = _walletServices.Single(x =&gt; x.WalletType == walletType);</span><br><span class="line">            walletService.Debit(amount);</span><br><span class="line">            <span class="keyword">return</span> Ok();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>示意圖如下：  </p><p><img src="/images/b/52.png" alt="ASP.NET Core 3 系列 - 注入多個相同的介面 (Interface) - 多介面注入">  </p><blockquote><p><code>注意！</code><br>通常只建議 <strong>Singleton</strong> 類型的服務這樣使用，因為使用 <strong>Transient</strong> 或 <strong>Scoped</strong> 類型的服務，注入時會 <code>new</code> 新的實例，若沒用到的話，就變成不必要的效能耗損。  </p></blockquote><p>如果服務註冊用相同的介面，註冊不同的實作類別，Constructor Injection 時不是用 <code>IEnumerable&lt;T&gt;</code> 注入，就只會得到最後一個註冊的類別，如上例會得到 <code>LinePayService</code> 的實例。  </p><p>示意圖如下：  </p><p><img src="/images/b/53.png" alt="ASP.NET Core 3 系列 - 注入多個相同的介面 (Interface) - 單一介面注入">  </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;通常在使用 ASP.NET Core 依賴注入 (Dependency Injection, DI) 都是一個介面對應一個實作類別。&lt;br&gt;若有多個類別時做相同的介面時，注入的方式就會有點變化。&lt;br&gt;本篇將介紹 ASP.NET Core 依賴注入多個相同的介面 (Interface)  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - 自行建置 Service Provider</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-build-service-provider.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-build-service-provider.html</id>
    <published>2019-10-29T14:44:00.000Z</published>
    <updated>2026-02-03T05:13:46.037Z</updated>
    
    <content type="html"><![CDATA[<p>一般情況 IHostBuilder 建置 Host 實例的時候，就會自動建置 Service Provider。<br>所以大部份情境不需要自行建置 Service Provider，但如果想在 IHostBuilder 建置出 Host 實例之前取得 Service Provider，就可以用 <strong>IServiceCollection</strong> 的擴充方法 <code>BuildServiceProvider</code> 建置出 Service Provider。<br>本篇將介紹 ASP.NET Core 如何透過 <code>BuildServiceProvider</code> 建置 Service Provider，以及要注意的地方。  </p><span id="more"></span><h2 id="建置-Service-Provider"><a href="#建置-Service-Provider" class="headerlink" title="建置 Service Provider"></a>建置 Service Provider</h2><p>通常會在 <code>Startup.ConfigureServices</code> 方法中，註冊 Services，待 Host 成立實體後，即可透過 Constructor Injection，提供 Services。<br>Host 建立實體前，若要使用 Service Provider，就需要透過 <strong>IServiceCollection</strong> 的擴充方法 <code>BuildServiceProvider</code> 建置，範例如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.DependencyInjection;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Logging;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            services.AddMvc();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 註冊 Services</span></span><br><span class="line">            services.AddSingleton&lt;ISampleSingleton, Sample&gt;();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 建置 Service Provider</span></span><br><span class="line">            <span class="keyword">var</span> serviceProvider = services.BuildServiceProvider();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 從 Service Provider 取得服務使用</span></span><br><span class="line">            <span class="keyword">var</span> logger = serviceProvider.GetService&lt;ILogger&lt;Startup&gt;&gt;();</span><br><span class="line">            <span class="keyword">var</span> sample = serviceProvider.GetService&lt;ISampleSingleton&gt;();</span><br><span class="line">            logger.LogInformation(<span class="string">$&quot;Type: <span class="subst">&#123;sample.GetType()&#125;</span>\r\n&quot;</span></span><br><span class="line">                                  + <span class="string">$&quot;HashCode: <span class="subst">&#123;sample.GetHashCode()&#125;</span>\r\n&quot;</span></span><br><span class="line">                                  + <span class="string">$&quot;Id: <span class="subst">&#123;sample.Id&#125;</span>&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            app.UseRouting();</span><br><span class="line">            app.UseEndpoints(endpoints =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                endpoints.MapControllerRoute(</span><br><span class="line">                    <span class="string">&quot;default&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;&#123;controller=Home&#125;/&#123;action=Index&#125;/&#123;id?&#125;&quot;</span></span><br><span class="line">                );</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="注意事項"><a href="#注意事項" class="headerlink" title="注意事項"></a>注意事項</h2><p>Host 實例使用的 Service Provider 與自行建立的 Service Provider 是完全不同的實體，而 <code>services.AddSingleton</code> 所註冊的 Services 僅在 Service Provider 實體中 <strong>Singleton</strong>，不同的 Service Provider 實體會各自存在自己的 Services。  </p><p>以上例來說，雖然 <strong>Sample</strong> 是使用 <code>AddSingleton</code> 註冊，但實際啟動時，會有兩個 <strong>Sample</strong> 的實體。圖如：  </p><p><img src="/images/b/51.png" alt="ASP.NET Core 3 系列 - 自行建置 Service Provider - 圖例">  </p><p>實際運行上例程式碼，Log Output 的 Sample 實例 HashCode 與 Controller 注入的 Sample 實例 HashCode 並不一樣，表示這兩個式不同的實例，範例執行結果：  </p><p><img src="/images/b/50.png" alt="ASP.NET Core 3 系列 - 自行建置 Service Provider - 範例執行結果">  </p><h2 id="附註"><a href="#附註" class="headerlink" title="附註"></a>附註</h2><p>Sample 類別的範例程式：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISampleSingleton</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">int</span> Id &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Sample</span> : <span class="title">ISampleSingleton</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> _counter;</span><br><span class="line">    <span class="keyword">private</span> <span class="built_in">int</span> _id;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">Sample</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        _id = ++_counter;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">int</span> Id =&gt; _id;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>範例 <em>Controllers\HomeController.cs</em>：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Mvc;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite.Controllers</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">HomeController</span> : <span class="title">Controller</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">readonly</span> ISample _singleton;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">HomeController</span>(<span class="params">ISampleSingleton singleton</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            _singleton = singleton;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="built_in">string</span> <span class="title">Index</span>()</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="string">$&quot;Type: <span class="subst">&#123;_singleton.GetType()&#125;</span>\r\n&quot;</span></span><br><span class="line">                   + <span class="string">$&quot;HashCode: <span class="subst">&#123;_singleton.GetHashCode()&#125;</span>\r\n&quot;</span></span><br><span class="line">                   + <span class="string">$&quot;Id: <span class="subst">&#123;_singleton.Id&#125;</span>&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;一般情況 IHostBuilder 建置 Host 實例的時候，就會自動建置 Service Provider。&lt;br&gt;所以大部份情境不需要自行建置 Service Provider，但如果想在 IHostBuilder 建置出 Host 實例之前取得 Service Provider，就可以用 &lt;strong&gt;IServiceCollection&lt;/strong&gt; 的擴充方法 &lt;code&gt;BuildServiceProvider&lt;/code&gt; 建置出 Service Provider。&lt;br&gt;本篇將介紹 ASP.NET Core 如何透過 &lt;code&gt;BuildServiceProvider&lt;/code&gt; 建置 Service Provider，以及要注意的地方。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - 程式進入點 Main 方法取得 DI 註冊的 Services</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-get-di-services-from-main.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-get-di-services-from-main.html</id>
    <published>2019-10-28T15:47:00.000Z</published>
    <updated>2026-02-03T05:13:46.020Z</updated>
    
    <content type="html"><![CDATA[<p>大多數情況使用 ASP.NET Core 依賴注入 (Dependency Injection, DI) 取得 Services 都是透過 Request 的 Controller 建構子而來，但在程式進入點 Main 方法中，並沒有 Constructor Injection。<br>本篇將介紹如何在程式進入點 Main 方法取得 ASP.NET Core 依賴注入的服務。  </p><span id="more"></span><h2 id="HostBuilder"><a href="#HostBuilder" class="headerlink" title="HostBuilder"></a>HostBuilder</h2><p>在 ASP.NET Core 的起手式，都會透過靜態類別 <code>Host</code> 或 <code>WebHost</code> 建立 <em>HostBuilder</em>，再透過 <em>HostBuilder</em> 建置出 <em>Host</em> 的實例，如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Hosting;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">var</span> hostBuilder = CreateHostBuilder(<span class="keyword">args</span>);</span><br><span class="line">            <span class="keyword">var</span> host = hostBuilder.Build();</span><br><span class="line">            host.Run();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> IHostBuilder <span class="title">CreateHostBuilder</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span> =&gt;</span><br><span class="line">            Host.CreateDefaultBuilder(<span class="keyword">args</span>)</span><br><span class="line">                .ConfigureWebHostDefaults(webBuilder =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    webBuilder</span><br><span class="line">                        .UseStartup&lt;Startup&gt;();</span><br><span class="line">                &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <em>HostBuilder</em> 建置出 <em>Host</em> 的實例之前，需要透過 <code>ConfigureServices</code> 先宣告 DI Services，當 <em>HostBuilder</em> 建置出 <em>Host</em> 實例時，就會依照 <code>ConfigureServices</code> 註冊的配置告知 Service Provider，讓 Service Provider 可以提供請求者 Services。<br>而 Service Provider 的實例會一直存在 <em>Host</em> 的實例之中。  </p><h2 id="取用-Service-Provider"><a href="#取用-Service-Provider" class="headerlink" title="取用 Service Provider"></a>取用 Service Provider</h2><p>為了方便說明，用之前<a href="/article/asp-net-core-3-dependency-injection.html">ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection)</a> 的 Sample 類別當作範例：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">int</span> Id &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISampleTransient</span> : <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISampleScoped</span> : <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISampleSingleton</span> : <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Sample</span> : <span class="title">ISampleTransient</span>, <span class="title">ISampleScoped</span>, <span class="title">ISampleSingleton</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> _counter;</span><br><span class="line">    <span class="keyword">private</span> <span class="built_in">int</span> _id;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">Sample</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        _id = ++_counter;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">int</span> Id =&gt; _id;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>Main</code> 方法 <em>HostBuilder</em> 建置出的 <em>Host</em> 實例，再從 Service Provider 中獲取需要的服務。  </p><p><strong>Singleton</strong> 及 <strong>Transient</strong> 類型的服務，都可以直接透過 Service Provider 取出，但 <strong>Scoped</strong> 類型的服務，必須先建立出 Service Scope，才可以在該 Scope 內的 Service Provider 中取出服務。<br>範例如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">var</span> hostBuilder = CreateHostBuilder(<span class="keyword">args</span>);</span><br><span class="line">        <span class="keyword">var</span> host = hostBuilder.Build();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">var</span> singleton = host.Services.GetService&lt;ISampleSingleton&gt;();</span><br><span class="line">        <span class="keyword">var</span> transient = host.Services.GetService&lt;ISampleTransient&gt;();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">using</span> (<span class="keyword">var</span> scope = host.Services.CreateScope())</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">var</span> <span class="keyword">scoped</span> = scope.ServiceProvider.GetService&lt;ISampleScoped&gt;();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        host.Run();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection">Introduction to Dependency Injection in ASP.NET Core</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;大多數情況使用 ASP.NET Core 依賴注入 (Dependency Injection, DI) 取得 Services 都是透過 Request 的 Controller 建構子而來，但在程式進入點 Main 方法中，並沒有 Constructor Injection。&lt;br&gt;本篇將介紹如何在程式進入點 Main 方法取得 ASP.NET Core 依賴注入的服務。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection)</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-dependency-injection.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-dependency-injection.html</id>
    <published>2019-10-28T14:09:00.000Z</published>
    <updated>2026-02-03T05:13:46.025Z</updated>
    
    <content type="html"><![CDATA[<p>ASP.NET Core 使用了大量的依賴注入 (Dependency Injection, DI)，把控制翻轉 (Inversion Of Control, IoC) 運用的相當落實。<br>DI 可算是 ASP.NET Core 最精華的一部分，有用過 Autofac 或類似的 DI Framework 對此應該不陌生。<br>本篇將介紹 ASP.NET Core 的依賴注入。  </p><span id="more"></span><h2 id="DI-容器介紹"><a href="#DI-容器介紹" class="headerlink" title="DI 容器介紹"></a>DI 容器介紹</h2><p>在沒有使用 DI Framework 的情況下，假設在 UserController 要呼叫 UserLogic，會直接在 UserController 實例化 UserLogic，如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">UserLogic</span> &#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Create</span>(<span class="params">User user</span>)</span> &#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">UserController</span> : <span class="title">Controller</span> &#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Register</span>(<span class="params">User user</span>)</span>&#123;</span><br><span class="line">        <span class="keyword">var</span> logic = <span class="keyword">new</span> UserLogic();</span><br><span class="line">        logic.Create(user);</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>xxx<strong>Logic</strong> 邏輯層分層命名，有興趣可以參考這篇：<a href="/article/software-layered-architecture-pattern.html">軟體分層架構模式</a>  </p></blockquote><p>以上程式基本上沒什麼問題，但程式相依性就差了點。UserController <strong>必須</strong> 要依賴 UserLogic 才可以運作，就算拆出介面改成：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">IUserLogic</span> &#123;</span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">Create</span>(<span class="params">User user</span>)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">UserLogic</span> : <span class="title">IUserLogic</span> &#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Create</span>(<span class="params">User user</span>)</span> &#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">UserController</span> : <span class="title">Controller</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">readonly</span> IUserLogic _userLogic;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">UserController</span>()</span> &#123;</span><br><span class="line">        _userLogic = <span class="keyword">new</span> UserLogic();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Register</span>(<span class="params">User user</span>)</span>&#123;</span><br><span class="line">        _userLogic.Create(user);</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>UserController 與 UserLogic 的相依關係只是從 Action 移到建構子，依然還是很強的相依關係。  </p><p>ASP.NET Core 透過 DI 容器，切斷這些相依關係，實例的產生不會是在使用方(指上例 UserController 建構子的 <code>new</code>)，而是在 DI 容器。<br>DI 容器的註冊方式也很簡單，在 <code>ConfigureServices</code> 註冊。<em>Startup.cs</em> 範例如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        services.AddMvc();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>services</strong> 就是一個 DI 容器。<br> 此例把 MVC 的服務註冊到 DI 容器，等到需要用到 MVC 服務時，才從 DI 容器取得物件實例。  </p></blockquote><p>基本上要注入到 Service 的類別沒什麼限制，除了靜態類別。<br>以下範例程式就只是一般的 Class 繼承 Interface：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">int</span> Id &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Sample</span> : <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> _counter;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">Sample</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        Id = ++_counter;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">int</span> Id &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要注入的 Service 需要在 <code>ConfigureServices</code> 中註冊實做類別。<em>Startup.cs</em> 如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.DependencyInjection;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            services.AddMvc();</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 在 Startup 註冊服務</span></span><br><span class="line">            services.AddScoped&lt;ISample, Sample&gt;();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            app.UseRouting();</span><br><span class="line">            app.UseEndpoints(endpoints =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                endpoints.MapControllerRoute(</span><br><span class="line">                    <span class="string">&quot;default&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;&#123;controller=Home&#125;/&#123;action=Index&#125;/&#123;id?&#125;&quot;</span></span><br><span class="line">                );</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>第一個泛型為注入的類型<br> 建議用 Interface 來包裝，這樣在才能把相依關係拆除。  </li><li>第二個泛型為實做的類別</li></ul><p>ASP.NET Core 3 開始，建議透過 Generic Host 建立 Web Host，所以也能用 HostBuilter 中的 <code>ConfigureServices</code> 方法註冊：</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Hosting;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.DependencyInjection;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">var</span> hostBuilder = CreateHostBuilder(<span class="keyword">args</span>);</span><br><span class="line">            <span class="keyword">var</span> host = hostBuilder.Build();</span><br><span class="line">            host.Run();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> IHostBuilder <span class="title">CreateHostBuilder</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span> =&gt;</span><br><span class="line">            Host.CreateDefaultBuilder(<span class="keyword">args</span>)</span><br><span class="line">                .ConfigureServices(services =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="comment">// 在 Generic Host Builder 註冊服務</span></span><br><span class="line">                    <span class="comment">// services.AddScoped&lt;ISample, Sample&gt;();</span></span><br><span class="line">                &#125;)</span><br><span class="line">                .ConfigureWebHostDefaults(webBuilder =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    webBuilder</span><br><span class="line">                        .ConfigureServices(services =&gt;</span><br><span class="line">                        &#123;</span><br><span class="line">                            <span class="comment">// 在 Web Host Builder 註冊服務</span></span><br><span class="line">                            <span class="comment">// services.AddScoped&lt;ISample, Sample&gt;();</span></span><br><span class="line">                        &#125;)</span><br><span class="line">                        .UseStartup&lt;Startup&gt;();</span><br><span class="line">                &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>注意！重複註冊，會產生出重複的結果。  </p></blockquote><h2 id="DI-運作方式"><a href="#DI-運作方式" class="headerlink" title="DI 運作方式"></a>DI 運作方式</h2><p>ASP.NET Core 的 DI 是採用 Constructor Injection，也就是說會把實例化的物件從建構子傳入。<br>如果要取用 DI 容器內的物件，只要在建構子加入相對的 Interface 即可。例如 <em>Controllers\HomeController.cs</em>：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Mvc;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite.Controllers</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">HomeController</span> : <span class="title">Controller</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">readonly</span> ISample _sample;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">HomeController</span>(<span class="params">ISample sample</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            _sample = sample;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="built_in">string</span> <span class="title">Index</span>()</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="string">$&quot;[<span class="subst">&#123;<span class="keyword">nameof</span>(ISample)&#125;</span>]\r\n&quot;</span></span><br><span class="line">                   + <span class="string">$&quot;Id: <span class="subst">&#123;_sample.Id&#125;</span>\r\n&quot;</span></span><br><span class="line">                   + <span class="string">$&quot;HashCode: <span class="subst">&#123;_sample.GetHashCode()&#125;</span>\r\n&quot;</span></span><br><span class="line">                   + <span class="string">$&quot;Type: <span class="subst">&#123;_sample.GetType()&#125;</span>&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>輸出內容如下：  </p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[ISample]</span><br><span class="line">Id: 1</span><br><span class="line">HashCode: 14145203</span><br><span class="line">Tpye: MyWebsite.Sample</span><br></pre></td></tr></table></figure><p>ASP.NET Core 實例化 Controller 時，發現建構子有 ISample 這個類型的參數，就把 Sample 的實例注入給該 Controller。  </p><blockquote><p>每個 Request 都會把 Controller 實例化，所以 DI 容器會從建構子注入 ISample 的實例，把 sample 存到欄位 _sample 中，就能確保 Action 能夠使用到被注入進來的 ISample 實例。  </p></blockquote><p>注入實例過程，情境如下：  </p><p><img src="/images/ironman/i04-4.png" alt="ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection) - 注入實例"></p><h2 id="Service-生命週期"><a href="#Service-生命週期" class="headerlink" title="Service 生命週期"></a>Service 生命週期</h2><p>註冊在 DI 容器的 Service 有分三種生命週期：  </p><ul><li><strong>Transient</strong><br> 每次注入時，都重新 <code>new</code> 一個新的實例。  </li><li><strong>Scoped</strong><br> 每個 <strong>Request</strong> 都重新 <code>new</code> 一個新的實例，同一個 <strong>Request</strong> 不管經過多少個 Pipeline 都是用同一個實例。上例所使用的就是 <strong>Scoped</strong>。  </li><li><strong>Singleton</strong><br> 被實例化後就不會消失，程式運行期間只會有一個實例。</li></ul><p>小改一下 Sample 類別的範例程式：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">int</span> Id &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISampleTransient</span> : <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISampleScoped</span> : <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">ISampleSingleton</span> : <span class="title">ISample</span></span><br><span class="line">&#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Sample</span> : <span class="title">ISampleTransient</span>, <span class="title">ISampleScoped</span>, <span class="title">ISampleSingleton</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> _counter;</span><br><span class="line">    <span class="keyword">private</span> <span class="built_in">int</span> _id;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">Sample</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        _id = ++_counter;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">int</span> Id =&gt; _id;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>Startup.ConfigureServices</code> 中註冊三種不同生命週期的服務。如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        services.AddTransient&lt;ISampleTransient, Sample&gt;();</span><br><span class="line">        services.AddScoped&lt;ISampleScoped, Sample&gt;();</span><br><span class="line">        services.AddSingleton&lt;ISampleSingleton, Sample&gt;();</span><br><span class="line">        <span class="comment">// Singleton 也可以用以下方法註冊</span></span><br><span class="line">        <span class="comment">// services.AddSingleton&lt;ISampleSingleton&gt;(new Sample());</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果有特殊需求，也可以透過委派的方式註冊。如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        services.AddTransient&lt;ISampleTransient&gt;(srv =&gt; &#123;</span><br><span class="line">            <span class="keyword">var</span> sample = <span class="keyword">new</span> Sample();</span><br><span class="line">            <span class="comment">// Do something ...</span></span><br><span class="line">            <span class="keyword">return</span> sample;</span><br><span class="line">        &#125;);</span><br><span class="line">        services.AddScoped&lt;ISampleScoped&gt;(srv =&gt; <span class="keyword">new</span> Sample());</span><br><span class="line">        services.AddSingleton&lt;ISampleSingleton&gt;(srv =&gt; <span class="keyword">new</span> Sample());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Service-Injection"><a href="#Service-Injection" class="headerlink" title="Service Injection"></a>Service Injection</h2><blockquote><p><strong>只要是透過 WebHost 產生實例的類別，都可以在建構子定義型態注入</strong>。  </p></blockquote><p>所以 Controller、View、Filter、Middleware 或自訂的 Service 等都可以被注入。<br>此篇我只用 Controller、View、Service 做為範例。  </p><h3 id="Controller"><a href="#Controller" class="headerlink" title="Controller"></a>Controller</h3><p>在 HomeController 中注入上例的三個 Services，範例 <em>Controllers\HomeController.cs</em>：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">HomeController</span> : <span class="title">Controller</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">readonly</span> ISample _transient;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">readonly</span> ISample _scoped;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">readonly</span> ISample _singleton;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">HomeController</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">        ISampleTransient transient,</span></span></span><br><span class="line"><span class="params"><span class="function">        ISampleScoped <span class="keyword">scoped</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        ISampleSingleton singleton</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        _transient = transient;</span><br><span class="line">        _scoped = <span class="keyword">scoped</span>;</span><br><span class="line">        _singleton = singleton;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> IActionResult <span class="title">Index</span>()</span> &#123;</span><br><span class="line">        ViewBag.TransientId = _transient.Id;</span><br><span class="line">        ViewBag.TransientHashCode = _transient.GetHashCode();</span><br><span class="line"></span><br><span class="line">        ViewBag.ScopedId = _scoped.Id;</span><br><span class="line">        ViewBag.ScopedHashCode = _scoped.GetHashCode();</span><br><span class="line"></span><br><span class="line">        ViewBag.SingletonId = _singleton.Id;</span><br><span class="line">        ViewBag.SingletonHashCode = _singleton.GetHashCode();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> View();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em>Views\Home\Index.cshtml</em>：  </p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">table</span> <span class="attr">border</span>=<span class="string">&quot;1&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span> <span class="attr">colspan</span>=<span class="string">&quot;3&quot;</span>&gt;</span>Cotroller<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Lifetimes<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Hash Code<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Transient<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.TransientId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.TransientHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Scoped<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.ScopedId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.ScopedHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Singleton<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.SingletonId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.SingletonHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span><br></pre></td></tr></table></figure><p>輸出內容如下：  </p><p><img src="/images/ironman/i04-1.png" alt="ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection) - Service 生命週期 - Controller"><br>從左到又打開頁面三次，可以發現 <strong>Singleton</strong> 的 Id 及 HashCode 都是一樣的，此例還看不太出來 <strong>Transient</strong> 及 <strong>Scoped</strong> 的差異。</p><p>Service 實例產生方式：  </p><p><img src="/images/a/209.gif" alt="ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection) - 實例產生動畫"></p><p>圖例說明：  </p><ul><li><strong>A</strong> 為 <strong>Singleton</strong> 物件實例<br> 一但實例化，就會一直存在於 DI 容器中。  </li><li><strong>B</strong> 為 <strong>Scoped</strong> 物件實例<br> 每次 <strong>Request</strong> 就會產生新的實例在 DI 容器中，讓同 <strong>Request</strong> 週期的使用方，拿到同一個實例。  </li><li><strong>C</strong> 為 <strong>Transient</strong> 物件實例<br> 只要跟 DI 容器請求這個類型，就會取得新的實例。</li></ul><h3 id="View"><a href="#View" class="headerlink" title="View"></a>View</h3><p>View 注入 Service 的方式，直接在 <code>*.cshtml</code> 使用 <code>@inject</code>，如範例 <em>Views\Home\Index.cshtml</em>：  </p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">@using MyWebsite</span><br><span class="line"></span><br><span class="line">@inject ISampleTransient transient</span><br><span class="line">@inject ISampleScoped scoped</span><br><span class="line">@inject ISampleSingleton singleton</span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">table</span> <span class="attr">border</span>=<span class="string">&quot;1&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span> <span class="attr">colspan</span>=<span class="string">&quot;3&quot;</span>&gt;</span>Cotroller<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Lifetimes<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Hash Code<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Transient<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.TransientId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.TransientHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Scoped<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.ScopedId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.ScopedHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Singleton<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.SingletonId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.SingletonHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">hr</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">table</span> <span class="attr">border</span>=<span class="string">&quot;1&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span> <span class="attr">colspan</span>=<span class="string">&quot;3&quot;</span>&gt;</span>View<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Lifetimes<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Hash Code<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Transient<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@transient.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@transient.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Scoped<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@scoped.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@scoped.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Singleton<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@singleton.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@singleton.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span><br></pre></td></tr></table></figure><p>輸出內容如下：  </p><p><img src="/images/ironman/i04-2.png" alt="ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection) - Service 生命週期 - View">  </p><p>從左到又打開頁面三次，<strong>Singleton</strong> 的 Id 及 HashCode 如前例是一樣的。<br><strong>Transient</strong> 及 <strong>Scoped</strong> 的差異在這次就有明顯差異，<strong>Scoped</strong> 在同一次 Request 的 Id 及 HashCode 都是一樣的，如紅綠籃框。  </p><h3 id="Service"><a href="#Service" class="headerlink" title="Service"></a>Service</h3><p>簡單建立一個 CustomService，注入上例三個 Service，程式碼類似 HomeController。如下 <em>Services\CustomService.cs</em>：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CustomService</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> ISample Transient &#123; <span class="keyword">get</span>; <span class="keyword">private</span> <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> ISample Scoped &#123; <span class="keyword">get</span>; <span class="keyword">private</span> <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> ISample Singleton &#123; <span class="keyword">get</span>; <span class="keyword">private</span> <span class="keyword">set</span>; &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CustomService</span>(<span class="params">ISampleTransient transient,</span></span></span><br><span class="line"><span class="params"><span class="function">        ISampleScoped <span class="keyword">scoped</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        ISampleSingleton singleton</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        Transient = transient;</span><br><span class="line">        Scoped = <span class="keyword">scoped</span>;</span><br><span class="line">        Singleton = singleton;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>註冊 CustomService：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">        services.AddScoped&lt;CustomService, CustomService&gt;();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>第一個泛型也可以是類別，不一定要是介面。<br> 缺點是使用方以 Class 作為相依關係，變成強關聯的依賴。  </p></blockquote><p>在 <em>Views\Home\Index.cshtml</em> 注入 CustomService：  </p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">@using MyWebsite</span><br><span class="line"></span><br><span class="line">@inject ISampleTransient transient</span><br><span class="line">@inject ISampleScoped scoped</span><br><span class="line">@inject ISampleSingleton singleton</span><br><span class="line">@inject CustomService customService</span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">table</span> <span class="attr">border</span>=<span class="string">&quot;1&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span> <span class="attr">colspan</span>=<span class="string">&quot;3&quot;</span>&gt;</span>Cotroller<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Lifetimes<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Hash Code<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Transient<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.TransientId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.TransientHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Scoped<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.ScopedId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.ScopedHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Singleton<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.SingletonId<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@ViewBag.SingletonHashCode<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">hr</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">table</span> <span class="attr">border</span>=<span class="string">&quot;1&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span> <span class="attr">colspan</span>=<span class="string">&quot;3&quot;</span>&gt;</span>View<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Lifetimes<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Hash Code<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Transient<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@transient.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@transient.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Scoped<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@scoped.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@scoped.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Singleton<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@singleton.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@singleton.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">hr</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">table</span> <span class="attr">border</span>=<span class="string">&quot;1&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span> <span class="attr">colspan</span>=<span class="string">&quot;3&quot;</span>&gt;</span>Custom Service<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Lifetimes<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Hash Code<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Transient<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@customService.Transient.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@customService.Transient.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Scoped<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@customService.Scoped.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@customService.Scoped.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">tr</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>Singleton<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@customService.Singleton.Id<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;<span class="name">td</span>&gt;</span>@customService.Singleton.GetHashCode()<span class="tag">&lt;/<span class="name">td</span>&gt;</span><span class="tag">&lt;/<span class="name">tr</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">table</span>&gt;</span></span><br></pre></td></tr></table></figure><p>輸出內容如下：  </p><p><img src="/images/ironman/i04-3.png" alt="ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection) - Service 生命週期 - Servie">  </p><p>從左到又打開頁面三次：  </p><ul><li><strong>Transient</strong><br> 如預期，每次注入都是不一樣的實例。  </li><li><strong>Scoped</strong><br> 在同一個 Requset 中，不論是在哪邊被注入，都是同樣的實例。  </li><li><strong>Singleton</strong><br> 不管 Requset 多少次，都會是同一個實例。</li></ul><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection">Introduction to Dependency Injection in ASP.NET Core</a>  </li><li><a href="https://joonasw.net/view/aspnet-core-di-deep-dive">ASP.NET Core Dependency Injection Deep Dive</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;ASP.NET Core 使用了大量的依賴注入 (Dependency Injection, DI)，把控制翻轉 (Inversion Of Control, IoC) 運用的相當落實。&lt;br&gt;DI 可算是 ASP.NET Core 最精華的一部分，有用過 Autofac 或類似的 DI Framework 對此應該不陌生。&lt;br&gt;本篇將介紹 ASP.NET Core 的依賴注入。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - Middleware 讀取 Request/Response Body</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-read-request-response-body.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-read-request-response-body.html</id>
    <published>2019-10-24T17:18:00.000Z</published>
    <updated>2026-02-03T05:13:46.047Z</updated>
    
    <content type="html"><![CDATA[<p>本篇將介紹 ASP.NET Core 3 透過 Middleware 讀寫 Request&#x2F;Response Body 的用法。<br>若對 Middleware 基本知識不熟習的話，可以參考 <a href="/article/asp-net-core-3-middleware">ASP.NET Core 3 系列 - Middleware</a>。  </p><span id="more"></span><h2 id="讀取-Request-Body"><a href="#讀取-Request-Body" class="headerlink" title="讀取 Request Body"></a>讀取 Request Body</h2><p>在 Middleware 的 Invoke 可以獲取到 HttpContext，其中會包含 Request 的內容，包含 URL、Header 等。<br>Request Body 是 Stream 型別，要取出內容，可以透過 StreamReader 如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System;</span><br><span class="line"><span class="keyword">using</span> System.IO;</span><br><span class="line"><span class="keyword">using</span> System.Threading.Tasks;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">FirstMiddleware</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">readonly</span> RequestDelegate _next;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">FirstMiddleware</span>(<span class="params">RequestDelegate next</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            _next = next;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">async</span> Task <span class="title">Invoke</span>(<span class="params">HttpContext context</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="built_in">string</span> requestContent;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">using</span> (<span class="keyword">var</span> reader = <span class="keyword">new</span> StreamReader(context.Request.Body))</span><br><span class="line">            &#123;</span><br><span class="line">                requestContent = <span class="keyword">await</span> reader.ReadToEndAsync();</span><br><span class="line">                context.Request.Body.Seek(<span class="number">0</span>, SeekOrigin.Begin);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">await</span> _next(context);</span><br><span class="line"></span><br><span class="line">            Console.WriteLine(<span class="string">$&quot;Request.Body=<span class="subst">&#123;requestContent&#125;</span>&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>注意！<code>Seek(0, SeekOrigin.Begin)</code> 非常重要，如果把 Stream 讀完後，不把 Stream Position 還原，之後的 Pipeline、Action 在取得 Request Body 時，會從 Stream 的結尾開始取資料，意味著取出來都是空資料。  </p></blockquote><h2 id="讀取-Response-Body"><a href="#讀取-Response-Body" class="headerlink" title="讀取 Response Body"></a>讀取 Response Body</h2><p>Middleware 取得 Response Body 相較於 Request 麻煩很多，因為 Response.Body 的 Stream 並不允許被讀取讀取，但可以被替換。<br>所以在 Response.Body 開始被寫入之前，先抽換成 MemoryStream；這樣之後的 Pipeline 在寫入 Response.Body 時，實際上都是寫入到被抽換的 MemoryStream 之中。<br>等到下層 Pipeline 都做完的時候，就可以讀取 MemoryStream 的資料，讀完後再把 MemoryStream 寫到真實的 Response.Body。<br>範例如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System;</span><br><span class="line"><span class="keyword">using</span> System.IO;</span><br><span class="line"><span class="keyword">using</span> System.Threading.Tasks;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">SecondMiddleware</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">readonly</span> RequestDelegate _next;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">SecondMiddleware</span>(<span class="params">RequestDelegate next</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            _next = next;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">async</span> Task <span class="title">Invoke</span>(<span class="params">HttpContext context</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="built_in">string</span> responseContent;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">var</span> originalBodyStream = context.Response.Body;</span><br><span class="line">            <span class="keyword">using</span> (<span class="keyword">var</span> fakeResponseBody = <span class="keyword">new</span> MemoryStream())</span><br><span class="line">            &#123;</span><br><span class="line">                context.Response.Body = fakeResponseBody;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">await</span> _next(context);</span><br><span class="line"></span><br><span class="line">                fakeResponseBody.Seek(<span class="number">0</span>, SeekOrigin.Begin);</span><br><span class="line">                <span class="keyword">using</span> (<span class="keyword">var</span> reader = <span class="keyword">new</span> StreamReader(fakeResponseBody))</span><br><span class="line">                &#123;</span><br><span class="line">                    responseContent = <span class="keyword">await</span> reader.ReadToEndAsync();</span><br><span class="line">                    fakeResponseBody.Seek(<span class="number">0</span>, SeekOrigin.Begin);</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">await</span> fakeResponseBody.CopyToAsync(originalBodyStream);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            Console.WriteLine(<span class="string">$&quot;Response.Body=<span class="subst">&#123;responseContent&#125;</span>&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上例 <code>Seek(0, SeekOrigin.Begin)</code> 被呼叫了兩次，原因：  </p><ol><li><code>_next(context)</code> 會寫入內容到 <em>fakeResponseBody</em>，導致 Stream Position 會被指到結尾，為了讀取 <em>fakeResponseBody</em> 內容，所以要把 Stream Position 指回起始位置。  </li><li>讀取完 <em>fakeResponseBody</em> 內容後，Stream Position 又會被指到結尾，為了把 <em>fakeResponseBody</em> 複製回原本的 Response.Body，所以要把 Stream Position 指回起始位置。</li></ol><p>執行流程如下：  </p><p><img src="/images/b/49.gif" alt="ASP.NET Core 3 系列 - Middleware 讀取 Request&#x2F;Response Body - 範例程式執行流程"></p><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://gunnarpeipman.com/aspnet-core-request-body/">Reading request body in ASP.NET Core</a>  </li><li><a href="https://github.com/aspnet/AspNetCore/issues/12505">Calling Request.EnableRewind throw on 3.0.0-preview7</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本篇將介紹 ASP.NET Core 3 透過 Middleware 讀寫 Request&amp;#x2F;Response Body 的用法。&lt;br&gt;若對 Middleware 基本知識不熟習的話，可以參考 &lt;a href=&quot;/article/asp-net-core-3-middleware&quot;&gt;ASP.NET Core 3 系列 - Middleware&lt;/a&gt;。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
    <category term="Middleware" scheme="https://blog.johnwu.cc/tags/middleware/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - Middleware</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-middleware.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-middleware.html</id>
    <published>2019-10-24T13:39:00.000Z</published>
    <updated>2026-02-03T05:13:46.027Z</updated>
    
    <content type="html"><![CDATA[<p>過去 ASP.NET 中使用的 HTTP Modules 及 HTTP Handlers，在 ASP.NET Core 中已不復存在，取而代之的是 Middleware。<br>Middleware 除了簡化了 HTTP Modules&#x2F;Handlers 的使用方式，還帶入了 Pipeline 的概念。<br>本篇將介紹 ASP.NET Core 3 的 Middleware 概念及用法。  </p><span id="more"></span><h2 id="Middleware-概念"><a href="#Middleware-概念" class="headerlink" title="Middleware 概念"></a>Middleware 概念</h2><p>ASP.NET Core 在 Middleware 的官方說明中，使用了 Pipeline 這個名詞，意旨 Middleware 像水管一樣可以串聯在一起，所有的 Request 及 Response 都會層層經過這些水管。<br>用圖例可以很容易理解，如下圖：  </p><p><img src="/images/ironman/i03-1.png" alt="ASP.NET Core 3 系列 - Middleware - 概念"></p><h2 id="App-Use"><a href="#App-Use" class="headerlink" title="App.Use"></a>App.Use</h2><p>Middleware 的註冊方式是在 <em>Startup.cs</em> 的 <code>Configure</code> 對 <code>IApplicationBuilder</code> 使用 <code>Use</code> 方法註冊。<br>大部分擴充的 Middleware 也都是以 <strong>Use</strong> 開頭的方法註冊，例如：  </p><ul><li><strong>UseRouting()</strong>：Request 路由使用的 Middleware  </li><li><strong>UseRewriter()</strong>：URL rewriting 的 Middleware</li></ul><p>一個簡單的 Middleware 範例。<em>Startup.cs</em> 如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            app.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;First Middleware in. \r\n&quot;</span>);</span><br><span class="line">                <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;First Middleware out. \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Second Middleware in. \r\n&quot;</span>);</span><br><span class="line">                <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Second Middleware out. \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Third Middleware in. \r\n&quot;</span>);</span><br><span class="line">                <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Third Middleware out. \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Run(<span class="keyword">async</span> context =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World! \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用瀏覽器打開網站任意連結，輸出結果：  </p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">First Middleware in.</span><br><span class="line">Second Middleware in.</span><br><span class="line">Third Middleware in.</span><br><span class="line">Hello World!</span><br><span class="line">Third Middleware out.</span><br><span class="line">Second Middleware out.</span><br><span class="line">First Middleware out.</span><br></pre></td></tr></table></figure><blockquote><p>在 Pipeline 的概念中，註冊順序是很重要的事情。資料經過的順序一定是<strong>先進後出</strong>。  </p></blockquote><p>Request 流程如下圖：  </p><p><img src="https://blog.johnwu.cc/images/a/114.gif" alt="ASP.NET Core 3 系列 - Middleware">  </p><p>Middleware 也可以作為攔截使用，<em>Startup.cs</em> 如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            app.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;First Middleware in. \r\n&quot;</span>);</span><br><span class="line">                <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;First Middleware out. \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Second Middleware in. \r\n&quot;</span>);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 水管阻塞，封包不往後送</span></span><br><span class="line">                <span class="keyword">var</span> condition = <span class="literal">false</span>;</span><br><span class="line">                <span class="keyword">if</span>(condition) &#123;</span><br><span class="line">                    <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Second Middleware out. \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Third Middleware in. \r\n&quot;</span>);</span><br><span class="line">                <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Third Middleware out. \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Run(<span class="keyword">async</span> context =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World! \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>輸出結果：  </p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">First Middleware in.</span><br><span class="line">Second Middleware in.</span><br><span class="line">Second Middleware out.</span><br><span class="line">First Middleware out.</span><br></pre></td></tr></table></figure><p>在 Second Middleware 中，因為沒有達成條件，所以封包也就不在往後面的水管傳送。流程如圖：  </p><p><img src="/images/ironman/i03-2.png" alt="ASP.NET Core 3 系列 - Middleware - 概念">  </p><h2 id="App-Run"><a href="#App-Run" class="headerlink" title="App.Run"></a>App.Run</h2><p><code>Run</code> 是 Middleware 的最後一個行為，以上面圖例來說，就是最末端的 Action。<br>它不像 <code>Use</code> 能串聯其它 Middleware，但 <code>Run</code> 還是能完整的使用 Request 及 Response。  </p><h2 id="App-Map"><a href="#App-Map" class="headerlink" title="App.Map"></a>App.Map</h2><p><code>Map</code> 是能用來處理一些簡單路由的 Middleware，可依照不同的 URL 指向不同的 <code>Run</code> 及註冊不同的 <code>Use</code>。<br>新增一個路由，<em>Startup.cs</em> 如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            app.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;First Middleware in. \r\n&quot;</span>);</span><br><span class="line">                <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;First Middleware out. \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Map(<span class="string">&quot;/second&quot;</span>, mapApp =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                mapApp.Use(<span class="keyword">async</span> (context, next) =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Second Middleware in. \r\n&quot;</span>);</span><br><span class="line">                    <span class="keyword">await</span> next.Invoke();</span><br><span class="line">                    <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Second Middleware out. \r\n&quot;</span>);</span><br><span class="line">                &#125;);</span><br><span class="line">                mapApp.Run(<span class="keyword">async</span> context =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Second. \r\n&quot;</span>);</span><br><span class="line">                &#125;);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.Run(<span class="keyword">async</span> context =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World! \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>開啟網站任意連結，會顯示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">First Middleware in.</span><br><span class="line">Hello World!</span><br><span class="line">First Middleware out.</span><br></pre></td></tr></table></figure><p>開啟網站 <code>http://localhost:5000/second</code>，則會顯示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">First Middleware in.</span><br><span class="line">Second Middleware in.</span><br><span class="line">Second.</span><br><span class="line">Second Middleware out.</span><br><span class="line">First Middleware out.</span><br></pre></td></tr></table></figure><h2 id="建立-Middleware-類別"><a href="#建立-Middleware-類別" class="headerlink" title="建立 Middleware 類別"></a>建立 Middleware 類別</h2><p>如果 Middleware 全部都寫在 <em>Startup.cs</em>，程式碼應該很難維護，所以應該把自製的 Middleware 邏輯獨立出來。<br>建立 Middleware 類別不需要額外繼承其它類別或介面，一般的類別即可，<em>FirstMiddleware.cs</em> 範例如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System.Threading.Tasks;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">FirstMiddleware</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">readonly</span> RequestDelegate _next;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">FirstMiddleware</span>(<span class="params">RequestDelegate next</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            _next = next;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">async</span> Task <span class="title">Invoke</span>(<span class="params">HttpContext context</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">$&quot;<span class="subst">&#123;<span class="keyword">nameof</span>(FirstMiddleware)&#125;</span> in. \r\n&quot;</span>);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">await</span> _next(context);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">$&quot;<span class="subst">&#123;<span class="keyword">nameof</span>(FirstMiddleware)&#125;</span> out. \r\n&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="全域註冊"><a href="#全域註冊" class="headerlink" title="全域註冊"></a>全域註冊</h3><p>在 <em>Startup.cs</em> 的 <code>Configure</code> 註冊 Middleware 就可以套用到所有的 Request。如下：</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            app.UseMiddleware&lt;FirstMiddleware&gt;();</span><br><span class="line"></span><br><span class="line">            app.Run(<span class="keyword">async</span> context =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World! \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="區域註冊"><a href="#區域註冊" class="headerlink" title="區域註冊"></a>區域註冊</h3><p>Middleware 也可以只套用在特定的 Controller 或 Action。註冊方式如 <em>Controllers\HomeController.cs</em>：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ..</span></span><br><span class="line">[<span class="meta">MiddlewareFilter(typeof(FirstMiddleware))</span>]</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">HomeController</span> : <span class="title">Controller</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    [<span class="meta">MiddlewareFilter(typeof(SecondMiddleware))</span>]</span><br><span class="line">    <span class="function"><span class="keyword">public</span> IActionResult <span class="title">Index</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Extensions"><a href="#Extensions" class="headerlink" title="Extensions"></a>Extensions</h3><p>大部分擴充的 Middleware 都會用一個靜態方法包裝，如：<code>UseRouting()</code>、<code>UseRewriter()</code>等。<br>自製的 Middleware 當然也可以透過靜態方法包，範例 <em>CustomMiddlewareExtensions.cs</em> 如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title">CustomMiddlewareExtensions</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> IApplicationBuilder <span class="title">UseFirstMiddleware</span>(<span class="params"><span class="keyword">this</span> IApplicationBuilder builder</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">return</span> builder.UseMiddleware&lt;FirstMiddleware&gt;();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>註冊 Extension Middleware 的方式如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            app.UseFirstMiddleware();</span><br><span class="line"></span><br><span class="line">            app.Run(<span class="keyword">async</span> context =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World! \r\n&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware">ASP.NET Core Middleware Fundamentals</a>  </li><li><a href="https://dotnetcoretutorials.com/2017/03/10/creating-custom-middleware-asp-net-core/">Creating Custom Middleware In ASP.NET Core</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;過去 ASP.NET 中使用的 HTTP Modules 及 HTTP Handlers，在 ASP.NET Core 中已不復存在，取而代之的是 Middleware。&lt;br&gt;Middleware 除了簡化了 HTTP Modules&amp;#x2F;Handlers 的使用方式，還帶入了 Pipeline 的概念。&lt;br&gt;本篇將介紹 ASP.NET Core 3 的 Middleware 概念及用法。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
    <category term="Middleware" scheme="https://blog.johnwu.cc/tags/middleware/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - 程式生命週期 (Application Lifetime)</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-application-lifetime.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-application-lifetime.html</id>
    <published>2019-10-23T17:42:00.000Z</published>
    <updated>2026-02-03T05:14:42.549Z</updated>
    
    <content type="html"><![CDATA[<p>要了解程式的運作原理，要先知道程式的進入點及生命週期。<br>過往 ASP.NET MVC 啟動方式，是繼承 <code>HttpApplication</code> 做為網站開始的進入點。<br>ASP.NET Core 改變了網站啟動的方式，是用 Console Application 的方式，Host Kestrel，提供 HTTP 的服務。<br>本篇將介紹 ASP.NET Core 3 的程式生命週期 (Application Lifetime) 及捕捉 Application 停啟事件。  </p><span id="more"></span><h2 id="程式進入點"><a href="#程式進入點" class="headerlink" title="程式進入點"></a>程式進入點</h2><p>.NET Core 把 Web 及 Console 專案都變成一樣的啟動方式，預設從 <em>Program.cs</em> 的 <code>Main()</code> 做為程式進入點，再從程式進入點把 Kestrel 實例化。  </p><p>透過 .NET Core CLI 建置的 <em>Program.cs</em> 內容大致如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Hosting;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            CreateHostBuilder(<span class="keyword">args</span>).Build().Run();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> IHostBuilder <span class="title">CreateHostBuilder</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span> =&gt;</span><br><span class="line">            Host.CreateDefaultBuilder(<span class="keyword">args</span>)</span><br><span class="line">                .ConfigureWebHostDefaults(webBuilder =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    webBuilder.UseStartup&lt;Startup&gt;();</span><br><span class="line">                &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Main()</code> 透過 CreateHostBuilder 方法宣告需要相依的相關服務，並設定 WebHost 啟動後要執行 <code>Startup</code> 類別。  </p><ul><li><strong>CreateHostBuilder</strong><br> 透過此方法宣告相依的服務及組態設定等，其中包含 WebHost。<br> 可以在 Host 產生之前設定一些<strong>前置準備</strong>動作，當 Host 建立完成時，就可以使用已準備好的物件等。  </li><li><strong>UseStartup</strong><br> 設定 WebHostBuilder 產生的 WebHost 時，所要執行的類別。  </li><li><strong>Build</strong><br> 當前置準備都設定完成後，就可以呼叫此方法實例化 Host，同時也會實例化 WebHost。  </li><li><strong>Run</strong><br> 啟動 Host。</li></ul><blockquote><p>.NET Core 3.0 官方建議的方式是透過 Generic Host 建立 Web Host。<br>但如果真的不想透過 Generic Host 建立 Web Host，可改成以下方式：  </p></blockquote><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Hosting;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">var</span> hostBuilder = CreateHostBuilder(<span class="keyword">args</span>);</span><br><span class="line">            <span class="keyword">var</span> host = hostBuilder.Build();</span><br><span class="line">            host.Run();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> IHostBuilder <span class="title">CreateHostBuilder</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span> =&gt;</span><br><span class="line">            WebHost.CreateDefaultBuilder(<span class="keyword">args</span>)</span><br><span class="line">                .UseStartup&lt;Startup&gt;();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Startup-cs"><a href="#Startup-cs" class="headerlink" title="Startup.cs"></a>Startup.cs</h3><p>Host 建置時，WebHost 會呼叫 <code>UseStartup</code> 泛型類別的 <strong>ConfigureServices</strong> 方法。<br>Host 啟動後，WebHost 會呼叫 <code>UseStartup</code> 泛型類別的 <strong>Configure</strong> 方法。  </p><p>透過 .NET Core CLI 建置的 <em>Startup.cs</em> 內容大致如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Hosting;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.DependencyInjection;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="comment">// 註冊 Services ...</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app, IWebHostEnvironment env</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">if</span> (env.IsDevelopment())</span><br><span class="line">            &#123;</span><br><span class="line">                app.UseDeveloperExceptionPage();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            app.UseRouting();</span><br><span class="line"></span><br><span class="line">            app.UseEndpoints(endpoints =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                endpoints.MapGet(<span class="string">&quot;/&quot;</span>, <span class="keyword">async</span> context =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World!&quot;</span>);</span><br><span class="line">                &#125;);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>ConfigureServices</strong><br>ConfigureServices 是用來將服務註冊到 DI 容器用的。這個方法可不實做，並不是必要的方法。<br><em>(DI 可以參考這篇：<a href="/article/asp-net-core-3-dependency-injection.html">ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection)</a>)</em>  </li><li><strong>Configure</strong><br>這是必要的方法，一定要時做。但 <code>Configure</code> 方法的參數並不固定，參數的實例都是從 WebHost 注入進來，可依需求增減需要的參數。  <ul><li><strong>IApplicationBuilder</strong> 是最重要的參數也是必要的參數，Request 進出的 Pipeline 都是透過 ApplicationBuilder 來設定。<br><em>(Pipeline 可以參考這篇：<a href="/article/asp-net-core-3-middleware.html">ASP.NET Core 3 系列 - Middleware</a>)</em></li></ul></li></ul><p>對 WebHost 來說 <em>Startup.cs</em> 並不是必要存在的功能。<br>可以試著把 <em>Startup.cs</em> 中的兩個方法，都改成在 WebHost Builder 設定，變成啟動的前置準備。<em>Program.cs</em> 如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Hosting;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">var</span> hostBuilder = CreateHostBuilder(<span class="keyword">args</span>);</span><br><span class="line">            <span class="keyword">var</span> host = hostBuilder.Build();</span><br><span class="line">            host.Run();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> IHostBuilder <span class="title">CreateHostBuilder</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span> =&gt;</span><br><span class="line">            Host.CreateDefaultBuilder(<span class="keyword">args</span>)</span><br><span class="line">                .ConfigureServices(services =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="comment">// Generic Host 註冊 Services ...</span></span><br><span class="line">                &#125;)</span><br><span class="line">                .ConfigureWebHostDefaults(webBuilder =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    webBuilder</span><br><span class="line">                        .ConfigureServices(services =&gt;</span><br><span class="line">                        &#123;</span><br><span class="line">                            <span class="comment">// Web Host 註冊 Services ...</span></span><br><span class="line">                        &#125;)</span><br><span class="line">                        .Configure(app =&gt;</span><br><span class="line">                        &#123;</span><br><span class="line">                            app.UseRouting();</span><br><span class="line">                            app.UseEndpoints(endpoints =&gt;</span><br><span class="line">                            &#123;</span><br><span class="line">                                endpoints.MapGet(<span class="string">&quot;/&quot;</span>,</span><br><span class="line">                                    <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">                                        <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World!&quot;</span>);</span><br><span class="line">                                    &#125;);</span><br><span class="line">                            &#125;);</span><br><span class="line">                        &#125;);</span><br><span class="line">                &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>把 <code>ConfigureServices</code> 及 <code>Configure</code> 都改到 WebHost Builder 註冊，網站的執行結果會是一樣的。<br>若是透過 Generic Host 建立 Web Host，也可以在 HostBuilder 用 <code>ConfigureServices</code> 註冊 Services。  </p><h2 id="Application-Lifetime"><a href="#Application-Lifetime" class="headerlink" title="Application Lifetime"></a>Application Lifetime</h2><p>除了程式進入點外，WebHost 的啟停也是網站事件很重要一環，ASP.NET Core 不像 ASP.NET MVC 用繼承的方式捕捉啟動及停止事件。而是透過 <code>IHostApplicationLifetime</code> 來捕捉 WebHost 的停啟事件。  </p><p><code>IHostApplicationLifetime</code> 有三個註冊監聽事件及終止網站事件可以觸發。如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title">IHostApplicationLifetime</span></span><br><span class="line">&#123;</span><br><span class="line">    CancellationToken ApplicationStarted &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">    CancellationToken ApplicationStopping &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">    CancellationToken ApplicationStopped &#123; <span class="keyword">get</span>; &#125;</span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">StopApplication</span>()</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><strong>ApplicationStarted</strong><br> 當 WebHost 啟動完成後，會執行的<strong>啟動完成事件</strong>。  </li><li><strong>ApplicationStopping</strong><br> 當 WebHost 觸發停止時，會執行的<strong>準備停止事件</strong>。  </li><li><strong>ApplicationStopped</strong><br> 當 WebHost 停止事件完成時，會執行的<strong>停止完成事件</strong>。  </li><li><strong>StopApplication</strong><br> 可以透過此方法主動觸發<strong>終止網站</strong>。</li></ul><h3 id="範例程式"><a href="#範例程式" class="headerlink" title="範例程式"></a>範例程式</h3><p>透過 Console 輸出執行的過程，<em>Program.cs</em> 範例如下：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Hosting;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            Output(<span class="string">&quot;[Program] Start&quot;</span>);</span><br><span class="line"></span><br><span class="line">            Output(<span class="string">&quot;[Program] Create HostBuilder&quot;</span>);</span><br><span class="line">            <span class="keyword">var</span> hostBuilder = CreateHostBuilder(<span class="keyword">args</span>);</span><br><span class="line"></span><br><span class="line">            Output(<span class="string">&quot;[Program] Build Host&quot;</span>);</span><br><span class="line">            <span class="keyword">var</span> host = hostBuilder.Build();</span><br><span class="line"></span><br><span class="line">            Output(<span class="string">&quot;[Program] Run Host&quot;</span>);</span><br><span class="line">            host.Run();</span><br><span class="line"></span><br><span class="line">            Output(<span class="string">&quot;[Program] End&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> IHostBuilder <span class="title">CreateHostBuilder</span>(<span class="params"><span class="built_in">string</span>[] <span class="keyword">args</span></span>)</span> =&gt;</span><br><span class="line">            Host.CreateDefaultBuilder(<span class="keyword">args</span>)</span><br><span class="line">                .ConfigureServices(services =&gt; &#123;</span><br><span class="line">                    Output(<span class="string">&quot;[Program] hostBuilder.ConfigureServices - Called&quot;</span>);</span><br><span class="line">                &#125;)</span><br><span class="line">                .ConfigureWebHostDefaults(webBuilder =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    webBuilder</span><br><span class="line">                        .ConfigureServices(services =&gt; &#123;</span><br><span class="line">                            Output(<span class="string">&quot;[Program] webBuilder.ConfigureServices - Called&quot;</span>);</span><br><span class="line">                        &#125;)</span><br><span class="line">                        .Configure(app =&gt; &#123;</span><br><span class="line">                            Output(<span class="string">&quot;[Program] webBuilder.Configure - Called&quot;</span>);</span><br><span class="line">                        &#125;)</span><br><span class="line">                        .UseStartup&lt;Startup&gt;();</span><br><span class="line">                &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Output</span>(<span class="params"><span class="built_in">string</span> message</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            Console.WriteLine(<span class="string">$&quot;[<span class="subst">&#123;DateTime.Now:yyyy/MM/dd HH:mm:ss&#125;</span>] <span class="subst">&#123;message&#125;</span>&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em>Startup.cs</em>：  </p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System.Threading;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Builder;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Http;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.DependencyInjection;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Hosting;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Startup</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">Startup</span>()</span></span><br><span class="line">        &#123;</span><br><span class="line">            Program.Output(<span class="string">&quot;[Startup] Constructor - Called&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ConfigureServices</span>(<span class="params">IServiceCollection services</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            Program.Output(<span class="string">&quot;[Startup] ConfigureServices - Called&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">IApplicationBuilder app, IHostApplicationLifetime appLifetime</span>)</span></span><br><span class="line">        &#123;</span><br><span class="line">            appLifetime.ApplicationStarted.Register(() =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                Program.Output(<span class="string">&quot;[Startup] ApplicationLifetime - Started&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            appLifetime.ApplicationStopping.Register(() =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                Program.Output(<span class="string">&quot;[Startup] ApplicationLifetime - Stopping&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            appLifetime.ApplicationStopped.Register(() =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                Thread.Sleep(<span class="number">5</span> * <span class="number">1000</span>);</span><br><span class="line">                Program.Output(<span class="string">&quot;[Startup] ApplicationLifetime - Stopped&quot;</span>);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            app.UseRouting();</span><br><span class="line"></span><br><span class="line">            app.UseEndpoints(endpoints =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                endpoints.MapGet(<span class="string">&quot;/&quot;</span>, <span class="keyword">async</span> context =&gt; &#123; <span class="keyword">await</span> context.Response.WriteAsync(<span class="string">&quot;Hello World!&quot;</span>); &#125;);</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// For trigger stop WebHost</span></span><br><span class="line">            <span class="keyword">var</span> thread = <span class="keyword">new</span> Thread(() =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                Thread.Sleep(<span class="number">5</span> * <span class="number">1000</span>);</span><br><span class="line">                Program.Output(<span class="string">&quot;[Startup] Trigger stop WebHost&quot;</span>);</span><br><span class="line">                appLifetime.StopApplication();</span><br><span class="line">            &#125;);</span><br><span class="line">            thread.Start();</span><br><span class="line"></span><br><span class="line">            Program.Output(<span class="string">&quot;[Startup] Configure - Called&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="執行結果"><a href="#執行結果" class="headerlink" title="執行結果"></a>執行結果</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">[2019/10/24 01:24:59] [Program] Start</span><br><span class="line">[2019/10/24 01:24:59] [Program] Create HostBuilder</span><br><span class="line">[2019/10/24 01:24:59] [Program] Build Host</span><br><span class="line">[2019/10/24 01:24:59] [Program] hostBuilder.ConfigureServices - Called</span><br><span class="line">[2019/10/24 01:24:59] [Program] webBuilder.ConfigureServices - Called</span><br><span class="line">[2019/10/24 01:24:59] [Startup] Constructor - Called</span><br><span class="line">[2019/10/24 01:24:59] [Startup] ConfigureServices - Called</span><br><span class="line">[2019/10/24 01:25:00] [Program] Run Host</span><br><span class="line">[2019/10/24 01:25:00] [Startup] Configure - Called</span><br><span class="line">[2019/10/24 01:25:00] [Startup] ApplicationLifetime - Started</span><br><span class="line">[2019/10/24 01:25:05] [Startup] Trigger stop WebHost</span><br><span class="line">[2019/10/24 01:25:05] [Startup] ApplicationLifetime - Stopping</span><br><span class="line">[2019/10/24 01:25:10] [Startup] ApplicationLifetime - Stopped</span><br><span class="line">[2019/10/24 01:25:10] [Program] End</span><br></pre></td></tr></table></figure><blockquote><p>輸出內容少了 <strong>[Program] webBuilder.Configure - Called</strong>，因為 <code>Configure</code> 只能有一個，後註冊的 <code>Configure</code> 會把之前註冊的蓋掉。  </p></blockquote><p>執行流程如下：  </p><p><img src="/images/b/48.png" alt="ASP.NET Core 3 系列 - 程式生命週期 (Application Lifetime) - 範例程式執行流程"></p><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup">Application startup in ASP.NET Core</a>  </li><li><a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/hosting?tabs=aspnetcore2x">Hosting in ASP.NET Core</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;要了解程式的運作原理，要先知道程式的進入點及生命週期。&lt;br&gt;過往 ASP.NET MVC 啟動方式，是繼承 &lt;code&gt;HttpApplication&lt;/code&gt; 做為網站開始的進入點。&lt;br&gt;ASP.NET Core 改變了網站啟動的方式，是用 Console Application 的方式，Host Kestrel，提供 HTTP 的服務。&lt;br&gt;本篇將介紹 ASP.NET Core 3 的程式生命週期 (Application Lifetime) 及捕捉 Application 停啟事件。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 3 系列 - 從頭開始</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-3-starting.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-3-starting.html</id>
    <published>2019-10-23T15:12:00.000Z</published>
    <updated>2026-02-03T05:15:49.008Z</updated>
    
    <content type="html"><![CDATA[<p>自九月推出 .NET Core 3.0 正式版後，最近終於開始把產品從 .NET Core 2 開始升級到 .NET Core 3.0。<br>之前寫的 <a href="/tags/it-%E9%82%A6%E5%B9%AB%E5%BF%99-2018-%E9%90%B5%E4%BA%BA%E8%B3%BD/">ASP.NET Core 2 系列文章</a> 有部分內容已過時，所以將重新整理成 ASP.NET Core 3 的內容，並補充一些說明。<br>本篇主要介紹基本的 ASP.NET Core 3 環境準備及如何用 Visual Studio Code (VS Code) 開發 ASP.NET Core。  </p><span id="more"></span><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>開發 .NET Core 必需要安裝 .NET Core SDK，所以先到官網下載 .NET Core SDK 的安裝檔，官網下載位置<a href="https://dotnet.microsoft.com/download">點此</a>。  </p><p>.NET Core 是跨作業系統的框架，不再像 .NET Framework 要依附在 Windows 的作業系統才能執行，所以可以依照各平台版本進行下載及安裝。<br>文中範例用的截圖大部分是 Windows 作業系統，但本系列教學都會是以指令為主，並不受限於 Windows 平台。<br><em>(安裝軟體步驟太簡單，除了按<strong>下一步</strong>以外，幾乎沒什麼好解說的，所以不介紹怎麼安裝軟體。)</em>  </p><p>安裝完成後，可以透過 .NET Core CLI (Command-Line Interface) 確認 .NET Core SDK 安裝的版本，指令如下：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet --version</span><br></pre></td></tr></table></figure><h2 id="建立網站專案"><a href="#建立網站專案" class="headerlink" title="建立網站專案"></a>建立網站專案</h2><p>先建立一個專案資料夾 <code>MyWebsite</code>，然後在該資料夾執行 .NET Core CLI 建置網站的指令：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 建立專案資料夾</span></span><br><span class="line"><span class="built_in">mkdir</span> MyWebsite</span><br><span class="line"></span><br><span class="line"><span class="comment"># 進入專案資料夾</span></span><br><span class="line"><span class="built_in">cd</span> MyWebsite</span><br><span class="line"></span><br><span class="line"><span class="comment"># 建立 ASP.NET Core 專案</span></span><br><span class="line">dotnet new web</span><br></pre></td></tr></table></figure><p><img src="/images/b/45.png" alt="ASP.NET Core 3 系列 - 從頭開始 - 建立專案"></p><p>.NET Core CLI 會在該資料夾，建立一個空的 ASP.NET Core 專案，內容如下：  </p><p><img src="/images/b/46.png" alt="ASP.NET Core 3 系列 - 從頭開始 - 專案目錄"></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">obj/                            <span class="comment"># 專案暫存目錄</span></span><br><span class="line">Properties/                     <span class="comment"># 開發用的環境設定</span></span><br><span class="line">MyWebsite.csproj                <span class="comment"># 專案檔</span></span><br><span class="line">appsettings.Development.json    <span class="comment"># 執行程式組態設定(開發階段)</span></span><br><span class="line">appsettings.json                <span class="comment"># 執行程式組態設定(預設)</span></span><br><span class="line">Program.cs                      <span class="comment"># 程式進入檔</span></span><br><span class="line">Startup.cs                      <span class="comment"># 啟動網站設定</span></span><br></pre></td></tr></table></figure><blockquote><p>.NET Core 3.0 之後，<code>*.csproj</code> 專案檔變得很乾淨俐落，如上圖。<br>當宣告 <code>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;</code> 就會預設參考 <code>Microsoft.AspNetCore.App</code> 套件。  </p></blockquote><h2 id="啟動網站"><a href="#啟動網站" class="headerlink" title="啟動網站"></a>啟動網站</h2><p>建立完成後，就可以用 .NET Core CLI 啟動網站了。啟動網站指令：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet run</span><br></pre></td></tr></table></figure><p>.NET Core CLI 預設會起一個<code>http://localhost:5000/</code>的站台，用瀏覽器打開此連結就可以看到 ASP.NET Core 網站了。如下：  </p><p><img src="/images/b/47.png" alt="ASP.NET Core 3 系列 - 從頭開始 - 啟動網站">  </p><h2 id="Visual-Studio-Code"><a href="#Visual-Studio-Code" class="headerlink" title="Visual Studio Code"></a>Visual Studio Code</h2><p>.NET Core 都已經跨作業系統了，開發工具當然也就不再限制於 Visual Studio IDE (Visual Studio 2019&#x2F;2017 等)。基本上純文字編輯器搭配 .NET Core CLI 就可以開發 ASP.NET Core 了，但沒有中斷點除錯或 Autocomplete 開發有些辛苦。  </p><ul><li>Windows 作業系統，最推薦的當然還是 <a href="https://visualstudio.microsoft.com/">Visual Studio IDE</a>  </li><li>跨平台的話強烈推薦 JetBrains 公司出的 <a href="https://www.jetbrains.com/rider/">Rider</a>  </li><li>最輕量化跨平台推薦 <a href="https://code.visualstudio.com/">Visual Studio Code</a> (簡稱 VS Code)。</li></ul><p>VS Code 是一套可安裝擴充套件的文字編輯器，有支援 Windows、Mac 及 Linux 版本，極輕量又免費。<br>只要安裝擴充套件就變成了 IDE，並且支援多種不同的程式語言。下載位置<a href="https://code.visualstudio.com/Download">點此</a>。  </p><blockquote><p>範例選擇用 VS Code 最主要的原因是免費且跨平台。  </p></blockquote><h3 id="安裝擴充套件"><a href="#安裝擴充套件" class="headerlink" title="安裝擴充套件"></a>安裝擴充套件</h3><p>打開 VS Code 可以在左邊看到五個 Icon，點選最下面的那個 Extensions 圖示，並在 Extensions 搜尋列輸入 <strong>C#</strong> ，便可以找到 <code>C#</code> 的擴充套件安裝。如下圖：</p><p><img src="/images/ironman/i01-4.png" alt="ASP.NET Core 3 系列 - 從頭開始 - VS Code C# 擴充套件"></p><h3 id="開啟專案"><a href="#開啟專案" class="headerlink" title="開啟專案"></a>開啟專案</h3><p>VS Code 跟一般文字編輯器有些不同，它是以資料夾為工作區域，開啟一個目錄，就等同於是開啟一個專案。從上方工具列 <strong>File</strong> -&gt; <strong>Open Folder</strong> 選擇 ASP.NET Core 專案目錄，大概隔幾秒後，VS Code 會提示是否要幫此專案加入 Build&#x2F;Debug 的設定。如下圖：  </p><p><img src="/images/ironman/i01-5.png" alt="ASP.NET Core 3 系列 - 從頭開始 - VS Code 開啟專案"></p><h3 id="Build-Debug-設定"><a href="#Build-Debug-設定" class="headerlink" title="Build&#x2F;Debug 設定"></a>Build&#x2F;Debug 設定</h3><p>如果沒有自動提示加入 Build&#x2F;Debug 設定，可以在左邊 Icon，點選倒數第二個 Debug 圖示，手動加入 Build&#x2F;Debug 設定。如下步驟：  </p><p><img src="/images/ironman/i01-6.png" alt="ASP.NET Core 3 系列 - 從頭開始 - VS Code Build&#x2F;Debug 設定"><img src="/images/ironman/i01-7.png" alt="ASP.NET Core 3 系列 - 從頭開始 - VS Code Build&#x2F;Debug 設定"></p><p>設定完成後，VS Code 會自動建立 <em>.vscode</em> 目錄及設定檔 <em>launch.json</em>、<em>tasks.json</em>。<br>目錄結構如下：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">.vscode/                        <span class="comment"># VS Code 設定檔目錄</span></span><br><span class="line">  launch.json                   <span class="comment"># 用 VS Code 啟動程式的設定檔</span></span><br><span class="line">  tasks.json                    <span class="comment"># 定義 launch.json 會用道的指令設定檔</span></span><br><span class="line">obj/                            <span class="comment"># 專案暫存目錄</span></span><br><span class="line">Properties/                     <span class="comment"># 開發用的環境設定</span></span><br><span class="line">MyWebsite.csproj                <span class="comment"># 專案檔</span></span><br><span class="line">appsettings.Development.json    <span class="comment"># 執行程式組態設定(開發階段)</span></span><br><span class="line">appsettings.json                <span class="comment"># 執行程式組態設定(預設)</span></span><br><span class="line">Program.cs                      <span class="comment"># 程式進入檔</span></span><br><span class="line">Startup.cs                      <span class="comment"># 啟動網站設定</span></span><br></pre></td></tr></table></figure><p>如果 VS Code 自動建立失敗，那就手動新增 <em>launch.json</em> 及 <em>tasks.json</em> 吧…<br>內容如下：  </p><p><em>launch.json</em>:  </p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.2.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;configurations&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;.NET Core Launch&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;coreclr&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;request&quot;</span><span class="punctuation">:</span> <span class="string">&quot;launch&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;preLaunchTask&quot;</span><span class="punctuation">:</span> <span class="string">&quot;build&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;program&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;workspaceFolder&#125;/bin/Debug/netcoreapp3.0/MyWebsite.dll&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;cwd&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;workspaceFolder&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;console&quot;</span><span class="punctuation">:</span> <span class="string">&quot;internalConsole&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;stopAtEntry&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><em>tasks.json</em>:  </p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;tasks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;build&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dotnet&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;process&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="string">&quot;build&quot;</span></span><br><span class="line">      <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;problemMatcher&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$msCompile&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="中斷點除錯"><a href="#中斷點除錯" class="headerlink" title="中斷點除錯"></a>中斷點除錯</h3><p>在程式碼行號左邊點擊滑鼠就可以下中斷點了，跟一般 IDE 差不多。然後在 Debug 側欄啟動偵錯：  </p><p><img src="/images/ironman/i01-8.png" alt="ASP.NET Core 3 系列 - 從頭開始 - VS Code 中斷點除錯"></p><p>當執行到該中斷點後，就會停下來，並在 Debug 側欄顯示當前變數狀態等，也可以用滑鼠移到變數上面檢視該變數的內容。如下：</p><p><img src="/images/ironman/i01-9.png" alt="ASP.NET Core 3 系列 - 從頭開始 - VS Code 中斷點除錯"></p><p>偵錯方式跟大部分的 IDE 都差不多，可以 Step over、Step in&#x2F;out 等。<br>如此一來就可以用 VS Code 輕鬆開發 ASP.NET Core。  </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;自九月推出 .NET Core 3.0 正式版後，最近終於開始把產品從 .NET Core 2 開始升級到 .NET Core 3.0。&lt;br&gt;之前寫的 &lt;a href=&quot;/tags/it-%E9%82%A6%E5%B9%AB%E5%BF%99-2018-%E9%90%B5%E4%BA%BA%E8%B3%BD/&quot;&gt;ASP.NET Core 2 系列文章&lt;/a&gt; 有部分內容已過時，所以將重新整理成 ASP.NET Core 3 的內容，並補充一些說明。&lt;br&gt;本篇主要介紹基本的 ASP.NET Core 3 環境準備及如何用 Visual Studio Code (VS Code) 開發 ASP.NET Core。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="VS Code" scheme="https://blog.johnwu.cc/tags/vs-code/"/>
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="ASP.NET Core 3" scheme="https://blog.johnwu.cc/tags/asp-net-core-3/"/>
    
  </entry>
  
  <entry>
    <title>Docker 教學 - .NET Core 程式碼分析報告 (SonarQube)</title>
    <link href="https://blog.johnwu.cc/article/docker-dotnet-sonarqube.html"/>
    <id>https://blog.johnwu.cc/article/docker-dotnet-sonarqube.html</id>
    <published>2019-09-04T15:02:00.000Z</published>
    <updated>2026-02-03T05:13:46.038Z</updated>
    
    <content type="html"><![CDATA[<p>SonarQube 是常見的程式碼分析工具，本篇介紹如何透過 Docker 進行 .NET Core 程式碼分析；<br>並搭配 Coverlet 產生程式碼測試覆蓋率分析，一併傳送至 SonarQube。  </p><span id="more"></span><h2 id="SonarQube"><a href="#SonarQube" class="headerlink" title="SonarQube"></a>SonarQube</h2><p>如果沒有 SonarQube 環境的話，可透過以下 docker 指令，快速啟動 SonarQube：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name SonarQube -p 9000:9000 sonarqube</span><br></pre></td></tr></table></figure><p>啟動後，用瀏覽器打開 <a href="http://localhost:9000/">http://localhost:9000/</a> 就能看到 SonarQube 站台。<br>預設帳號&#x2F;密碼為：<code>admin</code> &#x2F; <code>admin</code>  </p><p>登入後，可新增 SonarQube 專案，步驟如下：  </p><p><img src="/images/b/41.png" alt="Docker 教學 - .NET Core 程式碼分析報告 (SonarQube) - 新增 SonarQube 專案 1">  </p><p><img src="/images/b/42.png" alt="Docker 教學 - .NET Core 程式碼分析報告 (SonarQube) - 新增 SonarQube 專案 2">  </p><blockquote><p>分析報告要上傳到 SonarQube Server 時，需要用到 <code>Project Key</code> 及步驟 8 的 <code>Token</code>。  </p></blockquote><h2 id="Dockerfile"><a href="#Dockerfile" class="headerlink" title="Dockerfile"></a>Dockerfile</h2><p><code>build-unit-test.dockerfile</code></p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/sdk:<span class="number">2.2</span></span><br><span class="line"><span class="comment"># 安裝 Coverlet 及 SonarScanner</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet tool install --global coverlet.console &amp;&amp; \</span></span><br><span class="line"><span class="language-bash">    dotnet tool install --global dotnet-sonarscanner</span></span><br><span class="line"><span class="keyword">ENV</span> PATH=$PATH:/root/.dotnet/tools/</span><br><span class="line"><span class="comment"># 安裝 JRE，SonarScanner 是基於 Java 撰寫，所以需要 JRE</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update &amp;&amp; \</span></span><br><span class="line"><span class="language-bash">    apt-get install -y openjdk-8-jdk</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . .</span></span><br><span class="line"><span class="comment"># 啟動 SonarScanner</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet sonarscanner begin /k:<span class="string">&quot;&lt;sonarqube_project_key&gt;&quot;</span> \</span></span><br><span class="line"><span class="language-bash">    /d:sonar.host.url=http://192.168.1.11:9000 \</span></span><br><span class="line"><span class="language-bash">    /d:sonar.login=&lt;sonarqube_project_token&gt; \</span></span><br><span class="line"><span class="language-bash">    /d:sonar.exclusions=**/*.js,**/*.ts,**/*.css,bin/**/*,obj/**/*,wwwroot/**/*,ClientApp/**/* \</span></span><br><span class="line"><span class="language-bash">    /d:sonar.cs.opencover.reportsPaths=/coverage/coverage.opencover.xml \</span></span><br><span class="line"><span class="language-bash">    /d:sonar.coverage.exclusions=**/*Model.cs,MyProject.Test/**/*</span></span><br><span class="line"><span class="comment"># 執行 dotnet test</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet <span class="built_in">test</span> \</span></span><br><span class="line"><span class="language-bash">    /p:CollectCoverage=<span class="literal">true</span> \</span></span><br><span class="line"><span class="language-bash">    /p:CoverletOutputFormat=opencover \</span></span><br><span class="line"><span class="language-bash">    /p:CoverletOutput=/coverage/ \</span></span><br><span class="line"><span class="language-bash">    UnitTest.sln</span></span><br><span class="line"><span class="comment"># 結束 SonarScanner</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet sonarscanner end /d:sonar.login=&lt;sonarqube_project_token&gt;</span></span><br></pre></td></tr></table></figure><p>Docker Image 建置指令：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -f build-unit-test.dockerfile .</span><br></pre></td></tr></table></figure><blockquote><p>前一篇文章 <a href="/article/docker-dotnet-coverage-report-generator.html">Docker 教學 - .NET Core 測試報告 (Coverlet + ReportGenerator)</a> 有基本介紹一下 Coverlet，本篇就不再贅述。  </p></blockquote><ul><li>啟動 SonarScanner  <ul><li><strong>&#x2F;k</strong><br>SonarQube 的 <code>Project Key</code> 是必填欄位。  </li><li><strong>&#x2F;d:sonar.host.url</strong><br>是 SonarQube Server 的位置。  </li><li><strong>&#x2F;d:sonar.login</strong><br>SonarQube 的 <code>Token</code>，錯誤的話無法成功上傳到 SonarQube Server。  </li><li><strong>&#x2F;d:sonar.exclusions</strong><br>忽略程式碼分析的檔案或目錄。可用 <code>,</code> 隔開指定多個 Patterns。<br>此例忽略 <code>*.js</code>、<code>*.ts</code> 及 <code>*.css</code> 的檔案，如果要分析 <code>*.js</code>、<code>*.ts</code> 或 <code>*.css</code>，還需要在 <code>Dockerfile</code> 安裝 <code>node.js</code>。  </li><li><strong>&#x2F;d:sonar.cs.opencover.reportsPaths</strong><br>指定匯入 OpenCover 格式的測試報告路徑。可用 <code>,</code> 隔開匯入多個檔案。  </li><li><strong>&#x2F;d:sonar.coverage.exclusions</strong><br>忽略測試覆蓋率分析報告的檔案或目錄。可用 <code>,</code> 隔開指定多個 Patterns。</li></ul></li><li>結束 SonarScanner  <ul><li><strong>&#x2F;d:sonar.login</strong><br>SonarQube 的 <code>Token</code>，錯誤的話無法成功上傳到 SonarQube Server。</li></ul></li></ul><h2 id="執行結果"><a href="#執行結果" class="headerlink" title="執行結果"></a>執行結果</h2><p><img src="/images/b/43.png" alt="Docker 教學 - .NET Core 程式碼分析報告 (SonarQube) - 執行結果 1">  </p><p><img src="/images/b/44.png" alt="Docker 教學 - .NET Core 程式碼分析報告 (SonarQube) - 執行結果 2">  </p><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://github.com/tonerdo/coverlet/">Coverlet</a>  </li><li><a href="https://docs.sonarqube.org/latest/analysis/scan/sonarscanner-for-msbuild/">SonarQube - ReportGenSonarScanner for MSBuilderator</a>  </li><li><a href="https://docs.sonarqube.org/latest/project-administration/narrowing-the-focus/">SonarQube - Narrowing the Focus</a>  </li><li><a href="https://sonarcloud.io/documentation/analysis/coverage/">SonarCloud - Test Coverage &amp; Execution</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;SonarQube 是常見的程式碼分析工具，本篇介紹如何透過 Docker 進行 .NET Core 程式碼分析；&lt;br&gt;並搭配 Coverlet 產生程式碼測試覆蓋率分析，一併傳送至 SonarQube。  &lt;/p&gt;</summary>
    
    
    
    <category term="Docker" scheme="https://blog.johnwu.cc/categories/docker/"/>
    
    
    <category term="Docker" scheme="https://blog.johnwu.cc/tags/docker/"/>
    
    <category term=".NET Core" scheme="https://blog.johnwu.cc/tags/net-core/"/>
    
    <category term="SonarQube" scheme="https://blog.johnwu.cc/tags/sonarqube/"/>
    
  </entry>
  
  <entry>
    <title>Docker 教學 - .NET Core 測試報告 (Coverlet + ReportGenerator)</title>
    <link href="https://blog.johnwu.cc/article/docker-dotnet-coverage-report-generator.html"/>
    <id>https://blog.johnwu.cc/article/docker-dotnet-coverage-report-generator.html</id>
    <published>2019-08-28T16:59:00.000Z</published>
    <updated>2026-02-03T05:15:10.059Z</updated>
    
    <content type="html"><![CDATA[<p>本篇將介紹如何透過 Coverlet + ReportGenerator 產生 .NET Core 的測試報告；<br>並用 Dockerfile 產生測試報告的 Docker Image，以網頁的形式查看程式碼覆蓋率。  </p><span id="more"></span><h2 id="Coverlet"><a href="#Coverlet" class="headerlink" title="Coverlet"></a>Coverlet</h2><p>dotnet cli 提供的 <code>dotnet test</code> 指令，並沒有支援測試覆蓋率，可透過第三方套件分析程式碼覆蓋率。<br>Coverlet 是一套支援 .NET Core 且跨平台的程式碼覆蓋率分析工具。  </p><p>可透過 dotnet cli 的 <code>dotnet tool</code> 安裝，指令如下：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet tool install --global coverlet.console</span><br></pre></td></tr></table></figure><p>除了安裝 <code>dotnet tool</code> 外，測試專案也需要安裝 NuGet 套件 <code>coverlet.msbuild</code>，安裝指令：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet add package coverlet.msbuild</span><br></pre></td></tr></table></figure><p>安裝完成後，就可透過 <code>dotnet test</code> 指令，附帶參數執行程式碼覆蓋率分析：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">dotnet <span class="built_in">test</span> /p:CollectCoverage=<span class="literal">true</span> \</span><br><span class="line">  /p:CoverletOutputFormat=opencover \</span><br><span class="line">  /p:CoverletOutput=./coverage/ \</span><br><span class="line">  UnitTest.sln</span><br></pre></td></tr></table></figure><ul><li><strong>CollectCoverage</strong><br>使否啟用程式碼覆蓋率分析。<br><em>(預設為 false)</em>  </li><li><strong>CoverletOutputFormat</strong><br>程式碼覆蓋率分析報告的格式。<br>支援的格式有：<code>json</code>、<code>lcov</code>、<code>opencover</code>、<code>cobertura</code>、<code>teamcity</code>。<br><em>(預設為 JSON 格式)</em>  </li><li><strong>CoverletOutput</strong><br>分析報告的輸出路徑。<br>上例會在測試專案目錄下的 <code>coverage</code> 資料夾產生 <code>coverage.opencover.xml</code> 檔案。<br><em>(預設為測試專案目錄)</em></li></ul><p><code>dotnet test</code> 程式碼覆蓋率分析完成會輸出如下畫面：  </p><p><img src="/images/b/38.png" alt="Docker 教學 - .NET Core 測試報告 - Coverlet 程式碼覆蓋率分析">  </p><h2 id="ReportGenerator"><a href="#ReportGenerator" class="headerlink" title="ReportGenerator"></a>ReportGenerator</h2><p>上面步驟透過 Coverlet 產生的 <code>coverage.opencover.xml</code> 檔案並不適合閱讀，所以透過 ReportGenerator 這套工具，把 opencover 格式的測試報告轉換成 HTML，再透過瀏覽器打開，呈現圖形化介面報告。  </p><p>ReportGenerator 支援多種測試報告格式轉換，官方資料：  </p><table><thead><tr><th align="left"><strong>輸入格式</strong></th><th align="left"><strong>輸出格式</strong></th></tr></thead><tbody><tr><td align="left"><ul><li><a href="https://github.com/OpenCover/opencover">OpenCover</a> (<a href="https://www.nuget.org/packages/OpenCover">NuGet</a>)<br/>OpenCover format is also generated by <a href="https://github.com/tonerdo/coverlet/">coverlet</a> and <a href="https://github.com/SteveGilham/altcover">altcover</a></li><li><a href="https://www.jetbrains.com/dotcover/help/dotCover__Console_Runner_Commands.html">dotCover</a> (<a href="https://www.nuget.org/packages/JetBrains.dotCover.CommandLineTools/">NuGet</a>, &#x2F;ReportType&#x3D;DetailedXML)</li><li>Visual Studio (<a href="https://github.com/danielpalme/ReportGenerator/wiki/Visual-Studio-Coverage-Tools#vstestconsoleexe">vstest.console.exe</a>, <a href="https://github.com/danielpalme/ReportGenerator/wiki/Visual-Studio-Coverage-Tools#codecoverageexe">CodeCoverage.exe</a>)</li><li><a href="https://www.ncover.com/info/download">NCover</a> (tested version 1.5.8, other versions may not work)</li><li><a href="https://github.com/cobertura/cobertura">Cobertura</a></li><li><a href="https://www.jacoco.org/jacoco/index.html">JaCoCo</a></li><li><a href="https://openclover.org/">Clover</a></li><li>Mono (<a href="https://www.mono-project.com/docs/debug+profile/profile/profiler/#analyzing-the-profile-data">mprof-report</a>)</li><li><a href="https://gcc.gnu.org/onlinedocs/gcc/Gcov.html">gcov</a></li><li><a href="https://github.com/linux-test-project/lcov">lcov</a></li></ul></td><td align="left"><ul><li>HTML, HTMLSummary, HTMLInline, HtmlInline_AzurePipelines, HtmlInline_AzurePipelines_Dark, HTMLChart, <a href="https://en.wikipedia.org/wiki/MHTML">MHTML</a></li><li>Cobertura</li><li><a href="https://docs.sonarqube.org/latest/analysis/generic-test">SonarQube</a></li><li>XML, XMLSummary</li><li>Latex, LatexSummary</li><li>TextSummary</li><li>CsvSummary</li><li>PngChart</li><li>Badges</li><li><a href="https://github.com/danielpalme/ReportGenerator/wiki/Custom-reports">Custom reports</a></li></ul><br /><br /></td></tr></tbody></table><p>安裝方式一樣可透過 dotnet cli 的 <code>dotnet tool</code> 安裝，指令如下：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet tool install --global dotnet-reportgenerator-globaltool</span><br></pre></td></tr></table></figure><p>安裝完成後，就可透過 <code>reportgenerator</code> 指令，將測試報告格式轉換：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">reportgenerator \</span><br><span class="line">  -reports:./coverage/coverage.opencover.xml \</span><br><span class="line">  -reporttypes:Html \</span><br><span class="line">  -targetdir:./coverage</span><br></pre></td></tr></table></figure><ul><li><strong>reports</strong><br>要被轉換的測試報告來源檔案路徑。  </li><li><strong>reporttypes</strong><br>轉換輸出的格式。<br><em>(預設為 Html)</em>  </li><li><strong>targetdir</strong><br>轉換輸出的位置。</li></ul><p>轉換完成就會生成一大堆的 HTML 檔案。如下：  </p><p><img src="/images/b/39.png" alt="Docker 教學 - .NET Core 測試報告 - ReportGenerator 檔案">  </p><blockquote><p>用瀏覽器開啟 <code>index.htm</code> 就可以看到圖形化的測試報告進入點。  </p></blockquote><h2 id="Dockerfile"><a href="#Dockerfile" class="headerlink" title="Dockerfile"></a>Dockerfile</h2><p><code>build-unit-test.dockerfile</code></p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/sdk:<span class="number">2.2</span> AS dotnet-test-<span class="keyword">env</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet tool install --global coverlet.console &amp;&amp; \</span></span><br><span class="line"><span class="language-bash">    dotnet tool install --global dotnet-reportgenerator-globaltool</span></span><br><span class="line"><span class="keyword">ENV</span> PATH=$PATH:/root/.dotnet/tools/</span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet <span class="built_in">test</span> \</span></span><br><span class="line"><span class="language-bash">    /p:CollectCoverage=<span class="literal">true</span> \</span></span><br><span class="line"><span class="language-bash">    /p:CoverletOutputFormat=opencover \</span></span><br><span class="line"><span class="language-bash">    /p:CoverletOutput=/coverage/ \</span></span><br><span class="line"><span class="language-bash">    UnitTest.sln</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> reportgenerator -reports:/coverage/coverage.opencover.xml -targetdir:/coverage</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> nginx:alpine</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">rm</span> /usr/share/nginx/html/index.html</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=dotnet-test-env /coverage /usr/share/nginx/html</span></span><br></pre></td></tr></table></figure><p>Docker Image 建置指令：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -f build-unit-test.dockerfile -t my-project-coverage .</span><br></pre></td></tr></table></figure><p>以上 <code>Dockerfile</code> 共分為兩個階段：</p><ul><li>第一階段產生名稱為 dotnet-test-env 的暫存 Container  <ol><li>安裝 <code>Coverlet</code> 及 <code>ReportGenerator</code>  </li><li>把程式碼複製到 Container  </li><li>執行程式碼覆蓋率分析  </li><li>將測試報告轉換為 HTML 格式</li></ol></li><li>第二階段把 dotnet-test-env 的 &#x2F;coverage 目錄內檔案，複製到 nginx 預設目錄內，打包成最終階段的 Docker Image。</li></ul><p>建置完成後，就可以用 <code>docker run</code> 把測試報告的 Docker Image 執行起來了。<br>指令：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -p 8080:80 my-project-coverage</span><br></pre></td></tr></table></figure><p>用瀏覽器開啟 <a href="http://localhost:8080/">http://localhost:8080/</a> 就可以看到圖形化的測試報告了。  </p><p><img src="/images/b/40.png" alt="Docker 教學 - .NET Core 測試報告 - ReportGenerator 圖形化的測試報告">  </p><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://github.com/tonerdo/coverlet/">Coverlet</a>  </li><li><a href="https://github.com/danielpalme/ReportGenerator">ReportGenerator</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本篇將介紹如何透過 Coverlet + ReportGenerator 產生 .NET Core 的測試報告；&lt;br&gt;並用 Dockerfile 產生測試報告的 Docker Image，以網頁的形式查看程式碼覆蓋率。  &lt;/p&gt;</summary>
    
    
    
    <category term="Docker" scheme="https://blog.johnwu.cc/categories/docker/"/>
    
    
    <category term="Docker" scheme="https://blog.johnwu.cc/tags/docker/"/>
    
    <category term=".NET Core" scheme="https://blog.johnwu.cc/tags/net-core/"/>
    
  </entry>
  
  <entry>
    <title>ELK 筆記 - 硬體規格評估</title>
    <link href="https://blog.johnwu.cc/article/elk-hardware-specification.html"/>
    <id>https://blog.johnwu.cc/article/elk-hardware-specification.html</id>
    <published>2019-08-15T17:20:00.000Z</published>
    <updated>2026-02-03T04:09:36.648Z</updated>
    
    <content type="html"><![CDATA[<p>已經習慣把 Log 存到 Elasticsearch(以下簡稱 ES) 再透過 Kibana 查看日誌，所以每當有新產品要上線前，都會評估 ELK 需要的硬體規格。<br>依照產品大小不同，儲存 Log 的資料筆數跟空間，都有很大的差異，會直接影響到 CPU、記憶體、硬碟空間等。  </p><p>近期產品是上到 GCP 跟阿里雲，本篇硬體規格會以雲端服務的 Server 規格做為參考的基準。  </p><span id="more"></span><h2 id="情境描述"><a href="#情境描述" class="headerlink" title="情境描述"></a>情境描述</h2><p>本範例產品的參考資料：  </p><ul><li>在線數<strong>同時</strong>約：6000  </li><li>交易量<strong>每秒</strong>約：20筆  </li><li>Log <strong>每週</strong>約：320萬筆 (25~30 GB)</li></ul><p>算是一個不大的小系統，用 Kibana 查詢近期一週的 ES Index 用量。如圖：  </p><p><img src="/images/b/34.png" alt="ELK 筆記 - 硬體規格評估 - Elasticsearch Index 用量">  </p><h2 id="使用資源"><a href="#使用資源" class="headerlink" title="使用資源"></a>使用資源</h2><p>由於經費有限，所以只有使用兩台 ELK 做 HA，用 Ｍaster&#x2F;Slave 架構，沒有做到 Cluster。<br>兩台 Server 分別裝載著 ELK 三個服務，架構如下：  </p><p><img src="/images/b/36.png" alt="ELK 筆記 - 硬體規格評估 - ELK Ｍaster&#x2F;Slave 架構">  </p><p>此範例幾個月前從 GCP 轉移到阿里雲，在這兩個平台使用的 VM 等級如下：  </p><ul><li>GCP (n1-highmem-8)<ul><li>vCPU: 8 core</li><li>RAM: 52 GB</li><li>Disk: 20 GB + 300 GB</li></ul></li><li>阿里雲 (ecs.r5.2xlarge)<ul><li>vCPU: 8 core</li><li>RAM: 64 GB</li><li>Disk: 20 GB + 300 GB</li></ul></li></ul><blockquote><p>Ｍaster&#x2F;Slave 的機器規格都是一樣的。  </p></blockquote><p>產品運行超過半年，CPU 大約都落在 30% 左右，依照上述使用量，在阿里雲其中一台 ELK Server，近期一週的監控資訊：  </p><p><img src="/images/b/35.png" alt="ELK 筆記 - 硬體規格評估 - 阿里雲監控資訊">  </p><blockquote><p>找不到當時在 GCP 用量的截圖。  </p></blockquote><h2 id="硬體規格評估"><a href="#硬體規格評估" class="headerlink" title="硬體規格評估"></a>硬體規格評估</h2><p>首先要評估預計要放到 ELK 的 Log 量，資料筆數及資料大小。<br>再來就是 Log Parsing 的規則，如果 Parsing 很複雜 CPU 就會佔用較高的資源。  </p><p>ELK 三項服務，分別佔用資源情況：  </p><table><thead><tr><th align="left">服務</th><th align="center">CPU</th><th align="center">RAM</th><th align="center">Disk</th></tr></thead><tbody><tr><td align="left">Elasticsearch</td><td align="center">中高</td><td align="center">高</td><td align="center">極高</td></tr><tr><td align="left">Logstash</td><td align="center">中</td><td align="center">低</td><td align="center">低</td></tr><tr><td align="left">Kibana</td><td align="center">低</td><td align="center">低</td><td align="center">低</td></tr></tbody></table><blockquote><p>基本上可以完全不用考慮 Kibana 消耗資源。<br>主要高耗能的就是 <strong>Elasticsearch</strong> 跟 <strong>Logstash</strong>。  </p></blockquote><h3 id="CPU"><a href="#CPU" class="headerlink" title="CPU"></a>CPU</h3><p>CPU 是比較難評估部分，因為 Log Parsing 的複雜度以及查詢 ES 的條件，都會強烈影響 CPU 的使用量。  </p><ul><li><p>ES 查詢所消耗的 CPU，阿里雲提供參考：  </p><blockquote><p>每個 vCPU core 大約可處理 20~40 GB 查詢資料。<br>(依據本例使用情境，CPU 消耗偏高一些，但也沒落差太多。)  </p></blockquote></li><li><p>Logstash 依照上述的情境，Log 每秒也才 500 筆左右，分配 vCPU * 1，其實綽綽有餘了。  </p><blockquote><p>建議每處理 1500 筆資料，就分一個 vCPU core。</p></blockquote></li></ul><h3 id="RAM"><a href="#RAM" class="headerlink" title="RAM"></a>RAM</h3><ul><li><p>ES 使用記憶體有兩個條件限制：  </p><ol><li>最高只能設定為系統的 <strong>50%</strong>。例：系統 8 GB，ES 只能設定 4GB。  </li><li>不能超過 <strong>32 GB</strong>。</li></ol><p>如果條件允許，就直上 64 GB 記憶體，然後把一半分給 ES。<br>ES 查詢很吃記憶體，尤其是大區間的查詢，根據阿里雲提供的參考：  </p><blockquote><p>每 1 GB RAM，大約可處理 10 GB 查詢資料。  </p></blockquote></li><li><p>Logstash 的記憶體用於緩存消化不完的資料，CPU 不夠力的情況下就會需要比較高的記憶體。<br>不過還是要依照實際使用量調整，在預估資源分配上，不用考慮太高的比重。  </p><blockquote><p>基本上 1 GB 以內都夠用，甚至 128 MB 都夠用。</p></blockquote></li></ul><h3 id="Disk"><a href="#Disk" class="headerlink" title="Disk"></a>Disk</h3><p>ELK 實際存資料的是 ES，所以評估時就不考慮 Logstash 跟 Kibana。<br>系統碟分配 20 GB 基本上就很足夠了，甚至可以更低。<br>這邊只單純討論 ES 存資料的空間計算。  </p><p>估計 ES 硬碟空間要注意的項目：  </p><ol><li><strong>主要資料</strong><br>  ES 收到的內容及索引。  <blockquote><p>注意！儲存的資料大小，<em>不等於</em>原始資料的大小。<br>如果對資料加工不熟悉的話，建議在估計時 <code>* 1.5 倍</code> 當作<strong>彈性倍率</strong>。</p></blockquote></li></ol><ul><li>原始資料若透過 Logstash 加工，產生了很多欄位，且保留原始內容，實際存放的大小就會比原始資料大很多，可能會多到一倍。  </li><li>若加工的欄位建的好，且加工完就拋棄原始內容，實際存放就會比原始資料更小。如圖：<br><img src="/images/b/37.png" alt="ELK 筆記 - 硬體規格評估 - Logstash 拋棄原始內容"></li></ul><ol start="2"><li><strong>副本資料</strong><br>  主要資料的備援檔，大小與主要資料相等。<br>  若有兩個以上的節點，預設會建立一份副本，可變更副本數量。<br>  如果設定一份以上的副本，就會占用更多空間。  </li><li><strong>保留空間</strong><br>  預設儲存資料只會使用硬碟空間的 85%，超過 85% 就不會在寫入資料到 ES。</li></ol><p>預估硬碟空間公式如下：  </p><blockquote><p>主要資料 &#x3D; 原始資料 * 1.5(彈性倍率)<br>節點硬碟需求 &#x3D; 主要資料 * 1.15(保留空間) * (1 + 副本數量) &#x2F; ES節點數量</p></blockquote><p>假設預估每週產生 10 GB 的 Log，希望可以留 4 週，每個 ES 節點所需的硬碟空間：</p><ul><li>一台 ES  <blockquote><p>10 GB * 4 * 1.5 * 1.15 * (1 + 0) &#x2F; 1 ≒ 69 GB</p></blockquote></li><li>兩台 ES 做 Master&#x2F;Slave 架構  <blockquote><p>10 GB * 4 * 1.5 * 1.15 * (1 + 1) &#x2F; 2 ≒ 70 GB</p></blockquote></li><li>三台 ES 做 Cluster 架構<blockquote><p>10 GB * 4 * 1.5 * 1.15 * (1 + 1) &#x2F; 3 ≒ 46 GB</p></blockquote></li><li>五台 ES 做 Cluster 架構，建立三分副本<blockquote><p>10 GB * 4 * 1.5 * 1.15 * (1 + 3) &#x2F; 5 ≒ 55 GB</p></blockquote></li></ul><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><p><a href="https://github.com/AlibabaCloudDocs/elasticsearch/blob/master/cn.zh-CN/%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/%E8%A7%84%E6%A0%BC%E5%AE%B9%E9%87%8F%E8%AF%84%E4%BC%B0.md">AlibabaCloudDocs - Elasticsearch 规格容量评估</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;已經習慣把 Log 存到 Elasticsearch(以下簡稱 ES) 再透過 Kibana 查看日誌，所以每當有新產品要上線前，都會評估 ELK 需要的硬體規格。&lt;br&gt;依照產品大小不同，儲存 Log 的資料筆數跟空間，都有很大的差異，會直接影響到 CPU、記憶體、硬碟空間等。  &lt;/p&gt;
&lt;p&gt;近期產品是上到 GCP 跟阿里雲，本篇硬體規格會以雲端服務的 Server 規格做為參考的基準。  &lt;/p&gt;</summary>
    
    
    
    <category term="ELK" scheme="https://blog.johnwu.cc/categories/elk/"/>
    
    
    <category term="ELK" scheme="https://blog.johnwu.cc/tags/elk/"/>
    
    <category term="Elasticsearch" scheme="https://blog.johnwu.cc/tags/elasticsearch/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 2 筆記 - HTTPS ERR_CONNECTION_CLOSED</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-https-err-connection-closed.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-https-err-connection-closed.html</id>
    <published>2019-08-12T04:13:00.000Z</published>
    <updated>2026-02-03T04:09:36.646Z</updated>
    
    <content type="html"><![CDATA[<p>剛剛遇到近期開發的 ASP.NET Core 站台，在本機 MacOS 環境啟動後，瀏覽器用 HTTPS 打開會顯示以下錯誤：  </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">無法連上這個網站  </span><br><span class="line">localhost 拒絕連線。</span><br><span class="line"></span><br><span class="line">ERR_CONNECTION_CLOSED</span><br></pre></td></tr></table></figure><span id="more"></span><p>如圖：<img src="/images/b/33.png" alt="ASP.NET Core 2 筆記 - HTTPS ERR_CONNECTION_CLOSED - 錯誤訊息"></p><h2 id="解決方式"><a href="#解決方式" class="headerlink" title="解決方式"></a>解決方式</h2><p>瀏覽器因為安全性問題，不讓 ASP.NET Core 站台使用 HTTPS，可以透過以下指令把開發憑證清除，再重建：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> dotnet dev-certs https --clean</span><br><span class="line">dotnet dev-certs https</span><br></pre></td></tr></table></figure><p>回想一下原因，應該是上週將 MacOS 系統更新後，同時更新一些軟體等造成。(好像有更新 Chrome)  </p><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><p><a href="https://github.com/dotnet/corefx/issues/31749#issuecomment-423694193">HTTPS on macOS does not work running from the default ASP.NET Core Web App (MVC) template</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;剛剛遇到近期開發的 ASP.NET Core 站台，在本機 MacOS 環境啟動後，瀏覽器用 HTTPS 打開會顯示以下錯誤：  &lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;無法連上這個網站  &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;localhost 拒絕連線。&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;ERR_CONNECTION_CLOSED&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
  </entry>
  
  <entry>
    <title>Docker 教學 - Docker Image 混合公有及私有 Docker Registry</title>
    <link href="https://blog.johnwu.cc/article/docker-image-push-to-public-private-registry.html"/>
    <id>https://blog.johnwu.cc/article/docker-image-push-to-public-private-registry.html</id>
    <published>2019-08-05T15:02:00.000Z</published>
    <updated>2026-02-03T04:09:36.648Z</updated>
    
    <content type="html"><![CDATA[<p>本篇介紹如何依照不同的資料類型分層建置 Docker Image；<br>並把開放性資料及敏感資料的分層，分別推上公有和私有 Docker Registry。  </p><span id="more"></span><h2 id="情境描述"><a href="#情境描述" class="headerlink" title="情境描述"></a>情境描述</h2><p>最近在一個特殊的網路環境下，因流量限制及安全性考量，產生了這個特別的需求，網路架構如下圖：  </p><p><img src="/images/b/31.png" alt="Docker 教學 - Docker Image 混合公有及私有 Docker Registry - 情境描述網路架構圖">  </p><p>開發環境跟生產環境都可以連到外網，但兩邊的網路互不相通，只能透過 Cleanroom 存取兩邊的網路環境。<br>但上圖紅線部分，Cleanroom 連到生產環境有以下問題：  </p><ol><li>網路速度慢  </li><li>連線品質不佳  </li><li>網路費昂貴依流量計價  </li><li>不能把原始碼放到生產環境</li></ol><p>若直接將 Docker Images 從 Cleanroom 推上生產環境，每次佈署都會產生驚人的費用。<br>生產環境雖然可以連外網，但因安全性考量<strong>不能在外網建立私有 Docker Registry</strong>，所以只能在開發環境跟生產環境內部架設私有 Docker Registry。  </p><h2 id="製作-Docker-Image"><a href="#製作-Docker-Image" class="headerlink" title="製作 Docker Image"></a>製作 Docker Image</h2><p>在建置 Docker Image 時，可利用 Docker Layer 的特性，將非敏感資料及敏感資料分層，把敏感資料的 Layer Size 降至最低，透過 Cleanroom 傳送到生產環境，其餘則直接退送到外部公開的 Docker Registry。  </p><p>Docker Image 分為以下三個階段，分別建置出三個 Docker Image，再拼裝成可直接被運行的 Docker Image。<br>此範例主要是以 ASP.NET Core 作為建置範例，但此範例不侷限於 ASP.NET Core 專案。  </p><p>ASP.NET Core 編譯出來的專案，會依照 namespase 區分出對應的 DLL，例如：  </p><ol><li>MyProject.Website 專案，會生成 MyProject.Website.dll  </li><li>MyProject.Common 專案，會生成 MyProject.Common.dll</li></ol><p>而外部參考的第三方套件，會有自己所屬名稱的 DLL，利用此特性將內部開發的 DLL 及外部參考的 DLL 分離。  </p><h3 id="Build-Artifacts"><a href="#Build-Artifacts" class="headerlink" title="Build Artifacts"></a>Build Artifacts</h3><p>首先建立一個專門編譯的 Container，將編譯後的結果依照名稱特性，區分到不同目錄：  </p><p>檔案 <code>artifacts.dockerfile</code>：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/sdk:<span class="number">2.2</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /src</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./src .</span></span><br><span class="line"><span class="comment"># 編譯 ASP.NET Core，並將編譯結果輸出到 /publish 目錄</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet publish -o /publish --configuration Release</span></span><br><span class="line"><span class="comment"># 把名稱為 MyProject 開頭的檔案，都複製到 /sensitive 目錄</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> /sensitive; \</span></span><br><span class="line"><span class="language-bash">    <span class="built_in">cp</span> -R /publish/MyProject* /sensitive</span></span><br><span class="line"><span class="comment"># 把 /publish 目錄中 MyProject 開頭的檔案全部移除</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">rm</span> -rf /publish/MyProject*</span></span><br></pre></td></tr></table></figure><p>建置 Docker Image，並打上一個自訂的 Tag 名稱：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -f artifacts.dockerfile -t artifacts:latest .</span><br></pre></td></tr></table></figure><blockquote><p><code>artifacts.dockerfile</code> 所產出的 Docker Image 包含的原始碼及編譯後的 DLL，只用在以下兩個 Docker Image 建置時使用。<br>使用完畢<strong>應立即刪除</strong>，最好不要推上 Docker Registry。  </p></blockquote><h3 id="Build-Third-Party-Reference"><a href="#Build-Third-Party-Reference" class="headerlink" title="Build Third-Party Reference"></a>Build Third-Party Reference</h3><p>製作 ASP.NET Core Runtime 環境的 Docker Image，並以 Artifacts Image 作為檔案來源：  </p><p>檔案 <code>third-party.dockerfile</code>：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> artifacts:latest as artifacts</span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/aspnet:<span class="number">2.2</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=artifacts /publish .</span></span><br></pre></td></tr></table></figure><p>建置 Third-Party DLL 的 Docker Image，作為基底 Image：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -f third-party.dockerfile -t third-party:latest .</span><br></pre></td></tr></table></figure><blockquote><p><code>third-party.dockerfile</code> 產出的 Docker Image，是基於微軟官方所提供，隨處可得的公開資料內容；<br>而從 Artifacts 所複製進來的檔案，也都是第三方套件，並沒有什麼敏感性資訊。<br>由於此 Docker Image 不帶有敏感資訊，所以推上 Docker Hub 也無所謂。  </p></blockquote><h3 id="Build-Final-Image"><a href="#Build-Final-Image" class="headerlink" title="Build Final Image"></a>Build Final Image</h3><p>最後以 <code>third-party.dockerfile</code> 為運行環境的基底，做出最後可被執行的 Docker Image。  </p><p>檔案 <code>app.dockerfile</code>：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> artifacts:latest as artifacts</span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> third-party:latest</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=artifacts /sensitive .</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> dotnet MyProject.Website.dll</span></span><br></pre></td></tr></table></figure><p>建置運行環境的 Docker Image：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -f app.dockerfile -t my-project:latest .</span><br></pre></td></tr></table></figure><blockquote><p><code>app.dockerfile</code> 產出的 Docker Image，就是 Artifacts 當初編譯後的結果，只是將資料分在兩個 Dockre Image。  </p></blockquote><h2 id="Push-to-Docker-Registry"><a href="#Push-to-Docker-Registry" class="headerlink" title="Push to Docker Registry"></a>Push to Docker Registry</h2><p>將以上 Docker Image 準備好後，就可以開始時實作此範例流程了：  </p><p><img src="/images/b/32.png" alt="Docker 教學 - Docker Image 混合公有及私有 Docker Registry - 範例實作結果流程圖"></p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 把 my-project 推上開發環境的 Registry</span></span><br><span class="line">docker tag my-project:latest dev-private-registry/my-project:latest</span><br><span class="line">docker push dev-private-registry/my-project:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 把 third-party 推上外部的 Registry</span></span><br><span class="line">docker tag third-party:latest public-registry/third-party:latest</span><br><span class="line">docker push public-registry/third-party:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 在生產環境把第二步的 Image 拉下來</span></span><br><span class="line">docker pull public-registry/third-party:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 把 third-party 推上生產環境的 Registry</span></span><br><span class="line">docker tag public-registry/third-party:latest prod-private-registry/third-party:latest</span><br><span class="line">docker push prod-private-registry/third-party:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 5. 從 Cleanroom 拉下開發環境的 my-project</span></span><br><span class="line">docker pull dev-private-registry/my-project:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 6. 從 Cleanroom 把 my-project 推上生產環境的 Registry</span></span><br><span class="line">docker tag dev-private-registry/my-project:latest prod-private-registry/my-project:latest</span><br><span class="line">docker push prod-private-registry/my-project:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 7. Server 從生產環境的 Registry 取得可執行的 Docker Image</span></span><br><span class="line">docker run prod-private-registry/my-project:latest</span><br></pre></td></tr></table></figure><p>步驟 6 在推送的時候，會先比對目標 Docker Registry 使否已經有 Image 相依 Layers，如果有就不會傳送。<br>而 Layers 的比較依據是 sha，所以 Docker Image 名稱或 Tag 改變，也不會受影響。</p><blockquote><p><code>dev-private-registry</code>、<code>prod-private-registry</code>及<code>public-registry</code>請記得換上實際的 Docker Registry URL。</p></blockquote><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://docs.docker.com/engine/reference/builder/">Docker 官方文件 - Dockerfile reference</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本篇介紹如何依照不同的資料類型分層建置 Docker Image；&lt;br&gt;並把開放性資料及敏感資料的分層，分別推上公有和私有 Docker Registry。  &lt;/p&gt;</summary>
    
    
    
    <category term="Docker" scheme="https://blog.johnwu.cc/categories/docker/"/>
    
    
    <category term="Docker" scheme="https://blog.johnwu.cc/tags/docker/"/>
    
  </entry>
  
  <entry>
    <title>CentOS 安裝及設定 ASP.NET Core + Nginx Proxy</title>
    <link href="https://blog.johnwu.cc/article/centos-asp-net-core-neginx.html"/>
    <id>https://blog.johnwu.cc/article/centos-asp-net-core-neginx.html</id>
    <published>2019-07-31T15:17:00.000Z</published>
    <updated>2026-02-03T05:14:42.574Z</updated>
    
    <content type="html"><![CDATA[<p>本篇介紹在 CentOS 環境下，安裝及設定 ASP.NET Core Runtime 和 Nginx Proxy。<br>並將 ASP.NET Core 註冊成系統服務，便於開機後自動啟動，附上 Shell Script 寫的快速安裝腳本。  </p><span id="more"></span><h2 id="環境"><a href="#環境" class="headerlink" title="環境"></a>環境</h2><ul><li>CentOS 7 Minimal 版  </li><li>ASP.NET Core Runtime 2.2 版</li></ul><h2 id="安裝腳本"><a href="#安裝腳本" class="headerlink" title="安裝腳本"></a>安裝腳本</h2><p>新增一個檔案 <code>setup-aspnet-core.sh</code> 內容如下：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">main</span></span>() &#123;</span><br><span class="line">    <span class="built_in">sudo</span> yum -y install epel-release</span><br><span class="line">    <span class="built_in">sudo</span> yum -y update</span><br><span class="line"></span><br><span class="line">    install_nginx</span><br><span class="line">    install_dotnet</span><br><span class="line"></span><br><span class="line">    <span class="built_in">sudo</span> firewall-cmd --add-service=http --permanent</span><br><span class="line">    <span class="built_in">sudo</span> firewall-cmd --add-service=https --permanent</span><br><span class="line">    <span class="built_in">sudo</span> firewall-cmd --reload</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">install_nginx</span></span>() &#123;</span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;###################################&quot;</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;########## Install Nginx ##########&quot;</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;###################################&quot;</span></span><br><span class="line">    <span class="built_in">sudo</span> yum -y install httpd-tools nginx</span><br><span class="line">    <span class="built_in">sudo</span> setsebool -P httpd_can_network_connect on</span><br><span class="line">    <span class="built_in">sudo</span> sed -i <span class="string">&#x27;s/^SELINUX=.*/SELINUX=disabled/&#x27;</span> /etc/selinux/config</span><br><span class="line">    <span class="built_in">sudo</span> setenforce 0</span><br><span class="line">    <span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> nginx</span><br><span class="line">    <span class="built_in">sudo</span> systemctl restart nginx</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">install_dotnet</span></span>() &#123;</span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;###########################################&quot;</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;########## Install .NET Core 2.2 ##########&quot;</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;###########################################&quot;</span></span><br><span class="line">    <span class="built_in">sudo</span> rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm</span><br><span class="line">    <span class="built_in">sudo</span> yum -y install aspnetcore-runtime-2.2</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">main <span class="string">&quot;<span class="variable">$@</span>&quot;</span></span><br></pre></td></tr></table></figure><p>透過以下指令執行安裝腳本，便會自動安裝 ASP.NET Core Runtime 及 Nginx。  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> sh setup-aspnet-core.sh</span><br></pre></td></tr></table></figure><h2 id="註冊-ASP-NET-Core-服務"><a href="#註冊-ASP-NET-Core-服務" class="headerlink" title="註冊 ASP.NET Core 服務"></a>註冊 ASP.NET Core 服務</h2><p>在 <code>/etc/systemd/system/&lt;自訂名稱&gt;.service</code> 新增一個服務，把 ASP.NET Core 的啟停都透過系統服務控制。<br>例 <code>/etc/systemd/system/my-website.service</code> 內容如下：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">[<span class="string">Unit</span>]</span><br><span class="line"><span class="comment"># Description=&lt;此服務的摘要說明&gt;</span></span><br><span class="line"><span class="string">Description=MyWebsite</span></span><br><span class="line"></span><br><span class="line">[<span class="string">Service</span>]</span><br><span class="line"><span class="comment"># WorkingDirectory=&lt;ASP.NET Core 專案目錄&gt;</span></span><br><span class="line"><span class="string">WorkingDirectory=/usr/share/my-website</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ExecStart=/bin/dotnet &lt;ASP.NET Core 起始 dll&gt;</span></span><br><span class="line"><span class="string">ExecStart=/bin/dotnet</span> <span class="string">MyWebsite.dll</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 啟動若失敗，就重啟到成功為止</span></span><br><span class="line"><span class="string">Restart=always</span></span><br><span class="line"><span class="comment"># 重啟的間隔秒數</span></span><br><span class="line"><span class="string">RestartSec=10</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 設定環境變數，注入給 ASP.NET Core 用</span></span><br><span class="line"><span class="string">Environment=ASPNETCORE_ENVIRONMENT=Production</span></span><br><span class="line"><span class="string">Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false</span></span><br><span class="line"></span><br><span class="line">[<span class="string">Install</span>]</span><br><span class="line"><span class="string">WantedBy=multi-user.target</span></span><br></pre></td></tr></table></figure><blockquote><p>注意！ <code>dotnet</code> CLI 的路徑可能不一樣，有可能如上例在 <strong>&#x2F;bin&#x2F;dotnet</strong> 也有可能在 <strong>&#x2F;usr&#x2F;bin&#x2F;dotnet</strong><br>建議先用指令 <code>which dotnet</code> 查看 <code>dotnet</code> CLI 的路徑。</p></blockquote><p>服務相關指令：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 開啟，開機自動啟動服務</span></span><br><span class="line">systemctl <span class="built_in">enable</span> my-website.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># 關閉，開機自動啟動服務</span></span><br><span class="line">systemctl <span class="built_in">disable</span> my-website.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># 啟動服務</span></span><br><span class="line">systemctl start my-website.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重啟服務</span></span><br><span class="line">systemctl restart my-website.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># 停止服務</span></span><br><span class="line">systemctl stop my-website.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看服務狀態</span></span><br><span class="line">systemctl status my-website.service</span><br></pre></td></tr></table></figure><p>執行啟動指令後，再執行查看服務狀態確認是否執行成功。  </p><h2 id="設定-Nginx-Proxy"><a href="#設定-Nginx-Proxy" class="headerlink" title="設定 Nginx Proxy"></a>設定 Nginx Proxy</h2><p>新增檔案 <code>/etc/nginx/conf.d/default_proxy_settings</code>，以便其它設定重複使用：  </p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">proxy_http_version</span> <span class="number">1.1</span><span class="string">;</span></span><br><span class="line"><span class="string">proxy_set_header</span> <span class="string">Upgrade</span> <span class="string">$http_upgrade;</span></span><br><span class="line"><span class="string">proxy_set_header</span> <span class="string">Connection</span> <span class="string">$http_connection;</span></span><br><span class="line"><span class="string">proxy_set_header</span> <span class="string">Host</span> <span class="string">$host;</span></span><br><span class="line"><span class="string">proxy_set_header</span> <span class="string">X-Real-IP</span> <span class="string">$remote_addr;</span></span><br><span class="line"><span class="string">proxy_set_header</span> <span class="string">X-Forwarded-Host</span> <span class="string">$remote_addr;</span></span><br><span class="line"><span class="string">proxy_set_header</span> <span class="string">X-Forwarded-For</span> <span class="string">$proxy_add_x_forwarded_for;</span></span><br><span class="line"><span class="string">proxy_set_header</span> <span class="string">X-Forwarded-Proto</span> <span class="string">$scheme;</span></span><br><span class="line"><span class="string">proxy_cache_bypass</span> <span class="string">$http_upgrade;</span></span><br></pre></td></tr></table></figure><p>ASP.NET Core Proxy 設定 <code>/etc/nginx/conf.d/my-website.conf</code>：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">upstream</span> <span class="string">portal</span> &#123;</span><br><span class="line">    <span class="comment"># localhost:5000 改成 ASP.NET Core 所監聽的 Port</span></span><br><span class="line">    <span class="string">server</span> <span class="string">localhost:5000;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="string">server</span> &#123;</span><br><span class="line">    <span class="comment"># 只要是透過這些 Domain 連 HTTP 80 Port，都會轉送封包到 ASP.NET Core</span></span><br><span class="line">    <span class="string">listen</span> <span class="number">80</span><span class="string">;</span></span><br><span class="line">    <span class="comment"># 可透過空白區分，綁定多個 Domain</span></span><br><span class="line">    <span class="string">server_name</span> <span class="string">demo.johnwu.cc</span> <span class="string">example.johnwu.cc;</span></span><br><span class="line">    <span class="string">location</span> <span class="string">/</span> &#123;</span><br><span class="line">        <span class="string">proxy_pass</span> <span class="string">http://portal/;</span></span><br><span class="line">        <span class="string">include</span> <span class="string">/etc/nginx/conf.d/default_proxy_settings;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 用 HTTPS 必須要有 SSL 憑證，如果沒有要綁定 SSL 可以把下面整段移除</span></span><br><span class="line"><span class="string">server</span> &#123;</span><br><span class="line">    <span class="comment"># 只要是透過這些 Domain 連 HTTPS 443 Port，都會轉送封包到 ASP.NET Core</span></span><br><span class="line">    <span class="string">listen</span> <span class="number">443</span> <span class="string">ssl;</span></span><br><span class="line">    <span class="string">server_name</span> <span class="string">demo.johnwu.cc;</span></span><br><span class="line">    <span class="string">ssl_certificate</span> <span class="string">/etc/nginx/ssl/demo.johnwu.cc_bundle.crt;</span></span><br><span class="line">    <span class="string">ssl_certificate_key</span> <span class="string">/etc/nginx/ssl/demo.johnwu.cc.key;</span></span><br><span class="line"></span><br><span class="line">    <span class="string">location</span> <span class="string">/</span> &#123;</span><br><span class="line">        <span class="string">proxy_pass</span> <span class="string">http://portal/;</span></span><br><span class="line">        <span class="string">include</span> <span class="string">/etc/nginx/conf.d/nginx_proxy_conf;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>修改完成後，執行以下指令檢查及套用：  </p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 檢查 Nginx 的設定是否有誤</span></span><br><span class="line">nginx -t</span><br><span class="line"></span><br><span class="line"><span class="comment"># 若沒有錯誤，即可套用</span></span><br><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure><p>套用以上設定後，架構如下圖：  </p><p><img src="/images/b/30.png" alt="CentOS 快速安裝 ASP.NET Core 及 Nginx - 架構圖"></p><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://dotnet.microsoft.com/download/linux-package-manager/centos/sdk-current">Install .NET Core SDK on Linux CentOS &#x2F; Oracle - x64</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本篇介紹在 CentOS 環境下，安裝及設定 ASP.NET Core Runtime 和 Nginx Proxy。&lt;br&gt;並將 ASP.NET Core 註冊成系統服務，便於開機後自動啟動，附上 Shell Script 寫的快速安裝腳本。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="CentOS" scheme="https://blog.johnwu.cc/tags/centos/"/>
    
    <category term="Nginx" scheme="https://blog.johnwu.cc/tags/nginx/"/>
    
    <category term="Linux" scheme="https://blog.johnwu.cc/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>Docker 教學 - 打包 ASP.NET Core 前後端專案 Docker Image</title>
    <link href="https://blog.johnwu.cc/article/docker-build-asp-net-core-image.html"/>
    <id>https://blog.johnwu.cc/article/docker-build-asp-net-core-image.html</id>
    <published>2019-07-30T15:14:00.000Z</published>
    <updated>2026-02-03T05:13:46.028Z</updated>
    
    <content type="html"><![CDATA[<p>本篇將介紹如何透過 Dockerfile 製作 ASP.NET Core 的 Docker Image。<br>並透過 <code>.dockerignore</code> 及 <code>docker</code> 指令參數等小技巧，讓專案目錄整理得比較乾淨。  </p><span id="more"></span><h2 id="前置說明"><a href="#前置說明" class="headerlink" title="前置說明"></a>前置說明</h2><p>這個範例有使用到前端及後端的專案，目錄結構大致長成這樣：  </p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">build/</span>                      <span class="comment"># 存放建置 docker image 所需的檔案</span></span><br><span class="line"><span class="string">doc/</span>                        <span class="comment"># 專案相關的文件</span></span><br><span class="line"><span class="string">scripts/</span>                    <span class="comment"># 開發環境所需的腳本</span></span><br><span class="line"><span class="string">src/</span>                        <span class="comment"># dotnet 的專案目錄</span></span><br><span class="line">    <span class="string">XXXX.Domain/</span>            <span class="comment"># 函式庫專案目錄</span></span><br><span class="line">    <span class="string">XXXX.WebPortal/</span>         <span class="comment"># ASP.NET Core 專案目錄</span></span><br><span class="line">        <span class="string">ClientApp/</span>          <span class="comment"># 前端的專案</span></span><br><span class="line">            <span class="string">node_modules/</span>   <span class="comment"># 前端專案的相依目錄</span></span><br><span class="line">            <span class="string">package.json</span>    <span class="comment"># 前端專案的相依設定</span></span><br><span class="line">        <span class="string">wwwroot/</span>            <span class="comment"># 前端專案打包的結果</span></span><br><span class="line"><span class="string">test/</span>                       <span class="comment"># dotnet 的測試專案目錄</span></span><br><span class="line">    <span class="string">XXXX.IntegrationTest/</span>   <span class="comment"># 整合測試專案目錄</span></span><br><span class="line">    <span class="string">XXXX.UnitTest/</span>          <span class="comment"># 單元測試專案目錄</span></span><br><span class="line"><span class="string">XXXX.sln</span>                    <span class="comment"># dotnet 的方案檔</span></span><br></pre></td></tr></table></figure><h2 id="Dockerfile"><a href="#Dockerfile" class="headerlink" title="Dockerfile"></a>Dockerfile</h2><h3 id="dotnet-core"><a href="#dotnet-core" class="headerlink" title="dotnet core"></a>dotnet core</h3><p>大部分的 <code>Dockerfile</code> 範例，會把這個檔案放在方案根目錄；<br>但此檔跟建置相關，放到 <code>build</code> 目錄會比較是適合，同時改一下檔案名稱，比較容易識別。如：  </p><p><code>build/build-image.dockerfile</code> 內容如下：  </p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">### Build Stage</span></span><br><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/sdk:<span class="number">2.2</span> AS dotnet-build-<span class="keyword">env</span></span><br><span class="line"><span class="keyword">ARG</span> project_name</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./src /src</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /src</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet publish <span class="variable">$project_name</span> -o /publish --configuration Release</span></span><br><span class="line"></span><br><span class="line"><span class="comment">### Publish Stage</span></span><br><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/aspnet:<span class="number">2.2</span></span><br><span class="line"><span class="keyword">ARG</span> project_name</span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=dotnet-build-env /publish .</span></span><br><span class="line"><span class="keyword">ENV</span> project_dll=<span class="string">&quot;$&#123;project_name&#125;.dll&quot;</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> dotnet <span class="variable">$project_dll</span></span></span><br></pre></td></tr></table></figure><blockquote><ul><li>檔案名稱可以自訂，附檔名也沒有任何限制；但可透過修改 IDE 自動偵測附檔名，把 <code>*.dockerfile</code> 都以 <code>Dockerfile</code> 的格式開啟會比較方便。  </li><li>ASPNET_PROJECT_NAME</li></ul><p>  名稱從外部帶入有個好處，就是大部分的 dotnet core 專案都適用這個 <code>Dockerfile</code> 建置 Docker Image。  </p></blockquote><p>建置指令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker build -f [DOCKERFILE_PATH] -t [IMAGE_NAME]:[TAG] --build-arg project_name=[ASPNET_PROJECT_NAME] .</span><br><span class="line"><span class="comment"># 範例： docker build -f build/build-image.dockerfile -t web-portal:develop --build-arg project_name=XXXX.WebPortal .</span></span><br></pre></td></tr></table></figure><p>以上 <code>Dockerfile</code> 共分為兩個階段：  </p><ol><li>第一階段產生名稱為 <em>dotnet-build-env</em> 的暫存 Container，然後把方案根目錄的 <code>src</code> 複製到 <em>dotnet-build-env</em>，執行 <code>dotnet publish</code> 把建置的結果放到 <code>/publish</code> 目錄。<br><code>mcr.microsoft.com/dotnet/core/sdk:2.2</code> 是拿來編譯用的，大小約 1.74 GB。  </li><li>第二階段把 <em>dotnet-build-env</em> <code>/publish</code> 目錄內的檔案，全部複製到最終階段 Container 的 <code>/app</code> 目錄，並指定 Docker 啟動時要執行的指令。<br>  <code>mcr.microsoft.com/dotnet/core/aspnet:2.2</code> 是執行階段用的，大小約 260 MB。<br>  若要追求最小化 Docker Image，可選用 <code>alpine</code> 版本。</li></ol><p>建置流程如下：</p><p><img src="/images/b/28.png" alt="Docker 教學 - 打包 ASP.NET Core 前後端專案 Docker Image - Dockerfile dotnet core"></p><h3 id="npm"><a href="#npm" class="headerlink" title="npm"></a>npm</h3><p>此範例除了有 ASP.NET Core 專案，同時也包含了前端專案在裡面，前端專案如果是用 <em>TypeScript</em> 編寫或其它需要 <em>Webpack</em> 打包等動作，都會需要 <em>node.js</em>。<br>因此，可以透過另一個暫存 Container，負責打包前端專案。  </p><p><code>build/build-image.dockerfile</code> 內容如下：  </p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">### Build Stage - dotnet</span></span><br><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/sdk:<span class="number">2.2</span> AS dotnet-build-<span class="keyword">env</span></span><br><span class="line"><span class="keyword">ARG</span> project_name</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./src /src</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /src</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> dotnet publish <span class="variable">$project_name</span> -o /publish --configuration Release</span></span><br><span class="line"></span><br><span class="line"><span class="comment">### Build Stage - npm</span></span><br><span class="line"><span class="keyword">FROM</span> node:<span class="number">11</span> AS npm-build-<span class="keyword">env</span></span><br><span class="line"><span class="keyword">ARG</span> project_name</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> -p /publish</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> npm <span class="built_in">set</span> progress=<span class="literal">false</span>;</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./src /src</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /src/<span class="variable">$project_name</span></span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="keyword">if</span> [ -f <span class="string">&quot;package.json&quot;</span> ]; <span class="keyword">then</span> \</span></span><br><span class="line"><span class="language-bash">        npm i; \</span></span><br><span class="line"><span class="language-bash">        npm run build; \</span></span><br><span class="line"><span class="language-bash">        <span class="keyword">if</span> [ -d <span class="string">&quot;wwwroot&quot;</span> ]; <span class="keyword">then</span> <span class="built_in">cp</span> -R wwwroot /publish; <span class="keyword">fi</span>; \</span></span><br><span class="line"><span class="language-bash">    <span class="keyword">fi</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">### Publish Stage</span></span><br><span class="line"><span class="keyword">FROM</span> mcr.microsoft.com/dotnet/core/aspnet:<span class="number">2.2</span></span><br><span class="line"><span class="keyword">ARG</span> project_name</span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=dotnet-build-env /publish .</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=npm-build-env /publish .</span></span><br><span class="line"><span class="keyword">ENV</span> project_dll=<span class="string">&quot;$&#123;project_name&#125;.dll&quot;</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> dotnet <span class="variable">$project_dll</span></span></span><br></pre></td></tr></table></figure><blockquote><ul><li><em>npm-build-env</em> 先判斷專案目錄內是否有 <code>package.json</code> 檔案，若存在才會執行 npm build。這樣做的好處是，不管 dotnet core 專案是否有前端專案，都可共用此 <code>Dockerfile</code> 建置 Docker Image。  </li><li>以此範例來說，<code>src/XXXX.WebPortal/wwwroot</code> 目錄為前端專案打包的結果，ASP.NET Core 執行根路徑的 <code>wwwroot</code> 目錄為靜態檔案的位置。若實作上的目錄名稱不同，需自行更改位置。</li></ul></blockquote><p>以上 <code>Dockerfile</code> 分為三個階段，在中間又插入了 <code>npm run build</code> 的動作，然後把最終結果複製到 <em>Publish Stage</em>。  </p><p>建置流程如下：</p><p><img src="/images/b/29.png" alt="Docker 教學 - 打包 ASP.NET Core 前後端專案 Docker Image - Dockerfile npm">  </p><h2 id="dockerignore"><a href="#dockerignore" class="headerlink" title=".dockerignore"></a>.dockerignore</h2><p>從 Host 複製 <code>src</code> 目錄到 Container 時，可能會複製到不必要的檔案，如開發階段所產生的目錄： <code>bin</code>、<code>obj</code> 及 <code>node_modules</code> 等。<br>為了加速複製檔案，我們可以在方案根目錄新增 <code>.dockerignore</code>，告知 <strong>COPY</strong> 忽略這些目錄或檔案，例：  </p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">.git/</span></span><br><span class="line"><span class="string">.vs/</span></span><br><span class="line"><span class="string">.vscode/</span></span><br><span class="line"><span class="string">build/</span></span><br><span class="line"><span class="string">docs/</span></span><br><span class="line"><span class="string">scripts/</span></span><br><span class="line"><span class="string">**/bin/</span></span><br><span class="line"><span class="string">**/obj/</span></span><br><span class="line"><span class="string">**/packages/</span></span><br><span class="line"><span class="string">**/node_modules/</span></span><br><span class="line"><span class="string">**/publish/</span></span><br><span class="line"><span class="string">**/coverage/</span></span><br><span class="line"><span class="string">**/TestResults/</span></span><br></pre></td></tr></table></figure><h2 id="清除-Temp-images"><a href="#清除-Temp-images" class="headerlink" title="清除 Temp images"></a>清除 Temp images</h2><p><code>Dockerfile</code> 執行建置後，只會把最後一個 Container 賦予名稱當作最終結果，其它的 Stage 並不會消失，若執行 <code>docker images</code> 指令，會發現有一大堆顯示為 <strong>&lt;none&gt;</strong> 的 Docker Image。<br>可透過以下指令快速移除：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Linux / MacOS</span></span><br><span class="line">containers=`docker images -f <span class="string">&quot;dangling=true&quot;</span> -q`; <span class="keyword">if</span> [ -n <span class="string">&quot;<span class="variable">$containers</span>&quot;</span> ] ; <span class="keyword">then</span> docker rmi -f <span class="variable">$containers</span>; <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Windows</span></span><br><span class="line">FOR /f <span class="string">&quot;tokens=*&quot;</span> %i IN (<span class="string">&#x27;docker images -f &quot;dangling=true&quot; -q&#x27;</span>) DO docker rmi -f %i</span><br></pre></td></tr></table></figure><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><ul><li><a href="https://docs.docker.com/engine/reference/builder/">Docker 官方文件 - Dockerfile reference</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;本篇將介紹如何透過 Dockerfile 製作 ASP.NET Core 的 Docker Image。&lt;br&gt;並透過 &lt;code&gt;.dockerignore&lt;/code&gt; 及 &lt;code&gt;docker&lt;/code&gt; 指令參數等小技巧，讓專案目錄整理得比較乾淨。  &lt;/p&gt;</summary>
    
    
    
    <category term="Docker" scheme="https://blog.johnwu.cc/categories/docker/"/>
    
    
    <category term="npm" scheme="https://blog.johnwu.cc/tags/npm/"/>
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="Docker" scheme="https://blog.johnwu.cc/tags/docker/"/>
    
  </entry>
  
  <entry>
    <title>ASP.NET Core 教學 - Open XML SDK 匯入 Excel</title>
    <link href="https://blog.johnwu.cc/article/asp-net-core-import-from-excel.html"/>
    <id>https://blog.johnwu.cc/article/asp-net-core-import-from-excel.html</id>
    <published>2019-04-17T13:54:00.000Z</published>
    <updated>2026-02-03T04:09:36.646Z</updated>
    
    <content type="html"><![CDATA[<p><img src="/images/a/294.png" alt="ASP.NET Core 教學 - 匯出 Excel - 執行結果"></p><p>之前介紹過 ASP.NET Core 用 Open XML SDK 匯出 Excel 的功能，但沒介紹匯入 Excel。<br>被網友提問後，馬上補了這篇介紹 ASP.NET Core 利用 Open XML SDK 匯入 Excel 的基本用法。  </p><span id="more"></span><h2 id="安裝-NuGet-套件"><a href="#安裝-NuGet-套件" class="headerlink" title="安裝 NuGet 套件"></a>安裝 NuGet 套件</h2><p>Open XML SDK 這個套件支援 .NET 操作 Word、Excel、PowerPoint。<br>打開 NuGet 找到 <code>DocumentFormat.OpenXml</code> 並安裝。  </p><h2 id="View"><a href="#View" class="headerlink" title="View"></a>View</h2><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">form</span> <span class="attr">method</span>=<span class="string">&quot;post&quot;</span> <span class="attr">enctype</span>=<span class="string">&quot;multipart/form-data&quot;</span> <span class="attr">action</span>=<span class="string">&quot;/api/import&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;file&quot;</span> <span class="attr">name</span>=<span class="string">&quot;files&quot;</span> <span class="attr">accept</span>=<span class="string">&quot;application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">value</span>=<span class="string">&quot;送出&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="SpreadsheetDocument"><a href="#SpreadsheetDocument" class="headerlink" title="SpreadsheetDocument"></a>SpreadsheetDocument</h2><p>要操作 Excel 檔案，主要是透過 <code>SpreadsheetDocument</code> 物件。<br><code>SpreadsheetDocument</code> 可以讀取檔案，也可以直接讀取 <code>Stream</code>。<br>此範例是從 ASP.NET Core 的 Request 取得 <code>FileStream</code>，進而讀取 Excel 檔案：</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System.Linq;</span><br><span class="line"><span class="keyword">using</span> System.Threading.Tasks;</span><br><span class="line"><span class="keyword">using</span> DocumentFormat.OpenXml.Packaging;</span><br><span class="line"><span class="keyword">using</span> DocumentFormat.OpenXml.Spreadsheet;</span><br><span class="line"><span class="keyword">using</span> Microsoft.AspNetCore.Mvc;</span><br><span class="line"><span class="keyword">using</span> MyWebsite.Filters;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">MyWebsite.Controllers</span></span><br><span class="line">&#123;</span><br><span class="line">    [<span class="meta">Route(<span class="string">&quot;api/[controller]&quot;</span>)</span>]</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">ImportController</span> : <span class="title">Controller</span></span><br><span class="line">    &#123;</span><br><span class="line">        [<span class="meta">HttpPost</span>]</span><br><span class="line">        [<span class="meta">DisableFormValueModelBinding</span>]</span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">async</span> Task&lt;IActionResult&gt; <span class="title">Post</span>()</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">await</span> Request.StreamFile(<span class="keyword">file</span> =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">using</span> (<span class="keyword">var</span> document = SpreadsheetDocument.Open(<span class="keyword">file</span>.FileStream, <span class="literal">false</span>))</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="keyword">var</span> workbookPart = document.WorkbookPart;</span><br><span class="line">                    <span class="keyword">var</span> sheet = workbookPart.Workbook.Sheets.GetFirstChild&lt;Sheet&gt;();</span><br><span class="line">                    <span class="keyword">var</span> worksheet = ((WorksheetPart)workbookPart.GetPartById(sheet.Id)).Worksheet;</span><br><span class="line">                    <span class="keyword">var</span> sheetData = worksheet.GetFirstChild&lt;SheetData&gt;();</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">foreach</span> (<span class="keyword">var</span> row <span class="keyword">in</span> sheetData.ChildElements.Select(x=&gt;x <span class="keyword">as</span> Row))</span><br><span class="line">                    &#123;</span><br><span class="line">                        <span class="comment">// 取得每一行</span></span><br><span class="line">                        <span class="keyword">foreach</span>(<span class="keyword">var</span> cell <span class="keyword">in</span> row.ChildElements.Select(x =&gt; x <span class="keyword">as</span> Cell))</span><br><span class="line">                        &#123;</span><br><span class="line">                            <span class="comment">// 取得每一欄</span></span><br><span class="line">                            <span class="keyword">var</span> innerText = cell.CellValue.Text;</span><br><span class="line">                            <span class="comment">// Do somethings</span></span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">file</span>.FileStream;</span><br><span class="line">            &#125;);</span><br><span class="line">            <span class="keyword">return</span> Ok();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><code>DisableFormValueModelBinding</code> 及 <code>Request.StreamFile</code> 擴充方法請參考 <a href="/article/ironman-day23-asp-net-core-upload-download-files.html">[鐵人賽 Day23] ASP.NET Core 2 系列 - 上傳&#x2F;下載檔案</a></p></blockquote><h2 id="參考"><a href="#參考" class="headerlink" title="參考"></a>參考</h2><p><a href="https://github.com/OfficeDev/Open-XML-SDK">Open-XML-SDK</a><br><a href="https://social.technet.microsoft.com/wiki/contents/articles/35010.read-excel-files-using-open-xml-sdk-in-asp-net-c.aspx">Read Excel Files Using Open XML SDK In ASP.NET C#</a><br><a href="https://goo.gl/TU7QMY">How to create an Excel file in .NET using OpenXML – Part 1: Basics</a><br><a href="https://msdn.microsoft.com/zh-tw/library/office/bb448854.aspx">歡迎使用 Open XML SDK 2.5 for Office</a>  </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;/images/a/294.png&quot; alt=&quot;ASP.NET Core 教學 - 匯出 Excel - 執行結果&quot;&gt;&lt;/p&gt;
&lt;p&gt;之前介紹過 ASP.NET Core 用 Open XML SDK 匯出 Excel 的功能，但沒介紹匯入 Excel。&lt;br&gt;被網友提問後，馬上補了這篇介紹 ASP.NET Core 利用 Open XML SDK 匯入 Excel 的基本用法。  &lt;/p&gt;</summary>
    
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/categories/asp-net-core/"/>
    
    
    <category term="ASP.NET Core" scheme="https://blog.johnwu.cc/tags/asp-net-core/"/>
    
    <category term="C#" scheme="https://blog.johnwu.cc/tags/c/"/>
    
    <category term="Excel" scheme="https://blog.johnwu.cc/tags/excel/"/>
    
    <category term="Open XML SDK" scheme="https://blog.johnwu.cc/tags/open-xml-sdk/"/>
    
  </entry>
  
</feed>
