---
title: 'llms.txt:網站正在從 Human-first 走向 Human 及 Agent-first'
description: '分析 AI 時代網站架構的演變,以及從 Human-first 轉向 Agent-first 的趨勢'
slug: 'llms-txt-human-agent-first-web'
date: '2026-05-17'
drafted: false
featured: true
topic: 'artificial-intelligence'
tags: ['ai-agent', 'website', 'llms-txt']
authors: ['neil-tsai']
---
最近如果有在關注 AI、開發者工具或技術文件,應該多少都看過 `llms.txt` 這個東西。
很多人會用一句話介紹它:
> `llms.txt` 是給 AI 的 `robots.txt`。
這個說法不能算錯,但其實只講到表面。
`robots.txt` 是告訴爬蟲哪些地方能不能爬,`sitemap.xml` 是告訴搜尋引擎網站有哪些頁面,而 `llms.txt` 更像是網站主動提供給 AI 的「閱讀入口」:哪些內容重要、哪些文件值得優先看、這個網站到底是做什麼的。
它真正值得注意的地方,不是檔案格式,而是它背後代表的訊號:
✨ **網站正開始從 Human-first,逐漸走向 Human 及 Agent-first**
## llms.txt 是什麼?
`llms.txt` 是一個放在網站根目錄的文字檔:
```txt
https://example.com/llms.txt
```
通常會使用 Markdown 撰寫,內容可能長這樣:
```md
# Product Name
簡短介紹這個網站或產品。
## Docs
- [Getting Started](https://example.com/docs/start.md)
- [API Reference](https://example.com/docs/api.md)
## Examples
- [Example Apps](https://example.com/examples.md)
```
它的目的不是取代網站,而是提供一份內容入口:
- 更乾淨
- 更精簡
- 更容易被 AI 理解
❗ **換句話說,它不是給人類看的,而是給 AI 看的內容**
## 為什麼會出現這東西?
因為 AI 其實很不擅長閱讀現代網站。
🔥 **不是 AI 能力不夠,而是現代網站本來就不是為了 AI 設計的**
現在的網站通常包含:
- 大量 JavaScript
- SPA Hydration
- 追蹤腳本(ex. Google Analytics)
- 廣告
- 五花八門的結構
👀 **人類可以透過視覺快速忽略這些東西**
⚡ **但對 AI 來說,它們全都是 token,真正有價值的內容,可能只佔整個頁面的 5%**
AI 在讀網站時,很容易遇到大量像這樣的內容:
```html
```
💸 **這些東西對人類沒影響,但對 Context Window 是實際成本**
而且 AI 使用網站的方式,也跟人類不同。
人類會:
- 看畫面
- 點選單
- 掃視覺層級
- 自己判斷哪些資訊重要
但 AI 更需要的是:
- 這網站是做什麼的?
- 哪些文件最重要?
- 哪份是標準來源(Canonical Source)?
- 哪份文件最值得引用?
- 哪些內容是最新版本?
於是 `llms.txt` 的核心思想就出現了:
💡 **不要讓 AI 自己猜哪些內容重要,而是由網站主動提供一份 AI-friendly 的入口**
## 這東西真正的價值在哪?
很多人把 `llms.txt` 當成 SEO 延伸,但它真正的價值其實更接近:
### 網站層級的提示工程
因為它本質上是在幫 AI:
- 降低檢索雜訊(Retrieval Noise)
- 壓縮上下文(Context)
- 建立資訊層級(Hierarchy)
- 指向權威來源(Authoritative Source)
這件事很重要。
未來 AI 代理在使用網站時,很可能不會從首頁開始理解產品,而是:
```txt
1. 先讀 llms.txt
2. 找重要文件
3. 找 API 參考
4. 找範例
5. 建立上下文
```
這代表網站開始出現兩層結構:
```txt
Human-facing Website
Agent-facing Entry
```
這才是 `llms.txt` 真正有趣的地方。
它不是單純的文字檔,而是:
> **網站開始出現「AI-readable Layer」的早期跡象**
但這裡要先冷靜一下。
🔧 **支援的現狀**
目前 `llms.txt` 還不是正式 Web 標準,也沒有被所有主流 AI 平台正式全面採用。
它現在比較像:
- 社群提案
- 開發者慣例
- AI-friendly 文件實驗方向
不過已經有一些大型 AI 平台開始提供自己的 `llms.txt` 或類似入口。
例如:
- OpenAI(ex. [OpenAI API docs](https://developers.openai.com/api/docs/llms.txt))
- Anthropic(ex. [Anthropic Developer Documentation](https://platform.claude.com/llms.txt))
都已經提供更容易被 AI 閱讀的文件入口。
這代表:
> 即使 `llms.txt` 本身未必成為最終標準,「AI-readable Layer」這件事已經開始成形。
所以真正值得關注的,不是:
> `llms.txt` 會不會變成標準?
而是:
> 未來網站是否需要 AI-readable Layer?
這兩件事其實不一樣。
## Human-first 正在變成 Human 及 Agent-first
我認為這才是整件事最重要的地方。
過去的網站,本質上是:
```txt
HTML → Browser → Human
```
網站設計的核心問題是:
- 人怎麼看?
- 人怎麼點?
- 人怎麼搜尋?
- 人怎麼理解資訊?
但 AI 代理出現後,網站不再只面對人類。
開始也要面對:
- LLM
- AI 助理
- 程式碼撰寫代理
- 瀏覽器代理
- 自動化系統
於是網站開始變成:
```txt
Content → Agent → Human
```
這會改變很多事情。
例如:
- 文件架構
- SEO
- 元資料
- API 可發現性
- 內容建模
- 資訊層級
以前網站只需要:
> Human-readable
未來還會需要:
> Agent-readable
這其實是很大的轉變。
## 對工程師真正有價值的是什麼?
如果你是工程師,那真正該思考的事情,不是:
> 我要不要加一個 llms.txt?
而是:
> 我的產品能不能被 AI 正確理解?
這會影響很多東西。
## 入口內容是否乾淨?
如果你的內容:
- 版本混亂
- 標準來源不清楚
- 範例太少
- 導航太深
- HTML 雜訊過高
那 AI 很容易讀錯。
> AI 吃到垃圾,就只會吐出垃圾。(你還妄想可以變黃金?不可能 😅)
## Markdown 會越來越重要
這也是為什麼:
- Docs-as-code
- MDX
- README-first
- Knowledge Base
會越來越重要。
因為 Markdown 對:
> 人類可讀,AI 也友善
## 結構化內容也會越來越重要
未來網站可能不只需要:
> 漂亮 UI
還需要:
> 可被 AI 結構化理解
例如:
- 問答
- 範例
- API 結構
- 更新日誌
- 功能描述
都會變得更重要。
## GEO(Generative Engine Optimization)
以前網站競爭的是:
> 誰在 Google 排名比較前面
未來可能會慢慢變成:
> 誰比較容易被 AI 正確引用
這其實是完全不同的問題。
## llms.txt 也可能只是過渡產物
我不確定 `llms.txt` 最後會不會變成正式標準。
它可能:
- 被搜尋引擎整合
- 被瀏覽器標準化
- 被 AI 平台自訂格式取代
但它提出的問題不會消失。
因為網站正在慢慢從:
> 給人看的網站
變成:
> 給人與 AI 一起使用的系統
而 `llms.txt` 的真正意義,其實不是那個文字檔本身。
而是它第一次很明確地讓大家開始思考:
> 如果網站也要給 AI 使用,那網站應該長什麼樣子?
這才是它真正重要的地方。
## 結語
`llms.txt` 表面上看起來只是個小規格。
但它背後反映的,其實是整個網站生態正在改變。
未來網站不只需要:
- 被搜尋引擎索引
- 被人類閱讀
還需要:
- 被 AI 理解
- 被 AI 引用
- 被 AI 正確操作
而這種從 Human-first 走向 Human 及 Agent-first 的轉變,可能才是 AI 時代網站最值得注意的事情之一。
📌 順帶一提,本部落格也有提供 [llms.txt](https://www.thinkinmd.com/llms.txt) 及 [llms-full.txt](https://www.thinkinmd.com/llms-full.txt),幫助 AI 代理更好地理解網站內容。
## 參考
💭 [The /llms.txt file](https://llmstxt.org)
---
title: 'AI 時代的文件革命'
description: '為什麼 HTML 正在取代 Markdown 成為 Agent 的新介面?'
slug: 'artifact-driven-ai-workflows'
date: '2026-05-12'
drafted: false
featured: true
topic: 'artificial-intelligence'
tags: ['document', 'artifact-driven']
authors: ['neil-tsai']
---
最近看到一篇很有意思的文章:
> _[The Unreasonable Effectiveness of HTML](https://x.com/trq212/status/2052809885763747935)_
它表面上是在討論:
- HTML vs Markdown
- Claude Code
- AI 工作流程
但實際上,它談的是一件更深層的事情:
✨ **AI 時代的人機協作型態,正在發生根本改變。**
## Markdown 曾是 AI 的完美格式
過去幾年,Markdown 幾乎成為 AI Agent 與人類溝通的標準格式。
原因很簡單:
- 純文字
- 好編輯
- 好版本控制
- 可攜性高
- Token 成本低
它非常適合:
- 規格文件
- 列出待辦事項
- PR 說明
- 提示詞
- Agent 記憶
甚至很多 AI workflow,根本就是圍繞 Markdown 建立的。
但問題是:
💥 **AI 已經開始超出 Markdown 的能力範圍了。**
## 問題不在 AI,而在「輸出介面」
以前 AI 很弱。
所以:
- 幾十行摘要
- 小段程式碼
- 簡單說明
Markdown 足夠。
---
但現在的 Agent 已經能:
- 產生完整架構計劃
- 分析大型程式碼專案
- 建立複雜工作流程
- 做 UI 原型
- 整理跨來源研究
- 建立互動式工具
這時候,Markdown 開始出現根本性的限制。
---
因為 Markdown 本質上還是:
> 線性文字文件(Linear Text Document)
它擅長:
- 文字
- 標題
- 清單
但不擅長:
- 高資訊密度
- 空間佈局
- 視覺層級
- 互動
- 動態呈現
於是 AI 開始會做一些很荒謬的事情。
例如:
- ASCII 圖表
- Unicode 色塊
- 偽圖形排版
😅 **因為模型其實「想表達更多」,但 Markdown 裝不下。**
## 人類根本不會想讀長的 Markdown
作者提到:
> 超過 100 行的 Markdown 文件,幾乎沒人認真看。
這件事非常真實。
AI 越強:
- 文件越完整
- 思考越深
- 規格越大
結果就是:
💥 **人類閱讀成本爆炸。**
於是問題不再是:
> AI 能不能產生內容?
而是:
> 人類還願不願意讀?
這時候 HTML 的優勢就浮現了。
## HTML 真正厲害的地方,不是「漂亮」
很多人會誤以為:
> HTML 只是比較好看。
但其實不是。
真正的差別在於:
📄 **HTML 是「高資訊密度介面」。**
它能提供:
- 視覺層級
- 分頁
- 可折疊區塊
- 行內圖表
- 響應式佈局
- 顏色分組
- 互動元件
這些東西都在降低:
👀 **Cognitive Load(認知負擔)**
這很重要。
因為 AI 最大的問題之一,就是:
> 它輸出的資訊量,已經超過人類能輕鬆消化的程度。
HTML 本質上是在解決:
✨ **如何讓人類理解 AI 的輸出。**
而不只是「如何顯示內容」。
## HTML 不只是文件,而是「互動介面」
作者並不是把 HTML 當成:
- 網頁
- 文件格式
而是:
👉 **AI Runtime Interface**
例如:
- 滑桿
- 旋鈕
- 拖放
- 即時預覽
- 組態編輯器
- 提示詞遊樂場
這代表:
AI 不再只是「給你答案」。
而是:
> 給你一個可以探索問題空間的介面。
這跟傳統文件是完全不同的概念。
## 未來的文件,會開始「可執行化」
過去的文件是:
- 靜態
- 描述性
- 單向閱讀
但 AI 時代的產出物(artifact)開始變成:
- 互動式
- 可調整
- 可檢查
- 可執行
也就是:
🔑 **Document → Tool**
這是一個非常巨大的轉變。
## UI 開始變成「一次性用品」
作者提到:
他會直接讓 Claude 建立:
- 工單排程器(ticket prioritizer)
- 功能旗標編輯器(feature flag editor)
- 提示詞優化工具(prompt tuning tool)
- 臨時儀表板(temporary dashboard)
而且不是做成正式產品。
只是:
> 一次性工具。
這件事非常顛覆。
因為過去:
做 UI 成本很高。
所以:
- 要設計
- 要開發
- 要維護
但現在:
AI 可以在幾分鐘內生成一個剛好適合當下問題的 UI。
這代表:
🔥 **UI 正在 Disposable 化。**
介面不再是昂貴資產。
而是:
> AI 動態生成的工作介面。
## AI 工作流程正在從 Chat 走向 Artifact
我們過去的 AI 使用方式:
```text
Human ↔ Chat Interface ↔ AI
```
但未來會慢慢變成:
```text
Human ↔ Interactive Artifact ↔ AI
```
差別非常巨大。
因為:
- Chat 是線性的
- Artifact 是空間性的
- Chat 適合對話
- Artifact 適合理解複雜系統
而 AI 越強,複雜度只會越來越高。
## Markdown 的優勢,正在被削弱
文章裡有一句非常重要:
> 作者現在幾乎不自己改文件,而是讓 Claude 幫他改。
這件事會導致一個巨大改變。
過去 Markdown 的優勢:
- 人類可編輯
- 適合版本控制
但如果:
❗ **人類不再是主要編輯者。**
那 Markdown 的優勢就開始下降。
而 HTML 的缺點:
- 雜亂的差異
- 難以手動修改
反而就沒那麼嚴重了。
因為真正修改內容的人,是 Agent。
## 對軟體工程的影響,可能比想像更大
它可能正在預示:
**軟體開發介面的轉型。**
未來很多東西可能都會變成:
- AI 生成的儀表板
- 臨時探索 UI
- 解釋性產出物
- 互動規格
- 可執行報告
甚至:
**提示工程本身都可能 UI 化。**
不是再寫:
```text
請幫我調整語氣
```
而是:
- 調滑桿
- 即時預覽
- 即時比較
- 視覺化參數調整
這是非常不同的世界。
## HTML 也不是沒有問題
### 1. HTML 差異比較很糟
這是真的。
HTML 很容易:
- 雜亂
- 難以審核
- 合併非常痛苦
所以它未必適合:
- 長期作為事實來源(source-of-truth)
- 版本控制
### 2. AI 很容易產生華而不實的 UI
這其實是最大風險。
AI 很會:
- 做動畫
- 做漸層
- 做花俏排版
但:
😂 **好看的 UI 不等於好理解。**
真正困難的是:
> 提升理解能力(comprehension),而不是干擾(distraction)。
### 3. 維護成本仍然存在
一次性產出物很棒。
但如果開始長期維護:
- inline CSS
- spaghetti JS
- accessibility
- component consistency
問題還是會浮現。
🔎 **真正值得注意的,不是 HTML。**
很多人看完文章後,可能會以為重點是:
> 以後我們都該改用 HTML
😎 但我覺得真正重要的不是 HTML。
而是:
**AI 正在從「文字生成器」變成「介面生成器」。**
這才是革命性的地方。
## 結語
我認為這篇文章真正厲害的地方在於:
它不是在談:
- 前端技術
- 文件格式
- Claude 小技巧
而是在談:
**AI 時代的人機協作型態正在改變。**
過去我們使用 AI 的方式是:
> 讓 AI 幫我寫東西。
但未來可能會變成:
> 讓 AI 幫我建立理解問題的介面。
而這兩者之間,其實差了一整個世代。
## 最後一句總結
如果要用一句話總結這篇文章,我會這樣說:
> AI 的下一個階段,不只是生成內容,而是生成讓人類理解世界的介面。
---
title: 'OpenAI 與 Anthropic 模型提示引導的差異性'
description: 'AI 真的變笨了嗎?還是人不懂得變通?'
slug: 'prompt-guidance-diff'
date: '2026-05-03'
drafted: false
featured: true
topic: 'artificial-intelligence'
tags: ['prompt-engineering']
authors: ['neil-tsai']
---
## 前言
以下將以兩個模型為例,說明兩者在提示引導上的核心差異。這些差異不只是技術細節,更反映了**兩家公司對於「如何與 AI 互動」的不同哲學與設計思維**。
理解這些差異,**有助於我們在實際應用中選擇適合的提示策略,達到更好的效果**。
- OpenAI `GPT-5.5`
- Anthropic `Claude Opus 4.7`
## 一句話總結
- OpenAI:**把目標講清楚,讓模型自己選擇**。
- Claude:**把規格講清楚,讓模型精準照做**。
## 核心差異表
| 面向 | OpenAI | Claude |
| ---------- | -------------------------- | ---------------------------------------- |
| 提示哲學 | 結果導向 | 規格導向 |
| 最佳寫法 | 描述「好結果應該長什麼樣」 | 明確列出任務、限制、格式、範例 |
| 流程步驟 | 不建議過度指定流程 | 當順序重要時,可以用有序的方式來規劃步驟 |
| 模型自主性 | 較高,適合讓它自己規劃 | 較吃明確指令與上下文 |
| 範例 | 有幫助,但不是必備 | 很重要,官方建議 3–5 個範例 |
| 格式控制 | 指定輸出輪廓即可 | 建議更明確,甚至用 XML 標籤 |
| 風險 | 過度提示會變僵、慢、噪音多 | 提示太模糊會猜錯或輸出泛化答案 |
| 適合心法 | 交付任務 | 寫規格書 |
## OpenAI 提示重點
✨ **短一點、結果優先、少一點流程控制**
官方文件說比較新的模型通常更適合「結果導向」提示,也就是定義目標、成功條件、限制、可用證據與最終答案內容,而不是把每一步流程都寫死。
💥 **過度沿用舊的提示堆疊可能增加噪音、限縮模型搜尋空間,導致回答太機械**。
> 提示堆疊:在提示中明確指定每一步驟的內容和順序,試圖控制模型的思考過程。(**因為對話是會累積的,過度提示可能導致模型僵化或產生非預期噪音**)
👍 適合這樣寫:
```text
請比較 OpenAI 與 Claude 的提示引導差異。
成功標準:
- 一般使用者看得懂
- 說明什麼情境該用哪種提示方式
- 給出可直接套用的範例
- 若有不確定處,明確標示
```
👎 不太適合這樣寫:
```text
第一步先分析 A,第二步分析 B,第三步列出所有可能例外,
第四步逐項推理,第五步再得出結論……
```
除非那些步驟真的是任務不可變的流程,否則對比較新的模型來說,這種寫法反而礙手礙腳。
## Claude 提示重點
✨ **你要把需求寫得像一份清楚的工作規格**
Anthropic 明確建議使用清楚、直接、具體的指令,並把 Claude 想成一位「很聰明但剛到職、缺少你們工作脈絡的新員工」;你給越清楚的目標、限制與輸出格式,結果越穩。
Claude 也特別重視範例。官方說範例是最可靠的方式之一,可以引導格式、語氣與結構,並建議範例要貼近實際用例、涵蓋邊界情況,最好用 `` 或 `` 包起來。
👍 適合這樣寫:
```text
比較 OpenAI 與 Claude 的提示引導差異。
一般非 AI 工程背景的使用者。
1. 一句話結論
2. 差異表
3. 使用建議
4. 範例 prompt
- 不要過度技術化
- 不要只說抽象原則
- 每個差異都要說明實際影響
輸入:我要寫一封客戶道歉信
輸出:說明 OpenAI 與 Claude 分別該怎麼下 prompt
```
## 你值得知道的 5 件事
### 不要一套提示詞打天下
🔥 這是最大誤區。
✨ **根據模型特性調整提示結構,才能發揮最大效益**。
OpenAI 和 Anthropic 模型都很強,但吃的提示風格不同。
- 對於 GPT 而言,過度規劃流程可能會降低彈性。
- 對於 Claude 而言,提示太短太抽象可能會讓它猜錯需求。
### OpenAI 要管「結果」,不要管太多「過程」
你應該告訴它:
```text
我要什麼結果?
什麼算成功?
有哪些限制?
最後要用什麼格式?
```
而不是一直告訴它:
```text
你第一步要想什麼,第二步要想什麼,第三步要怎麼想。
```
### Claude 要給「上下文、格式、範例」
你應該告訴它:
```text
任務:
背景:
限制:
輸出格式:
範例:
檢查標準:
```
尤其是團隊要建立可重複使用的提示範本時,Claude 這種規格化寫法會比較具優勢。
### 規劃流程不是不能用,而是用途不同
- 對於 GPT 而言,只有在流程真的重要時才指定步驟,例如稽核、法規檢查、資料清理、測試驗證。
- 對於 Claude 而言,當順序、完整性、輸出一致性重要時,使用有序的方式來規劃流程是合理做法。Anthropic 也建議在順序或完整性重要時,使用條列或編號步驟。
### 兩家都開始重視「控制思考成本」
- OpenAI 文件也提到在把問題升級成「高推理強度(high reasoning effort)」之前,應該先重新檢查低強度(low)和中等強度(medium)的方法是不是已經足夠解決問題。
- Claude 也有推理強度設定,官方提醒**高推理強度可能帶來更高 token 成本**,甚至過度思考;如果複雜任務推理太淺,才提高推理強度或加入針對性提示。
🔥 **在提高推理強度之前,應先釐清目標並用低到中等推理嘗試解決;只有當結果不能滿足需求時,才逐步升級推理層級,而不是讓 AI 一開始就對模糊問題進行高強度推理**。
以模型選擇舉例:
- 在已知的晚餐選項(例如:牛排、沙拉、義大利麵)中選擇今天的菜單,可能只需要低推理強度,甚至是不用思考的 GPT-4.1 就能勝任。
- 在需要分析複雜數據或進行多步推理的任務中(例如:尋找某個區域的所有餐廳評價,以決定最佳用餐地點),可能才需要中等到高推理強度的模型,如 GPT-5.5。
## 結論
**OpenAI 比較像你交代一個成熟助理:「這是目標,幫我做到好。」**
**Claude 比較像你交給新人一份精準規格:「照這些條件產出我要的東西。」**
## 參考
💭 [Prompt guidance | OpenAI API](https://developers.openai.com/api/docs/guides/prompt-guidance?model=gpt-5.5)
💭 [Prompting best practices - Claude API Docs](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices)
---
title: 'E2E 測試不求人,MCP 幫你出張嘴'
description: '運用 Chrome DevTools MCP 協助 E2E 測試'
slug: 'effortless-e2e-mcp'
date: '2026-01-14'
drafted: false
featured: true
topic: 'test'
tags: ['e2e-test', 'mcp']
authors: ['neil-tsai']
---
此篇文章帶大家了解如何運用 **Chrome DevTools MCP 協助 E2E 測試**,有興趣就往下吧!
## 傳統 E2E 測試痛點
- **腳本撰寫繁瑣**:需手動撰寫測試腳本,維護成本高
- **語意不直覺**:測試語句偏技術導向,難以表達真實使用者意圖
- **元素選取脆弱**:DOM 結構稍有變動就會導致測試失敗
- **等待與同步困難**:處理載入、動畫、非同步事件需額外邏輯
- **學習門檻高**:需學習框架語法(如 Cypress、Playwright)與 DOM 操作細節
- **與 AI 不相容**:傳統測試流程無法與 AI Agent 整合,無法語意驅動或即時驗證
## 為什麼傳統 E2E 測試與 AI 不相容?
### 缺乏語意層設計
- 傳統測試框架是**基於程式語法與 DOM 操作,本就不具備語意解析能力**
- AI Agent 是**透過自然語言與使用者互動並依賴語意轉換層將語句轉為具體行為**
- 沒有 MCP 作為橋樑時,AI 就無法理解或執行測試流程
### 缺乏即時回饋管道
- 傳統測試流程是「寫 → 跑 → 看結果」,**無法即時回饋給 AI Agent**
- **AI 無法知道「這個按鈕點了沒」、「頁面跳轉成功沒」,導致無法調整策略**
- MCP 提供了即時觀察與操作能力,讓 AI 能根據真實結果做出反應
### 測試語句不具語意可解性
- 傳統測試語句如 `cy.get(‘.btn-login’).click()` **對 AI 而言是模糊的**
- **AI 無法理解這段程式碼的意圖,也無法推論其對應的使用者行為**
- 若改為「點擊登入按鈕」,AI 就能透過語意解析 + MCP 執行對應操作
### 驗證邏輯無法語意化
- 傳統斷言如 `expect(page.url()).toContain(‘/dashboard’)`,**AI 難以理解其驗證目標**
- 若能語意化為「確認使用者已進入 dashboard」,AI 就能主動去尋找驗證方式
### 缺乏上下文記憶與推理能力
- 傳統測試是**線性執行,不具備上下文推理**
- **AI Agent 需要知道「前一步登入成功 → 下一步才能點選設定」,這需要上下文感知(Context-Aware)的測試流程**
- MCP + LLM 可以提供這種語境推理能力
😫 以上痛點,是不是讓你很不想寫 E2E 測試呢?
😤 **但現在,我們真的可以只出張嘴就能測試了。**
## MCP(Model Context Protocol)
### 簡介
- MCP 是一種開放協定,用來讓大型語言模型(LLM)能夠安全地連接外部工具與資料來源。
- MCP 問世大幅減少了需要一直重複造輪子的情境,以往要讓 AI 可以存取外部資料,你可能需要實作函數調用功能(Function Calling Feature),搞得大家的版本都不太一樣,甚至影響後續的維護作業。

MCP vs API Model Context Protocol Explained
### Chrome DevTools MCP
Chrome DevTools MCP 讓 AI Agent 能夠透過 DevTools 進行真實的網頁操作與除錯。
情境例如:
- **模擬使用者行為**:點擊按鈕、填寫表單、導航頁面。
- **即時驗證程式碼變更**:AI 生成修正後,立即在瀏覽器中驗證效果。
- **分析網路與 console 錯誤**:協助定位 CORS 問題或 JS 錯誤。
- **執行效能追蹤**:分析 [LCP](https://jamespolik.pixnet.net/blog/posts/17346689970)、[CLS](https://jamespolik.pixnet.net/blog/posts/17346689970) 等指標。
### MCP 對自然語言 E2E 測試很重要嗎?
- 傳統 AI Agent 是「盲寫」:只能根據程式碼去預測結果
- MCP 給了 AI 一雙「眼睛」:能夠看到程式碼執行後的真實效果,大幅提升自然語言指令的準確性與可操作性
- 這讓你可以用一句話(例如:「打開首頁並登入」)讓 AI Agent 透過 MCP 操控 DevTools 完成整個 E2E 測試流程
### 技術架構概覽
- **自然語言指令**:使用者以口語或文字輸入操作意圖,例如:「打開首頁並登入」
- **AI Agent 解讀**:LLM 解析語意,理解使用者意圖並規劃操作步驟(例如:導航 → 點擊 → 驗證)
- **MCP 運用**:AI Agent 透過 MCP 與 DevTools MCP Server 溝通,將語意轉換為具體 DevTools 指令,MCP 提供上下文、工具能力、執行環境等資訊,讓 LLM 能安全且準確地操作瀏覽器
- **DevTools 操作**:MCP Server 執行 DevTools API(如 DOM 操作、網頁導航、效能追蹤等),模擬真實使用者行為
- **回傳結果**:DevTools 將操作結果(如頁面狀態、console log、network trace)回傳給 AI Agent,AI 可根據結果進行驗證、調整策略或提出修正建議
上面這幾步,實際上我們只需要關注自然語言指令就好(怎麼出張嘴就好)。
## 體驗環節
✨ 簡單透過 BMI 計算機 E2E 測試,來感受一下語意驅動的魅力!
我先給你一張圖片:

試想看看,我要怎麼描述給 AI 知道我要測試什麼?
你可能會說:「試試 BMI 計算機」(然後它就會開始亂試了...XD)
因為對 AI 來說,「試試 BMI 計算機」這樣的描述太模糊了...
我可能會這樣描述:
1. 打開 BMI 計算機(跟它說位址在哪)
2. 輸入身高 170cm
3. 輸入體重 50kg
4. 點擊計算按鈕
5. 確認結果是否為 17.3
如果你的工具設定是正確的情況下,理論上就會照我說的做了!
**如果這樣的測試可行,那是不是其實省去需要了解一個 E2E 測試框架的時間?...**
再試想一下這張圖片:

畫面部分有蠻大的變化了,這樣還能以同樣的案例測試嗎?
答案是:可以!
👌 雖然畫面變了,但結構都還在,當然就還能測試~
但如果今天把「身高」這個欄位移除,測試就會失敗了,這也會符合我們對於案例的預期!
### 實機操作
以 Gemini CLI 為例:
```bash
npm install -g @google/gemini-cli
git clone https://github.com/cdcd72/bmi-calculator-natural-language-e2e.git
cd bmi-calculator-natural-language-e2e
gemini
```
接著透過 `/auth` 指令登入 Google 後,再使用以下提示詞:
> 使用 Chrome,依照 @TestCases.md 進行測試,並且把結果更新在 @TestResults.md 中,填寫結果欄位就好
🔥 接著你就會看到工具開始進行 E2E 測試囉!
## 案例解析
### 案例變遷
有寫過測試的都知道,我們可能會管理一堆 E2E 測試的文件跟腳本(高機率是分開的)。
但如果可以導入文章這樣的測試,其實最終我們只會管理幾份文件且沒有任何腳本。
### 案例介紹

3 個案例
#### 案例 1
1. 打開 BMI 計算機(跟它說位址在哪)
2. 點擊計算鈕
3. 確認不會顯示任何結果
執行如下:

執行動作:new_page ➡️ take_snapshot ➡️ click
> AI 總要先到那個頁面,由於瀏覽器還沒打開,故它會先打開後,接著拍張照,這樣它就知道要點擊哪個按鈕!
#### 案例 2
1. 打開 BMI 計算機(跟它說位址在哪)
2. 輸入身高 170cm
3. 輸入體重 70kg
4. 點擊計算鈕
5. 確認結果是否為 24.2
執行如下:

執行動作:navigate_page ➡️ take_snapshot ➡️ fill ➡️ fill ➡️ click
> AI 知道瀏覽器已經存在,所以用導頁的方式移動(重置頁面),接著拍張照,這樣它就知道怎麼填寫資料跟點擊哪個按鈕!
#### 案例 3
1. 打開 BMI 計算機(跟它說位址在哪)
2. 輸入身高 170cm
3. 輸入體重 80kg
4. 點擊計算鈕
5. 確認結果是否為 肥胖
執行如下:

執行動作:navigate_page ➡️ fill_form ➡️ take_snapshot ➡️ fill_form ➡️ click
> AI 知道瀏覽器已經存在,所以用導頁的方式移動(重置頁面),只不過這次 AI 犯了個小錯,沒有先拍張照,就執行了填寫資料的動作,這也就是為什麼 `fill_form` 執行了兩次的原因。
### AI 自動校正特性
AI Agent 在執行 MCP 指令失敗時,會根據回傳的上下文資訊進行語意重構與策略調整,主動嘗試自動校正並重新執行操作,這讓語意驅動測試具備高度韌性與自我修復能力。
所以這也就是為什麼案例 3,它明明已經錯了,但還能讓測試繼續執行的原因。
除非這個錯誤是毀滅性的那種,不然原則上都會重試。
### 案例摘要

AI 測試完後,這還不是結束哦!你總是要追蹤吧?

這樣未來 AI 才會知道,上次測試的結果是怎麼樣的,進而能夠做出一些額外分析!
### 補充
- 你在寫測試案例時,雖不需要自己處理錯誤邏輯,**但你寫的語句越清晰、越具意圖性,MCP Server 的回饋就越準確**
- MCP Server 是整個語意驅動測試的防撞框架,負責處理語意模糊、執行錯誤、驗證失敗等狀況,**讓你可以專注在「測什麼」而不是「怎麼測」**
- 總言之,**測試案例的描述會很大程度反應最終的呈現結果,如果你特別關注某一種指標(ex. 測試涵蓋率),那你應該就要有相對應的描述在測試案例中,告訴它應該怎麼做,重點是講清楚**
## 延伸應用與未來展望
- **讓非工程角色也能寫測試**:減少測試撰寫門檻,提升測試覆蓋率與跨職能參與度
- **從「執行者」變成「測試設計師」**:AI 不只是執行語句,而是能主動生成測試案例、根據 UI 結構建議驗證點
- **多語系測試與在地化驗證**:可結合 LLM 的翻譯與語意對齊能力,進行跨語系一致性測試
- **語意驅動的測試自動化**:可搭配 Gemini CLI、Codex 等工具,實現語句驅動的測試流程
- **與設計系統或 UX 檢查整合**:AI Agent 可根據設計規範自動檢查 UI 是否符合預期(ex. 無障礙設計)
- **語言即測試介面**:測試不再是「打開測試框架 → 寫腳本 → 執行」,而是「說一句話 → 得到結果」
## 高能提醒
這類語意驅動測試並不是零成本的方案,但可以透過架構設計與工具選擇來控制花費。
> 我們不一定要把所有測試都語意化,而是挑選最有價值的場景,讓 AI Agent 幫我們省下最難寫、最難維護的那一段。
## 結尾
這樣的測試能不能測試 RWD 情境?
答案是:可以!
那能不能運用在其它情境?
- 驗證使用者登入成功後,是否跳轉至首頁並顯示使用者名稱
- 模擬使用者加入商品至購物車後,驗證購物車內商品與價格是否正確
- 模擬使用者填寫聯絡表單並送出,驗證成功訊息是否顯示
- 切換至英文語系後,驗證首頁標題是否顯示為 "Welcome"
- 點選 FAQ 區塊的問題後,驗證答案是否展開顯示
就等你來揭曉囉~
## 參考
💭 [mcp是什麼?mcp server是什麼?mcp中文、意思、實例一次看懂](https://www.bnext.com.tw/article/82706/what-is-mcp)
💭 [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp)
💭 [用 Chrome DevTools MCP 快速實現 E2E 自動化 UI 測試](https://www.youtube.com/watch?v=0aalWCXDX8o)
💭 [使用 ADO MCP 搭配 Chrome DevTools MCP 享受 E2E 自動化 UI 測試](https://www.youtube.com/watch?v=KQc7nAEl_Gs)
💭 [有了 Chrome DevTools MCP,AI 再次學會了睜眼說瞎話](https://blog.user.today/ai-automation-browser-mcp-tools/)
💭 [Chrome DevTools MCP vs Playwright MCP - どちらを選ぶべき?実測で比較](https://zenn.dev/nexta_/articles/google-chrome-mcp-server)
---
title: 'Builder 建造者模式'
description: '複雜物件建構及樣貌分離'
slug: 'design-pattern-builder'
date: '2020-12-14'
drafted: false
featured: false
topic: 'design-pattern'
tags: ['creational-pattern']
authors: ['neil-tsai']
---
此篇文章簡單帶大家了解 **Builder 建造者模式**,有興趣就往下看吧!
## 目的
以下擷取自 [wikipedia](https://en.wikipedia.org/wiki/Builder_pattern)
> The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation.
建造者模式是軟體設計原則之一,被設計來提供一個彈性解法來**處理創建多樣貌的物件問題**,其目的在於**將複雜物件的建構及樣貌分離**。
## Builder 實務使用路線圖

❗ 文章會專注說明 Route 1 做法,而 Route 2 只需要把 Director 拿掉即可,不再贅述!
## 類別圖

## 程式碼範例
### Director
```typescript
// 旅行社 - Director
export class TravelAgency {
private vocationBuilder: VocationBuilder;
constructor(vocationBuilder: VocationBuilder) {
this.vocationBuilder = vocationBuilder;
}
/**
* 創造假期
* @param {Date} beginDate?
* @param {Date} endDate?
* @param {Hotel} hotel?
* @param {Restaurant} restaurant?
* @returns Vocation
*/
createVocation(beginDate?: Date, endDate?: Date, hotel?: Hotel, restaurant?: Restaurant): Vocation {
return this.vocationBuilder
.setBeginDate(beginDate) // 設定開始日期
.setEndDate(endDate) // 設定結束日期
.setHotel(hotel) // 設定旅館
.setRestaurant(restaurant) // 設定餐廳
.create();
}
}
```
### Builder
```typescript
// 假期建造者 - Builder
export interface VocationBuilder {
/**
* 設定開始日期
* @param {Date} date?
* @returns VocationBuilder
*/
setBeginDate(date?: Date): VocationBuilder;
/**
* 設定結束日期
* @param {Date} date?
* @returns VocationBuilder
*/
setEndDate(date?: Date): VocationBuilder;
/**
* 設定住哪間旅館
* @param {Hotel} hotel?
* @returns VocationBuilder
*/
setHotel(hotel?: Hotel): VocationBuilder;
/**
* 設定吃哪間餐廳
* @param {Restaurant} restaurant?
* @returns VocationBuilder
*/
setRestaurant(restaurant?: Restaurant): VocationBuilder;
/**
* 讓使用者能取得假期物件
* @returns Vocation
*/
create(): Vocation;
}
// 七天假期建造者 - ConcreteBuilder
export class SevenDaysVocationBuilder implements VocationBuilder {
// 每天經過毫秒數
private PER_DAY_MILLISECONDS: number = 1000 * 60 * 60 * 24;
private beginDate?: Date;
private endDate?: Date;
private hotel?: Hotel;
private restaurant?: Restaurant;
setBeginDate(date?: Date): VocationBuilder {
this.beginDate = date;
return this;
}
setEndDate(date?: Date): VocationBuilder {
// ... 需滿足假期 7 天相關計算邏輯 ...
if(this.beginDate !== undefined && date !== undefined) {
if(this.diffDays(this.beginDate, date) === 7) {
// 日期剛好差 7 天
this.endDate = date;
} else {
// 日期可能小於或大於 7 天,則依據開始日期 + 7 天
this.endDate = new Date(this.beginDate.getTime() + (7 * this.PER_DAY_MILLISECONDS))
}
}
// ...
return this;
}
setHotel(hotel?: Hotel): VocationBuilder {
this.hotel = hotel;
return this;
}
setRestaurant(restaurant?: Restaurant): VocationBuilder {
this.restaurant = restaurant;
return this;
}
create(): Vocation {
// 若無設定結束日期,則依據開始日期 + 7 天
if(this.beginDate !== undefined && this.endDate === undefined)
this.endDate = new Date(this.beginDate.getTime() + (7 * this.PER_DAY_MILLISECONDS));
return new Vocation(this.beginDate, this.endDate, this.hotel, this.restaurant);
}
/**
* 計算差異天數
* @param {Date} date1
* @param {Date} date2
* @returns number
*/
private diffDays(date1: Date, date2: Date): number {
return Math.abs(date1.getTime() - date2.getTime()) / this.PER_DAY_MILLISECONDS;
}
}
```
### Model
```typescript
// 假期 - Product
export class Vocation {
private beginDate?: Date;
private endDate?: Date;
private hotel?: Hotel;
private restaurant?: Restaurant;
constructor(beginDate?: Date, endDate?: Date, hotel?: Hotel, restaurant?: Restaurant) {
this.beginDate = beginDate;
this.endDate = endDate;
this.hotel = hotel;
this.restaurant = restaurant;
}
/**
* 取得開始日期
* @returns Date
*/
getBeginDate(): Date | undefined {
return this.beginDate;
}
/**
* 取得結束日期
* @returns Date
*/
getEndDate(): Date | undefined {
return this.endDate;
}
/**
* 取得旅館
* @returns Hotel
*/
getHotel(): Hotel | undefined {
return this.hotel;
}
/**
* 取得餐廳
* @returns Restaurant
*/
getRestaurant(): Restaurant | undefined {
return this.restaurant;
}
}
```
```typescript
// 旅館
export class Hotel {
private name: string;
constructor(name: string) {
this.name = name;
}
/**
* 取得名稱
* @returns string
*/
getName(): string {
return this.name;
}
// ...
}
```
```typescript
// 餐廳
export class Restaurant {
private name: string;
constructor(name: string) {
this.name = name;
}
/**
* 取得名稱
* @returns string
*/
getName(): string {
return this.name;
}
// ...
}
```
詳細可參考[範例程式碼](https://github.com/cdcd72/DesignPatterns/tree/master/Creational/Builder)···
## Builder 使用時機
1. **當你/妳今天想要創造同一物件,但它可以有不同樣貌時** (可以想像每個人可規劃不同的假期,但最終都是假期這件事···)
2. **想要簡化創造物件的複雜程度** (每個人可規劃不同假期,但總不可能一開始就把條件都給好,那假設有 100 種可能?所以我需要寫能滿足這 100 種可能的建構子?)
## 好處
1. 可以一步一步依照需求來建構不同樣貌的相同物件
2. Builder 顧名思義,只負責建構你/妳所需的物件,所以可以跟你/妳的業務邏輯有所區隔 (符合[單一職責原則](/posts/oo-single-responsibility-principle))
## 壞處
1. 增加可維護性的代價,你/妳懂得 😅 ··· (你/妳可能會有許多 Builder,若更嚴謹點再把 Director 加進來,類別數量上升了,數量變多管理上就會變得不容易,更何況是多人協作情況···)
## 補充
1. 整個複雜物件的建構過程被封裝起來,所以客戶端無法影響物件建立的步驟 (物件建立這件事對客戶端是隱藏的)
2. 對於客戶端而言,允許用多個步驟來建立物件,但要是不知道有哪些方法可以用,可能也無法建立出想要的物件
3. 對其下生成的複雜物件之間會有部分 (is-part-of) 關係,意指藉由多個部分才算一個整體 (假期中沒有旅館可能就怪怪的···)
4. Director 實際扮演的角色為指揮 Builder 如何建構物件,但多數實務上會把 Director 省略掉,讓客戶端直接控制其建構步驟
## 結論
眼尖的朋友可能會注意到 Builder 介面定義的方法均是回傳介面自己,或許這不是很純正的 Builder Pattern,但這樣寫是有好處的,讓我娓娓道來 😏 ···
其實 Builder Pattern 可以結合 Fluent Interface 做到更進階的用法,詳細可以參考我之前寫的「[Fluent Interface|一種程式碼”寫作”風格](/posts/coding-style-of-fluent-interface)」哦!
看完 Builder Pattern 之後醒悟不少,了解到如果需要不同樣貌的相同物件,可以不需要透過補上建構子來達成這件事,往往實務在看一個類別的建構子,太多選擇反而會不知道怎麼用這個類別,不然就是一定要給不太需要的參數,程式可讀性就變差了 😩 ···
而套用 Builder Pattern 甚至可以把一些建構方式條件化或者是推遲建構的時間,種種這些皆變成了可能,更彈性的建構物件,善用 Builder Pattern 吧!
## 參考
💭 [Builder](https://refactoring.guru/design-patterns/builder)
---
title: '如何移除 Visual Studio 2019 已授權金鑰'
description: '改邪歸正,從你我做起'
slug: 'how-to-remove-visual-studio-2019-license'
date: '2020-10-08'
drafted: false
featured: false
topic: 'visual-studio'
tags: ['ide']
authors: ['neil-tsai']
---
此篇文章主要帶大家**以不重灌系統前提下讓 Visual Studio 2019 可以重新輸入授權金鑰**,有興趣就往下看吧!
## 前言
你/妳一定會覺得奇怪,怎麼會有人有文章標題的需求呢?**就還真的是有 😂 ···**
我們先假設一種情形:
1. 身分是工程師
2. 未特別向公司申請開發工具授權,而是隨意使用網路上存在的任意授權(不用說得很明白吧 😂)
3. 公司臨時要外部稽核
如果碰到上述情況,就變成需要配合公司政策走,也就是需要向公司申請開發工具授權···等等(正常來說也應該都是用公司授權,如果公司有買的話)···
❗ **但我這邊要鄭重說明,我並不是鼓勵大家去使用網路上的任意授權,如果有能力的話,還是盡量都走正規管道取得授權並去使用!**
所以寫這篇文章,主要是幫助那些曾經走歪路的,可以導回正軌哦 😂 ···
## 前情提要

相信大家看到上圖,應該都不陌生,就是當你安裝完 Visual Studio 後會有的試用期啦···
然後當你選擇**使用產品金鑰解除鎖定**時,就會**寫授權進入註冊表中**,所以**之後你幾乎無法透過已有介面來重新輸入授權**,等到需要轉換授權時,就會很頭痛 😩 ···

所以可能就要變相使用一些手段來改變授權碼,這邊提供一個可以用指令換掉授權的可能方式給大家參考,但跟我今天要說明的倒是沒太大關係···
💭 [Automatically apply product keys when deploying Visual Studio](https://docs.microsoft.com/en-us/visualstudio/install/automatically-apply-product-keys-when-deploying-visual-studio?view=vs-2019)
## 實際做法
### 不重灌作業系統前提下
為了 Visual Studio 2019 可以重新輸入授權,而要重灌作業系統,怎麼想都覺得累 😩 ···
那接下來就一步一步跟著做吧!
1. `Windows` 搜尋 `regedit`

2. 搜尋 `HKEY_CLASSES_ROOT\Licenses\41717607-F34E-432C-A138-A3CFD7E25CDA`

3. 刪除它

4. 打開 `Visual Studio Installer` 找到你的 `Visual Studio 2019` 並**修復(repair)**它

5. 待修復完畢後,打開 `Visual Studio 2019` 應該又能**重新手動輸入授權**了哦

❗ 不過刪除之前,建議可以先把 `HKEY_CLASSES_ROOT\Licenses\41717607-F34E-432C-A138-A3CFD7E25CDA` 備份起來,養成備份的好習慣···

### 重灌系統前提下
就···是重灌系統,沒什麼好說的···
但工程師應該會心力交瘁,畢竟要把那些開發工具、環境都要重新搞一遍,**這做法不實際,但這也是最後手段 😂 ···**
## 參考
💭 [How do I remove a license from Visual Studio 2019?](https://social.msdn.microsoft.com/Forums/en-US/3371f1fc-1724-4005-ae47-8497c06aa739/how-do-i-remove-a-license-from-visual-studio-2019?forum=vssetup)
💭 [How to remove product key form visual studio 2017 professional .](https://social.msdn.microsoft.com/Forums/vstudio/en-US/18e79979-806b-4f93-a063-a30b5125f868/how-to-remove-product-key-form-visual-studio-2017-professional-?forum=visualstudiogeneral)
---
title: '探討 .NET HtmlEncode 解決方案之間的比較'
description: '多方比較後,總結出最適合的 HtmlEncode 方案'
slug: 'discuss-dotnet-htmlencode-solutions-and-compare'
date: '2020-07-25'
drafted: false
featured: false
topic: 'net'
tags: ['xss', 'html-encode']
authors: ['neil-tsai']
---
此篇文章主要帶大家**認識 .NET 各種 HtmlEncode 解決方案之間的比較**,有興趣就往下看吧!
## 前言
一般來說,為了避免網站應用程式被 [Cross-Site Scripting(XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting),我們都需要特別小心處理 HTML 裡面的內容,比較常見的手法會在 HTML 寫上 ``,而當使用者載入該頁面時,就會直接執行此段惡意程式碼,嚴重甚至導致自己的敏感資料外洩都有可能···
那有沒有辦法預防呢?有的,大部分解決方案會傾向透過 HtmlEncode 將 HTML 文件中不允許出現的字元進行編碼,通常會編碼 `<`、`>`、`&` 等字元,透過這樣子的處理可以大幅下降載入網頁時被執行惡意程式碼的可能性!
## 案例

AntiXSS issue
最近在幫忙公司一個案子解有資安疑慮(XSS)的問題,而源碼檢測有找到一個中風險(Medium)的疑慮,就是該案子有使用到 [Microsoft Anti-Cross Site Scripting(AntiXSS)](https://www.nuget.org/packages/AntiXSS/)套件,源碼檢測報告中的描述如下:
> The Microsoft Anti-Cross Site Scripting (AntiXSS) Library 3.x and 4.0 does not properly evaluate characters after the detection of a Cascading Style Sheets (CSS) escaped character, which allows remote attackers to conduct cross-site scripting (XSS) attacks via HTML input, aka “AntiXSS Library Bypass Vulnerability.”
>
> From White Source Report
去查了一下這個套件最新釋出的版本是 4.3.0(最後更新日期為 2014/6/2),而且擁有者還是微軟 😂 ···
雖然報告中是描述 4.2.1 版本才有中風險疑慮,但**公司上層的決定比較偏向是需要根除專案內和此套件有依賴關係的程式碼**,因為確實專案使用此套件目的也只有需要 HtmlEncode 而已,所以只要找到一個合適的替代方案,就能讓專案和此套件解耦,站在後端工程師的角度,我也覺得是有必要的!
💥 但合適的替代方案,需要考量哪些因素呢 🤔?
這邊我會站在後端工程師的角度來帶你/妳看,為什麼我會這樣去選擇?
在那之前我們需要先評估一些現有的解決方案讓大家知道~
## 評估 HtmlEncode 各解決方案
.NET HtmlEncode 現有的一些解決方案可能如下幾個:
1. Encoder.HtmlEncode(AntiXSS)
2. Server.HtmlEncode
3. AntiXssEncoder.HtmlEncode
4. HttpUtility.HtmlEncode
**上述這些都可以解決你/妳 HtmlEncode 的需求,但若要以可長期維護來看,可能就要慎選了···**
## 分析 HtmlEncode 各解決方案可否長期維護性
這邊會依據上述提到的那些解決方案,逐一討論並做出一個合適的抉擇···
### Encoder.HtmlEncode(AntiXSS)
- 命名空間 (Namespace) : `Microsoft.Security.Application`
- 組件 (Assembly) : `AntiXssLibrary.dll`
- 範例 (Example) : `Encoder.HtmlEncode("")`
- 支援 (Support) : `沒特別相依在哪一個 .NET 框架上`
- 評語 : 雖然**沒有特別相依在哪一個 .NET 框架是一件好事**,但文章開頭就有說明**使用這個套件有一定風險存在**,而且多數客戶開始漸漸有意識到資訊安全的重要性,所以網站應用程式被弱掃的可能性很高,**等到被掃出來後再修改程式,倒不如一開始就不要裝,而且還可以減少對套件的依賴**,省時省錢又省力呢 😂 ···
### Server.HtmlEncode
- 命名空間 (Namespace) : `System.Web`
- 組件 (Assembly) : `System.Web.dll`
- 範例 (Example) : `Server.HtmlEncode("")`
- 支援 (Support) : `.NET Framework 4.8 ~ 3.5`
- 評語 : **早期 .NET Framework 網站應用程式的首選**,因 System.Web.Mvc 命名空間下的 Controller 類別有開出 Server 這一屬性(型態為 HttpServerUtilityBase),所以**透過繼承這個 Controller 類別的 MVC Controller 就可以直接使用 Server.HtmlEncode!**但**假設你/妳的網站應用程式未來有升版為 .NET 或 .NET Core 的打算,就不建議還在 .NET Framework 的時期就使用它**,因為未來你/妳還是需要找一個替代方案(ex. HttpUtility.HtmlEncode)😂 ···
( PS. 可能有人會問在 System.Web.Http 命名空間下的 ApiController 為什麼沒辦法直接用 Server.HtmlEncode 呢?因為其實它就沒特別開 Server 這一屬性,起初我也以為 Server 這一屬性是 Controller 基本必備的,但後來看了一下開出的屬性跟方法,發現原來還是有差異存在··· )
### AntiXssEncoder.HtmlEncode
- 命名空間 (Namespace) : `System.Web.Security.AntiXss`
- 組件 (Assembly) : `System.Web.dll`
- 範例 (Example) : `AntiXssEncoder.HtmlEncode("", false)`
- 支援 (Support) : `.NET Framework 4.8 ~ 4.5`
- 評語 : **.NET Framework 4.5 之後額外的新選擇**,如果不特別去使用 Server.HtmlEncode 的話,但一樣**假設你/妳的網站應用程式未來有升版為 .NET 或 .NET Core 的打算,就不建議還在 .NET Framework 的時期就使用它**,因為未來你/妳還是需要找一個替代方案(ex. HttpUtility.HtmlEncode)😂 ···
## HttpUtility.HtmlEncode
- 命名空間 (Namespace) : `System.Web`
- 組件 (Assembly) : `System.Web.HttpUtility.dll`
- 範例 (Example) : `HttpUtility.HtmlEncode("")`
- 支援 (Support) : `.NET 5` `.NET Core 3.1 ~ 2.0` `.NET Framework 4.8 ~ 1.1` `.NET Standard 2.1 ~ 2.0`
- 評語 : 前面講那麼多,**最後就是來說服你/妳改使用 HttpUtility.HtmlEncode**,不得不說這個我也是最近才知道,知道得有點晚 Orz,總歸來說**它的優點就在於移植到新框架時,可以幾乎無痛升級**,也就是幾乎不用改你/妳當初寫的那些 HtmlEncode 程式碼,在新框架中它還是維持原樣,我想這是工程師樂見的!
## 總結
若你/妳的網站專案還在 `.NET Framework` 時期,建議以下做法:
1. 若**有使用到 AntiXSS 套件,建議不再使用它**(因為已多年無維護),更何況是有不依賴套件的做法存在,我們也不希望因為套件,讓我們的程式綁手綁腳,狠下心斷捨離就對了 😤 ···
2. 考量到**未來框架移植的可能**,將 Server.HtmlEncode & AntiXssEncoder.HtmlEncode 的寫法**汰換**為 HttpUtility.HtmlEncode 的寫法,未來移植時就可以少痛一點點 👍 ···
若你/妳的網站專案還在 `.NET Core` 時期,建議以下做法:
1. 若**有使用到 AntiXSS 套件,建議不再使用它**(因為已多年無維護),更何況是有不依賴套件的做法存在,我們也不希望因為套件,讓我們的程式綁手綁腳,狠下心斷捨離就對了 😤 ···
2. 相較於 .NET Framework 網站專案,.NET Core 本來就沒有 Server.HtmlEncode & AntiXssEncoder.HtmlEncode 的寫法,所以**如果不是特別使用坊間的一些套件,就建議一律改用 HttpUtility.HtmlEncode 的寫法**,未來移植時就可以少痛一點點 👍 ···
## 參考
💭 [AntiXSS](https://www.nuget.org/packages/AntiXSS/)
💭 [HttpServerUtilityBase](https://docs.microsoft.com/zh-tw/dotnet/api/system.web.httpserverutilitybase?view=netframework-4.8)
💭 [AntiXssEncoder](https://docs.microsoft.com/zh-tw/dotnet/api/system.web.security.antixss.antixssencoder?view=netframework-4.8)
💭 [HttpUtility](https://docs.microsoft.com/zh-tw/dotnet/api/system.web.httputility?view=netcore-3.1)
---
title: '如何透過 System.DirectoryServices 來確認 Windows 使用者是否為啟用狀態!?'
description: '初步了解 System.DirectoryServices'
slug: 'how-to-use-adsi-confirm-windows-users-is-actived'
date: '2020-06-25'
drafted: false
featured: false
topic: 'net-standard'
tags: ['active-directory', 'adsi', 'winnt']
authors: ['neil-tsai']
---
此篇文章主要帶大家**利用 .NET 透過 System.DirectoryServices 套件來確認 Windows 使用者是否為啟用狀態**,有興趣就往下看吧!
## 前言
最近在公司處理到一張蠻特別的單子,需求大致上是需要去**尋覽出客戶機器上那些已被啟用的 Windows 使用者,然後取得那些使用者的資訊(ex. 使用者名稱)去做一些後續處理**,於是乎我在茫茫網路海中找到了一個普遍的做法,但我實在有點好奇它的運作方式,便促使了這篇文章的誕生,希望也可以幫助到你/妳···
## 範例
### 大致步驟
1. 透過 `nuget` 取得 `System.DirectoryServices` 套件
2. 透過 `System.DirectoryServices` 操作 `Active Directory Service Interface(ADSI)`
### 程式碼
```csharp
// WindowsOperateHelper.cs
using ActiveDirectoryOperateCore.Enum;
using System;
using System.Collections.Generic;
using System.DirectoryServices;
// 略...
// Account disable flag
private const int ACCOUNTDISABLE_FLAG = (int)AdsUserFlag.ADS_UF_ACCOUNTDISABLE;
///
/// Get actived windows user directory entries
///
///
public static List GetWindowsActiveUserDirectoryEntries()
{
List directoryEntries = new List();
// Get local machine directory entry
DirectoryEntry localMachineDirectoryEntry = new DirectoryEntry("WinNT://" + Environment.MachineName);
foreach (DirectoryEntry childDirectoryEntry in localMachineDirectoryEntry.Children)
{
// Focus user directory entry
if (childDirectoryEntry.SchemaClassName == "User")
{
var userflags = (int)childDirectoryEntry.Properties["UserFlags"].Value;
// If userflags bitwise and accountDisableFlag equal two, mean this windows user state is inactive
if ((userflags & ACCOUNTDISABLE_FLAG) != ACCOUNTDISABLE_FLAG)
{
directoryEntries.Add(childDirectoryEntry);
}
}
}
return directoryEntries;
}
```
```csharp
// Program.cs
using ActiveDirectoryOperateCore.Helper;
using System;
using System.Linq;
// 略...
///
/// Display actived windows user names
///
private static void DisplayWindowsActiveUserNames()
{
// Get actived windows user names
var windowsActiveUserNames =
WindowsOperateHelper.GetWindowsActiveUserDirectoryEntries()
.Select(windowsActiveUserDirectoryEntry => windowsActiveUserDirectoryEntry.Name);
foreach (var windowsActiveUserName in windowsActiveUserNames)
{
Console.WriteLine(windowsActiveUserName);
}
}
```
🤔 雖然參考了他人的程式碼,但我這邊還是盡可能改成我覺得比較好維護的方式供各位參考!
1. 將 ADS_USER_FLAG 抽去 Enum
2. 將 WindowsOperateHelper 抽去 .NET Standard 專案(可以兼容 .NET Framework 和 .NET Core 應用)
[範例專案](https://github.com/cdcd72/ActiveDirectoryOperations.Demo)可參考···
### 參考來源
💭 [Get a list of active local users with .NET](http://blog.bitcollectors.com/adam/2011/10/get-a-list-of-active-local-users/)
💭 [ADS_USER_FLAG_ENUM enumeration](https://docs.microsoft.com/en-us/windows/win32/api/iads/ne-iads-ads_user_flag_enum)
## 了解 Active Directory Service Interface
看完上面的範例程式碼後,是不是開始好奇為什麼透過 System.DirectoryServices 可以取得 Active Directory 的資訊呢 🤔!?
但在那之前,還需要先了解 **Active Directory Service Interface(ADSI)**是什麼?為什麼我們需要透過 ADSI 來操作 Active Directory?
> Active Directory Service Interface(ADSI)是一組基於 COM 技術上的應用程式開發介面,程式開發人員可以利用這些介面來連接並存取 Active Directory 執行查詢,更新或刪除等管理功能,ADSI 同時可支援以 LDAP(輕量級目錄存取協定)為主的目錄服務(例如 Novell Directory Service),以及以 Windows NT 網域為主所組成的 WinNT 網域目錄。
>
> [Active Directory Service Interface from wikipedia](https://zh.wikipedia.org/wiki/Active_Directory_Service_Interface)
基本 wiki 的描述幾乎已經解答完什麼是 ADSI 了,所以我就不再多描述了,建議如果想要深入了解的人,一定要看看 wiki 的完整內容 😜!
## 了解 System.DirectoryServices
了解 ADSI 後,可能你/妳就清楚為什麼當初 .NET Framework 另外提供了一顆專門操作 ADSI 的元件(元件目前已經有支援 .NET Standard 2.0),大概可以知道他們有以下幾點考量:
1. 尽可能簡化開發人員操作 ADSI 的難度
2. 有一點 Facade 的味道,讓原本不易理解的原生介面(Native interfaces)換上新外觀(多墊一層)
3. 因這些原生介面被 System.DirectoryServices 封裝關係,所以可以減少應用程式對於 ADSI 這些原生介面的依賴程度
💭 [System.DirectoryServices](https://www.nuget.org/packages/System.DirectoryServices/)
## Userflags 和 ADS_USER_FLAG 的關係
啊啊!拉回來探討主題,不小心扯到太遠了,好奇心真可怕 😜
❓ **那為什麼利用 Userflags 和 ADS_USER_FLAG 就有辦法確認 Windows 使用者的啟用狀態呢!?**
先稍微回顧一下當初下判斷的地方,如下:
```csharp
var userflags = (int)childDirectoryEntry.Properties["UserFlags"].Value;
// If userflags bitwise and accountDisableFlag equal two, mean this windows user state is inactive
if ((userflags & ACCOUNTDISABLE_FLAG) != ACCOUNTDISABLE_FLAG)
{
// 略..
}
```
這邊會稍微逐步去解析為什麼需要下這樣的判斷,先了解下表意義:

Windows user state is inactive
假如今天把 Userflags 和 ACCOUNTDISABLE_FLAG 做 & 運算(bitwise and)後得出的結果為 2 的話,剛好等於 ADS_UF_ACCOUNTDISABLE 所定義的結果,所以我們可以認為該名 Windows 使用者目前為停用狀態···

Windows user state is active
假如今天這名使用者的 Userflags 變為 66081(十進位)時,則代表這名使用者的狀態已被更改為啟用狀態!
···
Userflags 加上 2 對於二進位來說,從右往左數第二位如果是 1 的話就會往前進位,接著只要與 2 做 & 運算一定就會為 0,當初我也是想破頭,才知道為什麼要這麼做···(要是被大學老師知道,肯定被宰 😂···
🌟 **總結來說,其它 ADS_USER_FLAG 也可以用相同方式來做判斷!**
❗ **另外要注意的是 Userflags 是系統會控制,請以它為基準去判斷,若有人為加工情形,就有誤判的可能 🤔···**
## 額外參考
💭 [Active Directory](https://zh.wikipedia.org/wiki/Active_Directory)
💭 [How to use the UserAccountControl flags to manipulate user account properties](https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties)
## 結尾
感謝各位花時間看完此篇文章,如果本文中有描述錯誤,還請各位指教。
從需求的理解到實務上的處理,發現真的是一個坑比一個坑大,雖然今天介紹的部分我自認為沒有算非常詳細,但應該可以幫助到你/妳去了解實務上大概要如何去操作 Active Directory!
System.DirectoryServices 也已經有支援 .NET Standard,所以不管是現在還在開發 .NET Framework 或者是 .NET Core 應用程式的工程師,都可以善加利用 .NET Standard 的特性來幫助自己寫出更好維護的專案結構!
---
title: '快速理解為什麼程式需要單元測試?'
description: '單元測試的重要性'
slug: 'why-program-needs-unit-testing'
date: '2020-05-19'
drafted: false
featured: false
topic: 'test'
tags: ['unit-test']
authors: ['neil-tsai']
---
此篇文章主要帶大家**快速理解為什麼程式需要單元測試**,有興趣就往下看吧!
請先試想下列幾個問題:
1. 我們會因使用者需求,寫出可以跑的程式就好…
2. 某天來了一個新需求,這個需求會改到部分已經原有的程式,比較好的做法可能是以不影響原本程式邏輯的狀況下來擴充程式…
3. 時代在變,做法也在變,好想重構程式碼…
4. 我的程式是不是有幾行沒用到?
你/妳可能會這樣回答:
1. 這不是廢話嗎?時間就是金錢阿!
2. 如果修改程式會破壞原本的邏輯,我請你/妳來幹嘛 😂?
3. 沒有一個工程師不願意重構吧!?但礙於時程壓力,往往沒有重構的時間,懂嗎 😂?
4. 在時限內完成需求,就是完成任務啦!接著就開始下一個開發循環囉…
我的見解:
1. 若以時程考量來看,在時程內完成可以跑的程式我覺得沒什麼大問題,但我會建議一定還要寫單元測試出來!
2. 大多數情況下,不可能沒有新需求,客戶是貪婪的 😂!工程師為了完成需求,盡可能要在維持開放封閉原則情況下來修改程式…
3. 呼應第 1 點,若有寫完可以跑的程式搭配上幾個單元測試案例,之後就有時間可以重構!
4. 呼應第 3 點,在重構程式碼時,就可以找到一些程式碼異味(Code Smell)並處理!
上面講這麼多就是為了將各位帶入單元測試的**好**究竟在哪裡?總算要進入主題啦 😜
# 單元測試
## 介紹
👉 其實單元測試你/妳可以把它想像成走路線小遊戲…
**當今天有一個要求(Request)打了某一個 Web API,根據需求所撰寫的程式,是否有辦法走完每一條路線(使用案例)呢?**
下圖所示為程式碼涵蓋率 100% 的情況(代表每一路線均有走完):

Code coverage 100%
而當今天因為新需求,而有了新路線時,如下圖:

Code coverage 75%
❗ **D 路線是因需求變化而產生,因為 D 修改的程式對於 A、B、C 來說則是不穩定的因素,需要特別留意…**
寫完單元測試後才漸漸明白它可以幫我們快速做[回歸測試(Regression Testing)](https://kkboxsqa.wordpress.com/2014/02/27/%E5%9B%9E%E6%AD%B8%E6%B8%AC%E8%A9%A6-regression-testing/),D 測試寫出來,直接一併測 A、B、C、D,就能知道 D 的修改是否會影響 A、B、C 哦!
## 好處
- 改 A 壞 B 馬上現形
- 縮短開發者回歸測試時間
- 程式回傳變得可預期
- 程式愈符合物件導向設計原則
- 容易快速重構程式碼
- 排除過度設計的程式碼
- 降低程式過度耦合
## 難處
- 對於一個**未曾有過撰寫單元測試經驗的開發者**來說,**撰寫單元測試所耗用的時間可能會大於實際撰寫程式碼的時間**,因為思維會變成「撰寫可測試程式的測試程式」,所以多數人會選擇放棄撰寫單元測試…
- 需先判斷**是否有寫單元測試的價值才去寫**,因為對於**需求不確定、價值性不高去寫反而會浪費時間**,因為**需求變動,勢必單元測試程式也會變動**
## 實例探討
### 改變前
以檔案下載(FileDownload)這支公開(Public)方法來看,你/妳可能會這麼寫:

FileDownload before
若要嘗試測試該方法,需要先意識到幾件事:
1. 以程式碼最小單位來進行測試
- DownloadInit、getFilePath、FileLocalDownload、download 這些私有(Private)方法均屬於構成檔案下載行為的一部分
2. 找出外部影響(需要 [Mock](https://en.wikipedia.org/wiki/Mock_object) 的對象,因為不希望外部影響是不可控的)
- System.Configuration.ConfigurationManager.AppSettings
- CMFile
⭐ 了解這幾件事之後,接著就是把這些不可控的程式變得可預期,但怎麼變?
1. System.Configuration.ConfigurationManager.AppSettings
- 想辦法讓它不去讀設定檔,而是透過執行測試程式途中去替換(方法很多)
2. CMFile
- 非常不建議直接創建(new)實例,這樣寫就表示 CMFile 一旦有變化,FileDownload 該方法會變得非常不穩定(間接來說就是 FileService 和 CMFile 有耦合關係)
### 改變後

Rely on IRemoteFileService

FileDownload after
⭐ 經過調整後將 FileService 和 CMFile 解開了耦合,但怎麼解的?
透過定義一層新介面(IRemoteFileService)並實作它,將所有有關 CMFile 的實作集結於此,接著透過依賴注入(Dependency injection)來幫助我們產生實例 ,回過頭來看 FileDownload 居然變得可以依照預期來測試!
你可能會納悶為什麼有這樣的效果?**其實關鍵就是解開耦合,一旦程式耦合嚴重,基本上程式是沒有辦法寫單元測試出來,能夠寫出單元測試的程式,表示愈符合物件導向設計原則!**
### 從案例看依賴反轉原則(DIP)

Rely on concrete class

Rely on interface
若想知道更多依賴反轉原則概念的話,可參考我寫的「[依賴反轉原則 DIP|夾心餅乾原則!?](/posts/oo-dependency-inversion-principle)」…
### 失敗原因
單元測試基本構成如下圖:

所以可以從以下幾點去找出問題所在:
1. 預期的輸入資料就有問題
2. 因需求變動的被測試程式並不符合測試程式預期
3. 調整完因需求變動的被測試程式,但未一併調整測試程式
## 開發流程衍生探討
### Traditional Development

Traditional development
🔖 圖片出處:《**The Art of Unit Testing: with examples in C# Second Edition**》
傳統開發流程沒什麼好說的,非常直覺…
寫完程式 ➡️ 整合測試 ➡️ 發現問題 ➡️ 修正問題 ➡️ 整合測試 ➡️ … Loop
理想情況下,程式會越修越完善,但非常花費時間成本(短期其實看不出來)…
### Test-Driven Development, TDD

Test-Driven Development

Test-Driven Development
🔖 圖片出處:《**The Art of Unit Testing: with examples in C# Second Edition**》
測試驅動開發流程和傳統開發流程相比,差異可就大了!
直接先強制你/妳撰寫單元測試程式,然後依據該測試程式的預期來完成真正的程式,懵了吧 😂
❗ **但為什麼可行?回想一下單元測試可以幫助開發者達成什麼事?就是回歸測試!**
假設我要寫檔案上傳這一功能,可能的測試案例有以下這些:
1. 檔案大小限制
2. 檔案有無加密
3. 檔案上傳
4. ...
**寫好這些測試案例**後,接下來就是要**想辦法完成每一個測試案例的預期!**
⭐ 當這些預期被滿足後,很神奇地你/妳會發現寫的程式會符合以下幾點:
1. 程式碼很大程度均被涵蓋(程式涵蓋率)
2. 幾乎不會出現過度設計的狀況
3. 避免掉偏離物件導向設計原則(降低程式耦合)
神不神奇?雖然你/妳聽我這樣講可能沒什麼感覺,但以結果論來看,我認為測試驅動開發流程幾乎屌打傳統開發流程…
因為 TDD 把**寫測試這件事變成了前提**並且**很好的控制了程式短時間的擴展問題**(開發者總是思考的是程式彈性,但往往 API 介面都開得太多,剛好 TDD 可以抑制這種情況發生,把介面變動程度降低到最小)!
❗ 但老實說 TDD 並不是可以容易施行的開發流程,能不能透過一些工具來輔助與團隊成員之間的配合,這些都是比較關鍵的因素,需要特別留意!
若未來有機會迅速在專案導入 TDD,會在將經驗分享出來 XD…
先留一個洞給自己做準備~
## 參考
💭 [單元測試為什麼會這麼重要─成為優秀的軟體工程師](https://lakesd6531.pixnet.net/blog/post/348346657-%e5%96%ae%e5%85%83%e6%b8%ac%e8%a9%a6%e7%82%ba%e4%bb%80%e9%ba%bc%e6%9c%83%e9%80%99%e9%ba%bc%e9%87%8d%e8%a6%81%e2%94%80%e6%88%90%e7%82%ba%e5%84%aa%e7%a7%80%e7%9a%84%e8%bb%9f%e9%ab%94)
💭 [一次搞懂單元測試、整合測試、端對端測試之間的差異](https://blog.miniasp.com/post/2019/02/18/Unit-testing-Integration-testing-e2e-testing)
💭 [自動軟體測試、TDD 與 BDD](https://medium.com/@yurenju/%E8%87%AA%E5%8B%95%E8%BB%9F%E9%AB%94%E6%B8%AC%E8%A9%A6-tdd-%E8%88%87-bdd-464519672ac5)
---
title: '如何在 .NET Core 運用 Data Protection API 來保護敏感資料?'
description: '運用 Data Protection API 來保護敏感資料'
slug: 'how-to-protect-sensitive-data-with-data-protection-api-in-netcore'
date: '2020-05-12'
drafted: false
featured: false
topic: 'net-core'
tags: ['crypto', 'data-protection-api']
authors: ['neil-tsai']
---
此篇文章主要帶大家**利用 Data Protection API 實作資料保護機制於 .Net Core 3.1** 上,有興趣就往下看吧!
❗ **保護敏感資料不一定要用 Data Protection API**,但 .NET Core 是有提供這一方案給各位參考的,使用上也非常單純!
## 資料保護 Data Protection API
[DPAPI](https://en.wikipedia.org/wiki/Data_Protection_API)(Data Protection API)是一個單純的密碼學應用程式介面,最早出現在 Windows 2000,其後的 Windows 作業系統也都有其影子存在···
DPAPI 理論上可以對任何種類的資料進行對稱式加密(Symmetric Encryption)···
在 Windows 作業系統中主要被用來將非對稱私鑰做對稱式加密,而其中運用使用者或系統的秘密(Secret)作為熵(Entropy)的重要來源···
❗ **DPAPI 本身不會自己儲存持久性資料,它只將明文轉換為密文(反之亦然)···**
💭 [Entropy (information theory)](https://en.wikipedia.org/wiki/Entropy_(information_theory))
## .NET Core Data Protection
其實在開發網頁應用時常會碰到需要存取敏感的安全數據(例如:客戶的識別碼),但 Windows 本就提供的 DPAPI 只適合給桌上應用(Desktop Application)並不適合給網頁應用來使用···
所以在經過一段時間淬鍊下 .NET Core Data Protection 就誕生了,其實官方他們也想了很多,才把它實現出來···
- 問題陳述
- 設計理念
- API 受眾
- 套件佈局
👍 有興趣真的可以看看下方連結!一個元件的誕生真的要考慮很多···
💭 [ASP.NET Core Data Protection](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-3.1)
⭐ .NET Core Data Protection 旨在**取代**舊有 .NET Framework 1.x – 4.x 中大家熟悉的 MachineKey 加解密,同時可為現代應用程式隨即使用的解決方案!
### 大致步驟
1. 從 `DataProtectionProvider` 取得 `DataProtector`
2. 呼叫 `Protect` 方法保護你/妳想保護的資料
3. 呼叫 `Unprotect` 方法將被你/妳保護的資料解回原貌
### 實際演練
1️⃣ 建立一個 Security 資料夾並創建一個儲存目的字串(Purpose Strings)的類別···

DataProtectionPurposeStrings.cs
2️⃣ 將儲存目的字串的類別註冊為 Singleton,方便之後注入···

Startup.cs
❗️ 以上兩步供各位參考,但實際怎麼做會更好···可以再討論···😂
3️⃣ 引入 `Microsoft.AspNetCore.DataProtection` 並從 `DataProtectionProvider` 取得 `DataProtector`

RegionController.cs
透過 DataProtectionProvider 來 CreateProtector 預設提供了三種方式:
- `CreateProtector(string purpose)`
- `CreateProtector(IEnumerable purposes)`
- `CreateProtector(string purpose, params string[] subPurposes)`
❗️ 要先有一個認知,就是**透過不同 Purpose Strings 創建出來的 DataProtector 彼此之間均是獨立的**。
換句話說,就是當今天我是以字串 “A” 建立 Protector 的話,若是以字串 “B” 建立 Protector 就無法解除由 A Protector 保護的資料哦!
❗️ **Purpose Strings 不用特別去加密它,它主要是用來呈現你/妳的使用意圖!**
💭 [Purpose strings in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings?view=aspnetcore-3.1)
🌟 官方對於 Purpose Strings 如何定義有給出一個建議答案···
眼尖的你/妳可能有發現我把它定義為字串陣列 👇
`new string[] { "Web.Controllers.RegionController", "RegionIdRouteValue" }`
你/妳可以把它理解成我的意圖 👇
在 RegionController 中去保護 RegionIdRouteValue 實際數值···
既然它是陣列,也並沒有侷限只能傳兩個,就看你/妳如何去定義。但數值當中不能有 null 哦(文件有寫😂)!
4️⃣ 對需要保護的資料呼叫 `Protect` 來保護,反之呼叫 `Unprotect` 解除保護···

RegionController.cs
因範例專案並沒有實際去接資料庫,所以你/妳可以把 Region 當成回傳給 View 的 Model,應該會清楚不少。
實務上我並不想讓前端知道 Region 實際的 Id 是什麼,所以透過 Data Protection API 就可以幫忙我們保護實際數值,直到它真正需要被利用時才解除保護!
### 情境
❗️ 注意觀察 URI 的變化···

Web
資料保護**前**:

資料保護**後**:

[範例專案](https://github.com/cdcd72/NetCore.DPAPI.Demo)可參考···
## 額外參考
💭 [基礎密碼學(對稱式與非對稱式加密技術)](https://medium.com/@RiverChan/%E5%9F%BA%E7%A4%8E%E5%AF%86%E7%A2%BC%E5%AD%B8-%E5%B0%8D%E7%A8%B1%E5%BC%8F%E8%88%87%E9%9D%9E%E5%B0%8D%E7%A8%B1%E5%BC%8F%E5%8A%A0%E5%AF%86%E6%8A%80%E8%A1%93-de25fd5fa537)
## 結尾
感謝各位花時間看完此篇文章,如果本文中有描述錯誤,還請各位多多指教。
我也是直到最近才知道有 DPAPI 這一現成方案可以來幫忙我們保護敏感資料,以往若是有這種需求通常都會自己造輪子來做···
造輪子也不是不好,但就難在如何把整個加解密架構做得很好很安全,理論上開發人員只需要知道如何使用加解密程式,而不能太深入了解內部如何加解密···
微軟官方針對 .NET Core Data Protection 就有想了 3 方面的受眾,你/妳就知道為什麼了···
因為開發人員的職責並沒有一定包含管理密碼,如果搞得大家都知道如何管理密碼,這樣的密碼相當於沒有安全性可言···
---
title: '.NET Core 2.1 升版至 3.1'
description: '.NET Core 2.1 升版至 3.1 的經驗分享'
slug: 'migrate-netcore-21-to-31'
date: '2020-04-16'
drafted: false
featured: false
topic: 'net-core'
tags: ['migration']
authors: ['neil-tsai']
---
此篇文章主要以**自身專案情況將 .NET Core 2.1 升版至 3.1 哦**,有興趣就往下看吧!
## 為什麼需要升版?

.NET Core release lifecycles
從官方核心支援政策可以看出,目前仍長期支援(LTS)的版本有:
- .NET Core 3.1(2022 年 12 月 3 日停止支援)
- .NET Core 2.1(2021 年 8 月 21 日停止支援)
雖然目前仍然可以繼續使用 .NET Core 2.1 沒什麼問題,但是既然有新版本釋出,幹嘛不用呢 σ`∀´)σ···
其實最主要是因為 .NET Core 3.0 以後的版本釋出了許多新特性,另一方面則是大幅簡化程式碼撰寫量,像是有些在 .NET Core 2.X 的程式碼都已經被包進了官方套件中···
👋 Worker Service 就是 .NET Core 3.0 之後才有的新應用,可以參考我寫的「[Worker Service 長時間服務託管|.NET Core 3 新功能](/posts/net-core-3-to-implement-worker-service)」去了解哦···
🌟 當然有另外一部份原因是為了未來的 .NET 5 做好準備,官方也已經釋出了 [.NET 5 Preview 1 版本](https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-1/),當中也提到了他們將會盡量減輕從 .NET Core 3.1 升版 .NET 5 的負擔,開發人員聽到這個應該無一不興奮吧😆···
💭 [.NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core)
## 升版準備及建議
### 準備
1. Visual Studio 2019
2. .NET Core SDK(例如我只要從 2.1 升到 2.2,那我需要準備好 .NET Core SDK 2.2 或者更高版本 SDK)
### 建議
❗ **逐步升版**是比較好的選擇哦,如下圖箭頭指示:

.NET Core migrate
以我的例子來看,我要從 2.1 升版至 3.1,那我需要做以下 3 步(由上至下):
- [ASP.NET Core 2.1 遷移至 2.2](https://docs.microsoft.com/en-us/aspnet/core/migration/21-to-22?view=aspnetcore-3.1&tabs=visual-studio)
- [ASP.NET Core 2.2 遷移至 3.0](https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio)
- [ASP.NET Core 3.0 遷移至 3.1](https://docs.microsoft.com/en-us/aspnet/core/migration/30-to-31?view=aspnetcore-3.1&tabs=visual-studio)
這樣的好處是比較不會手忙腳亂,而且也可以順便看看升版前後究竟在架構上有什麼樣的變化哦!
### 實際演練
當時的準備情況:
1. Visual Studio 2019
2. .NET Core SDK 3.1
#### 2.1 → 2.2
以我的例子來看,我的方案(Solution)有三個專案(Project),這些專案均要升版至 2.2,整體來說才真正升版為 .NET Core 2.2···

Solution structure
先觀察一下這些專案檔···

Web.csproj

Web.Core.csproj

Web.Domain.csproj
可以觀察出幾點:
- TargetFramework 為 netcoreapp2.1
- 官方套件版本均為 2.1.2
❗ 若要讓專案升版為 .NET Core 2.2,則需將 TargetFramework 改為 netcoreapp2.2,然後官方套件版本均要改為 2.2.0!
當然也可以透過對專案右鍵 ➡️ 屬性 ➡️ 修改目標 Framework 為 .NET Core 2.2:

Project Attribute
改完之後應該會如下這樣:

Web.csproj

Web.Core.csproj

Web.Domain.csproj
接著調整相容性版本為 2.2,若專案屬於 .NET Core 2.X 比較需要在意這件事哦!

Set compatibilityVersion to 2.2
關於相容性版本疑問可以參考下方連結,敘述的較為詳細,這邊就不多做描述了···
💭 [What is SetCompatibilityVersion inside of the startup class of asp.net Web API core project](https://stackoverflow.com/questions/54193865/what-is-setcompatibilityversion-inside-of-the-startup-class-of-asp-net-web-api-c)
以上做完,應該就順利升為 .NET Core 2.2 了!
···
此時你應該會想跑一下應用程式是否如以往正常,但是😱···

HTTP Error 502.5 – ANCM Out-Of-Process Startup Failure
別慌!先試試下面命令:
```bash
dotnet --list-runtimes
```
輸出如下:

Output
有發現了嗎?原來是我沒有安裝 .NET Core Runtime 2.2 的關係,導致沒辦法運行以 .NET Core 2.2 為基底的應用程式 ( ×ω× )···
💭 [HTTP Error 502.5 – ANCM Out-Of-Process Startup Failure (Windows Server)](http://docs.lacunasoftware.com/en-us/articles/amplia/on-premises/windows/troubleshoot/502-5.html)
👏 第 1 步完工啦!
#### 2.2 → 3.0
2.1 升版至 2.2 其實修改的內容不多,蠻容易的···
但從 2.2 升版至 3.0 變化會稍微大一點,但照表操課還是能一一解決,讓我們開始吧!
一樣先來修改專案檔···

Web.csproj

Web.Core.csproj

Web.Domain.csproj
❗ 若要讓專案升版為 .NET Core 3.0,則需將 TargetFramework 改為 netcoreapp3.0,然後眼尖的你可能會發現怎麼官方套件被消失了😱···
別慌!先看看文件:

Microsoft docs
其實文件就有說明需要移除的官方套件有哪些哦!所以刪下去就對啦···
···
接著來改 `Startup.cs`···

Include
❌ 移除 `Microsoft.AspNetCore.Mvc`
✔️ 增加 `Microsoft.Extensions.Hosting` 以便可以繼續使用 `env.IsDevelopment()`

ConfigureServices
✔️ 更改 `services.AddMvc()` 為 `services.AddRazorPages()`
💭 [MVC service registration](https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio#mvc-service-registration)

Configure
✔️ 更改 `IHostingEnvironment` 為 `IWebHostEnvironment`
✔️ 增加 `app.UseRouting()`
💭 [Routing in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.1)
✔️ 增加 `app.UseAuthorization()`
💭 [ASP.NET Core Middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1)
✔️ 使用 `app.UseEndpoints()`
···
接著來改 `Program.cs`···

CreateHostBuilder
✔️ 更改 `IWebHostBuilder` 為 `IHostBuilder`
❗ ❗ ❗ **額外提醒** ❗ ❗ ❗

System.Text.Json
❗ **.NET Core 3.0 開始官方移除了對 Newtonsoft.Json 的依賴,而改預設使用 [System.Text.Json](https://docs.microsoft.com/en-us/dotnet/api/system.text.json) 做為 JSON 序列器···**
System.Text.Json 效能上比 Newtonsoft.Json 更好也更加嚴謹,但目前可以處理的事情沒有比 Newtonsoft.Json 多,但基本的序列化跟反序列化是沒問題地,因為它還在發展中,但可以預期的是未來 System.Text.Json 廣泛被使用程度會超越 Newtonsoft.Json,值得學習使用看看哦!
💭 [How to migrate from Newtonsoft.Json to System.Text.Json](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#case-insensitive-deserialization)
👏 第 2 步完工啦!
#### 3.0 → 3.1
3.0 升版至 3.1 問題不大,蠻容易的···
一樣先來修改專案檔···

Web.csproj

Web.Core.csproj

Web.Domain.csproj
❗ 若要讓專案升版為 .NET Core 3.1,則需將 TargetFramework 改為 netcoreapp3.1,然後以我的情況來看就改這樣,沒了😂···
👏 第 3 步完工啦!
## 心得
不得不說 .NET Core 的進程非常快速,前年才釋出 .NET Core 2.1,去年就釋出了 .NET Core 3.1,畢竟若大家有能力的話,也都可以去改 [dotnet/core](https://github.com/dotnet/core),我想這就是大家努力共同產生的結果吧!
整體來說從 .NET Core 2.1 升版至 3.1 不困難,因為官方文件整理得蠻詳細了,但**建議看英文版文件**,中文看起來應該就是機翻出來的,可能要通靈一下😂···
最後在提醒一下,這篇文章主要是以我自身專案狀況來描述,但基本上不會差太多哦!
---
title: 'Worker Service 長時間服務託管'
description: '.NET Core 3 新功能'
slug: 'net-core-3-to-implement-worker-service'
date: '2020-04-13'
drafted: false
featured: false
topic: 'net-core'
tags: ['worker-service']
authors: ['neil-tsai']
---
此篇文章主要帶大家**利用 .NET Core 3.1 實作 Worker Service**,有興趣就往下看吧!
## Worker Service
### 介紹
**Worker Service** 是 .NET Core 3.0 新釋出的功能,官方也有釋出一專案範本(Template)供開發者去使用,讓開發者可以快速發展自己所需的**背景服務**!

Worker service project template
👉 一般來說 Worker Service 會被用來執行**需要長時間處理**或**定期需要做**的事情!(其實就有點像 Windows 平台上的 Windows Service、Linux 平台上的 Daemon Service)
舉幾個例子:
- 處理佇列(queue)上的訊息(message)或事件(event)
- 偵測改變(ex. 檔案、物件···)並快速做出回應
- 從各方資料來源彙總並利用
- 從資料攝取管道(data ingestion pipeline)中擷取所需片段
- 定期資料清理
⭐ 若搭配一些已有的排程套件(ex. Coravel),就可以讓 Worker Service 客製批次工作更加彈性化!
除上面所述之外,既然是從 .NET Core 衍生出來的功能,那它當然也有保留原有的那些被大幅簡化的特徵(日誌、依賴注入、組態設定···等等),好 Worker Service 不用嗎? _(:3 ⌒゙)_
### 基本使用步驟
#### 初始化專案
1️⃣ 建立 `global.json` 檔案,將 .NET Core SDK 限定在 `3.1.201` 版本
```bash
dotnet new globaljson --sdk-version 3.1.201
```
2️⃣ 建立 WorkerServiceDemo 專案
```bash
dotnet new worker -n WorkerServiceDemo
cd WorkerServiceDemo
```
以上做完,最基本可運行的 Worker Service 就做完了,真的不唬 😎···
#### 觀察專案結構
就是以 .NET Core 為基底的專案呀,不是錯覺 (◔౪◔)···

Project structure
❗ 關鍵差在使用了 `Microsoft.Extensions.Hosting` 套件,考試會考很重要?!
💭 [在 ASP.NET Core 中使用託管服務的背景工作](https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio)
該套件含有 `IHostedService` 介面,後續會再提及該介面為什麼很關鍵~
···
先看 `Worker` 類別好了···

Worker.cs
其實官方文件也描述得蠻清楚了,這個 `BackgroundService` 類別就是繼承了 `IHostedService` 及 `IDisposable` 介面,則開發者一定要覆寫的方法為 `ExecuteAsync` ,其餘 `StartAsync`、`StopAsync`、`Dispose` 等方法可視需求覆寫,但切記覆寫完最後依然要呼叫父類別方法哦!
💭 [利用.NET Core中的Worker Service,来创建windows服务或linux守护程序](https://www.cnblogs.com/OpenCoder/p/12191164.html)
···
接著來看 `Program` 類別···

Program.cs
需要於 `ConfigureServices` 中註冊所需的託管服務(有繼承 `IHostedService` 介面),以圖片為例就是 `Worker` 類別!
所以你/妳現在知道它的來龍去脈了嗎?當然若你/妳想了解更深入可以看看官方文件或已開源的程式碼唷~
#### 轉換為 Windows Service (Optional)
那如果我們想把已經寫好的 Worker Service 轉換為可以在 Windows 執行的服務,該怎麼做呢?
只要於專案新增 `Microsoft.Extensions.Hosting.WindowsServices` 套件就行了···
```bash
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
```
接著在 `Program` 類別 `CreateHostBuilder` 處加上 `UseWindowsService()`,完工!

Add UseWindowsService()
···
部署為 Windows 服務的語法(透過 PowerShell)可參考如下:
```bash
# Build and Publish Application
dotnet build
dotnet publish -c Release -o "PathToPublish"
# Create ServiceName Service
sc.exe create ServiceName binpath="PathToPublish\ServiceName.exe"
```
- PathToPublish:將專案發行的路徑位置
- ServiceName:自定義服務名稱
語法執行完應該會類似如下圖,表示已建立了一個新服務:

Windows Service
這邊在附上一些控制服務的語法(透過 PowerShell)可參考如下:
```bash
# Start ServiceName Service
sc.exe start ServiceName
# Stop ServiceName Service
sc.exe stop ServiceName
# Query ServiceName Service Status
sc.exe query ServiceName
# Delete ServiceName Service
sc.exe delete ServiceName
```
- ServiceName:自定義服務名稱
#### 轉換為 Linux Daemon Service(Optional)
那如果我們想把已經寫好的 Worker Service 轉換為可以在 Linux 執行的服務,該怎麼做呢?
只要於專案新增 `Microsoft.Extensions.Hosting.Systemd` 套件就行了···
```bash
dotnet add package Microsoft.Extensions.Hosting.Systemd
```
接著在 `Program` 類別 `CreateHostBuilder` 處加上 `UseSystemd()`,完工!

Add UseSystemd()
部署為 Linux 服務的語法這部分我就沒有特別去深入了解了,之後有碰到我再回來補 😜···
#### 範例專案
可參考[範例專案](https://github.com/cdcd72/NetCore.WorkerService.Demo)···
## 參考
💭 [Upgrade net core2.2 app to net core3.1 as worker service](https://medium.com/ricos-note/upgrade-net-core2-2-app-to-net-core3-1-as-worker-service-cdf5aa6a329e)
💭 [Introduction to Worker Services in .NET Core 3.0](https://medium.com/@nickfane/introduction-to-worker-services-in-net-core-3-0-4bb3fc631225)
💭 [Creating a Worker Service in ASP .NET Core 3.0](https://medium.com/swlh/creating-a-worker-service-in-asp-net-core-3-0-6af5dc780c80)
💭 [WHAT ARE .NET WORKER SERVICES?](https://www.stevejgordon.co.uk/what-are-dotnet-worker-services)
💭 [[NETCore] ASP.NET Core 3.0 Worker Service 搭配 Coravel 建立排程服務](https://marcus116.blogspot.com/2019/09/netcore-aspnet-core-30-worker-service-with-coravel.html)
## 結尾
因為自己本身平常幾乎都是在 Windows 上開發,所以對於 .NET 的 Windows Service 不是很陌生,但那時候就覺得要部署一個 Windows Service 怎麼這個麻煩 😩···
而且視專案狀況,有可能會很肥(佔硬碟容量),更何況是若管理不慎,甚至會影響整台主機效能!
我也是最近才知道 .NET Core 3 釋出的 Worker Service(去年就有了),好奇研究了一下,結果好像回不去了 😂···
Worker Service 簡單且省力,趕緊來試試看吧!
---
title: 'Singleton 單例模式'
description: '單純但暗藏危機'
slug: 'design-pattern-singleton'
date: '2020-03-24'
drafted: false
featured: false
topic: 'design-pattern'
tags: ['creational-pattern']
authors: ['neil-tsai']
---
此篇文章簡單帶大家了解 **Singleton 單例模式** 哦,有興趣就往下看吧!
## 目的
以下擷取自 [wikipedia](https://en.wikipedia.org/wiki/Singleton_pattern)
> The singleton pattern is a software design pattern that restricts the instantiation of a class to one “single” instance.
單例模式是軟體設計原則之一,其目的在於將類別實例化限制為一”單個”實例。
❗️ 換句話說,從應用程式開始生命週期所創造的這一”單個”實例,理論上到應用程式結束其生命週期都只會存在一個。
## 類別圖

從類別圖看有幾點需注意:
1. instance 為靜態變數
2. Singleton 類別建構子為 private(不讓其它類別有機會創造實例)
3. getInstance 為靜態方法
## 程式碼
```typescript
class Singleton {
private static instance: Singleton;
private constructor() { }
/**
* 取得實例
* @returns Singleton
*/
public static getInstance(): Singleton {
if(!Singleton.instance)
Singleton.instance = new Singleton();
return Singleton.instance;
}
}
```
詳細可參考[範例程式碼](https://github.com/cdcd72/DesignPatterns/tree/master/Creational/Singleton)···
## 好處
1. 從應用程式生命週期開始至結束,應**保證只能取到一個且相同的實例**(物件)
2. 因第 1 點關係,故可**減少創造實例的開銷**(記憶體浪費)
3. 與全域變數不同的是該模式可以**延遲實例初始化(Lazy initialization)**,但在部分程式語言實作該模式時,仍要注意是否有同步(Synchronize)問題發生
## 壞處
1. 程式碼在無形之中有了**隱藏性的依賴關係**(因為**不是透過介面暴露而得知**)
2. 若單純僅使用該模式,它算是**打破了 [SRP](/posts/oo-single-responsibility-principle)**(Singleton 類別包含了**創建目標類別實例的職責**及**目標類別實例原有的行為執行職責**),但可以透過工廠(Factory)來分離出創建實例的職責
3. 因第 1 點關係,故會導致**類別之間緊密耦合(Tight coupling)**
4. 因第 3 點關係,**單元測試會變得較為困難**,因單元測試比較大的前提會是類別之間需[鬆耦合(Loose coupling)](https://en.wikipedia.org/wiki/Loose_coupling),從而可以獨立對它們進行單元測試
5. 由於保證只能取到一個且相同的實例,所以在**[多執行緒(Multi-threading)](https://en.wikipedia.org/wiki/Thread_(computing)#Multithreading)下,需要特別注意狀態變化**,若要最小程度的影響應用程式,則該實例執行途中變動幅度應該要最小
6. 因第 5 點關係,一但取出之實例對於商業邏輯有極大程度影響時,則每一次商業邏輯的結果不一定會如預期,間接導致單元測試很難撰寫(因為沒有可以預期的案例)
7. 一般來說透過靜態方法來初始化實例是比較好的方式,但靜態方法會導致開發人員知道他們不需要知道的類別內部程式碼結構
8. 因第 7 點關係,若有寫過單元測試就會知道,即便使用一些單元測試框架,也非常不好處理靜態方法···
9. 要擴展該模式並非易事(若以全面性來考量,則會考慮太多狀況),可能可以透過[裝飾者模式(Decorator Pattern)](https://github.com/cdcd72/DesignPatterns/tree/master/Structural/Decorator)在不影響現階段行為下,附加新行為於物件上
10. 若以某些語言(原生或非原生)GC 回收機制來看,該模式可能會有較為棘手的記憶體管理問題出現
## 結論
其實該模式早前就於 GoF《Design Patterns: Elements of Reusable Object-Oriented Software》中出現,但其實到目前為止,還沒有一個完全的定調認為該模式屬於[反模式(Anti-pattern)](https://en.wikipedia.org/wiki/Anti-pattern),所以網路上有蠻多人議論紛紛,到底該模式是不是一個反模式!?
總結來說,使用 Singleton 模式需要非常小心且最好經過深思熟慮,至於它是不是反模式,則端看開發人員當初的考量及如何去設計類別···
如果是我的話,我會先考慮什麼情況會需要使用單例模式,有沒有其它的替代方案可以先處理!?因為在怎麼樣我都想避掉使用靜態變數或方法,因為一但使用了靜態變數或方法,接著你開始就要考慮如何撰寫單元測試了···😱
## 參考
💭 [What Is a Singleton?](https://medium.com/better-programming/what-is-a-singleton-2dc38ca08e92)
💭 [What is so bad about singletons? [closed]](https://stackoverflow.com/questions/137975/what-are-drawbacks-or-disadvantages-of-singleton-pattern)
💭 [When Singleton Becomes an Anti-Pattern](https://dzone.com/articles/singleton-anti-pattern)
---
title: 'AspectCore'
description: '.Net Core 輕量 AOP 實現'
slug: 'use-aspectcore-to-implement-aop-mechanism'
date: '2020-03-20'
drafted: false
featured: false
topic: 'net-core'
tags: ['aop', 'aspectcore']
authors: ['neil-tsai']
---
此篇文章主要帶大家**利用 AspectCore 實作 AOP 機制於 .Net Core 3.1** 上,有興趣就往下看吧!
## 剖面導向程式設計 AOP
AOP 為 Aspect-Oriented Programming 簡寫,均意為剖面導向程式設計。
## 目的
以下擷取自 [wikipedia](https://en.wikipedia.org/wiki/Aspect-oriented_programming)
👉 **將橫切關注點(Cross-cutting concerns)與業務主體進行進一步分離,以提高程式碼的模組化程度!**
## 見解
當今天系統擁有足夠多的商業邏輯(服務、使用案例···)時,你會希望在每一支商業邏輯上都加上記錄 Log 的功能嗎?
倘若從不同角度來看這件事:
- 使用者:當然要留下記錄阿!
- 開發者:哇!這可能要改到死了···(維護成本太高)
所以回過頭看 AOP 的目的就能知道是要**將一些不應該屬於商業邏輯的其它功能(日誌、安全檢查···)切離!**
換句話說,我可以決定(透過設定一些規則)哪些商業邏輯需要去記錄 Log 這件事,而記錄 Log 的程式碼則不會存在於商業邏輯之中。
更詳細的內容可參考下方連結,就不再多描述囉···
💭 [AOP 觀念與術語](https://openhome.cc/Gossip/SpringGossip/AOPConcept.html)
## AspectCore
### 介紹
AspectCore 是適用於 .Net Core 上的輕量級 AOP 解決方案,它更好的遵循 .Net Core 模組化開發理念,可以更容易建構低耦合、易擴展的 Web 應用程式。AspectCore 使用 Emit 實現高效的動態代理而不依賴任何第三方 AOP 函式庫。
### 基本使用步驟
#### 初始化專案
1. 建立 `global.json` 檔案,將 .NET Core SDK 限定在 `3.1.101` 版本
```bash
dotnet new globaljson --sdk-version 3.1.101
```
2. 建立 `AspectCoreAopDemo` 專案
```bash
dotnet new webapi -n AspectCoreAopDemo
cd AspectCoreAopDemo
```
3. 安裝 `AspectCore.Extensions.DependencyInjection` 套件
```bash
dotnet add package AspectCore.Extensions.DependencyInjection
```
#### 定義攔截器
透過繼承 `AbstractInterceptorAttribute` 可以自行定義攔截器。
```csharp
// ServiceAopAttribute.cs
public class ServiceAopAttribute : AbstractInterceptorAttribute
{
// 自定義攔截器也可以透過 DI 注入所需服務...
[FromServiceContext]
public ILogger Logger { get; set; }
public async override Task Invoke(AspectContext context, AspectDelegate next)
{
try
{
await next(context); // 進入 Service 前會於此處被攔截(如果符合被攔截的規則)...
}
catch (Exception ex)
{
Logger.LogError(ex.ToString()); // 記錄例外錯誤...
throw;
}
}
}
```
#### 設定動態代理
1. `Startup.cs` 於 `ConfigureServices` 方法內設定動態代理(代理規則設定)
```csharp
// Startup.cs
// 略...
public void ConfigureServices(IServiceCollection services)
{
// 注入所需 Services
services.AddTransient();
services.AddTransient();
services.AddControllers();
// 設定動態代理
services.ConfigureDynamicProxy(config => { config.Interceptors.AddTyped(Predicates.ForMethod("Execute*")); });
}
// 略...
```
❗️ **它是會爬介面定義,不是只有單純看類別(只有類別且無實作介面,好像就不會被偵測到需要代理)。**
**常用代理規則**設定:
- 全域會被代理:
`config.Interceptors.AddTyped();`
- 後綴為 Service 會被代理:
`config.Interceptors.AddTyped(Predicates.ForService("*Service"));`
- 前綴為 Execute 的方法會被代理:
`config.Interceptors.AddTyped(Predicates.ForMethod("Execute*"));`
- App1 命名空間下的 Service 不會被代理:
`config.NonAspectPredicates.AddNamespace("App1");`
- 最後一層為 App1 的命名空間下的 Service 不會被代理:
`config.NonAspectPredicates.AddNamespace(".App1");`
- ICustomService 不會被代理:
`config.NonAspectPredicates.AddService("ICustomService");`
- 後綴為 Service 不會被代理:
`config.NonAspectPredicates.AddService("Service");`
- 命名為 Query 的方法不會被代理:
`config.NonAspectPredicates.AddMethod("Query");`
- 後綴為 Query 的方法不會被代理:
`config.NonAspectPredicates.AddMethod("*Query");`
❗️ AspectCore 也提供 `NonAspectAttribute` 來使得 Service 或 Method **不會**被代理:
```csharp
// IOtherService.cs
public interface IOtherService
{
string Execute();
[NonAspect]
string ExecuteXXXX(); // 此方法並不會被代理...(即便是 Startup 有設定其代理規則,也會被排除...)
string GetXXXX();
}
```
2. `Program.cs` 於 `CreateHostBuilder` 處加上 `UseServiceProviderFactory(new DynamicProxyServiceProviderFactory())`,將預設 DI 交由 AspectCore 處理
```csharp
// Program.cs
// 略...
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
})
// 略...
.UseServiceProviderFactory(new DynamicProxyServiceProviderFactory());
```
若上述都有做完,基本的 AOP 機制也就被實現完了👏
回頭思考做了哪些動作呢?
1. 增加自定義攔截器
2. 設定代理規則
3. 替換預設 DI 交由 AspectCore 處理
最簡單的情況大概就是動了三個檔案吧··· 😂
換句話說,即便是等到**開發中後期才套入 AOP 機制也都不會太麻煩**呢!
### 實際使用情形
可以參考[範例專案](https://github.com/cdcd72/NetCore.AspectCore.AOP.Demo),其中 `README.md` 有相關使用上的描述!
#### 情境
代理規則設定:`config.Interceptors.AddTyped(Predicates.ForMethod ("Execute*"));`

ICustomService.cs

IOtherService.cs
1️⃣ 若我打 `https://localhost:44389/customexecutexxxx`,結果為:

Executed method can catched by serviceaop
因為符合所設定的代理規則,所以呼叫 `CustomService.ExecuteXXXX` 方法前會經由 ServiceAop(被攔截)。
2️⃣ 若我打 `https://localhost:44389/otherexecutexxxx`,結果為:

Executed method can’t catched by serviceaop
由於 IOtherService 該方法處加上 NonAspectAttribute, 所以呼叫 `OtherService.ExecuteXXXX` 方法前不會經由 ServiceAop(不會被攔截)。
## 參考
我所使用 `AspectCore.Extensions.DependencyInjection` 版本為 2.0.0 唷!
雖然都是從以下連結參考內容,但已不建議再去看囉,因為資訊已過時了。
💭 [ASP.NET Core 3.0 使用 AspectCore-Framework 实现 AOP](https://www.cnblogs.com/king-23100/p/11821020.html)
💭 [Asp.Net Core轻量级Aop解决方案:AspectCore](https://www.cnblogs.com/liuhaoyang/p/aspectcore-introduction-1.html)
## 結尾
以前在維護公司專案的時候,也有使用到 AOP 的概念來切離關注點,只是那時候對 AOP 的認識甚少,少到我根本也不知道為什麼要套用 AOP,直到最近需要 Survey AOP 概念時,才真正了解為什麼要 AOP?那它究竟能幫助我們到什麼程度?
其實 .Net Core 本身也就有 Middleware 可以實作類似 AOP 的概念(Ex. 攔截要求···等等),但是要從頭思考怎麼用 Middleware 去實現時,我決定先去找找有沒有更單純的替代方案··· 😂
於是我找到了 AspectCore 並試用了一下,覺得操作真的很單純,基本上不用動到太多程式碼,代理規則愛怎麼換就怎麼換,故分享給大家知道!因為網路上大多數會找到對岸的文章,但是多數文章資訊都已經過時了···
---
title: '里氏替換原則 LSP'
description: '先聽爸爸說'
slug: 'oo-liskov-substitution-principle'
date: '2020-03-16'
drafted: false
featured: false
topic: 'object-oriented-design-principles'
tags: ['solid']
authors: ['neil-tsai']
---
此篇文章偏重於以圖解方式,簡單帶大家了解 **里氏替換原則** 哦,有興趣就往下看吧!
LSP 為 Liskov Substitution Principle 簡寫,均意為里氏替換原則。
👍 若開發途中有持續遵循 LSP 原則的話,其實它會讓你程式越來越彈性!(越好實現出 [OCP](/posts/oo-open-closed-principle) 原則)
## 定義
以下擷取自 [wikipedia](https://en.wikipedia.org/wiki/Liskov_substitution_principle)
> if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.).
假如 S 是 T 的子型態,則 T 型態的物件(OT)可以被 S 型態的物件(OS)所取代(換句話說,T 型態的物件(OT)可以被任何 S 型態的物件(OS)所替代),而不改變程式任何的描述(正確性、行為···等等)。
## 見解
確實定義不是那麼好理解,簡單說就是:
⭐️ 不論是透過**繼承(Inherit)**、**實作(Implement)的子類別**,在**程式經過替換子類別**這一手段下,以**不影響其正確性或行為**情況下,就算是有符合該原則了!
## 類別圖探討
這邊依舊拿 [SRP](/posts/oo-single-responsibility-principle) 最後討論出來的方案 C 做說明,如下原圖:

Plan C
接著將焦點移向右下角 Printer 處。

❓ 紅圈處就符合 LSP 原則,為什麼?
以程式的行為來看,要把文章做呈現(Print),但是我可以透過使用文字文件(PlainText)、可攜式文件(PDF)來呈現文章。
那假如今天我想要透過網頁(Html)來呈現,只要去實作 Printer 介面,然後讓 ConcreteUtility 替換掉原本 printer 的實例(物件)就可以了!
即便是替換掉了 printer 實例(物件) ,也沒有因此改變掉程式原本的行為(文章呈現),故符合 LSP 原則!
## 參考
💭 [PHP OO 物件導向原則:里氏替換原則LSP](https://wadehuanglearning.blogspot.com/2017/08/php-oo-lsp.html)
## 結尾
感謝各位花時間看完此篇文章,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以讓大家了解 Liskov Substitution Principle,開發途中經常會碰到需要繼承(Inherit)、實作(Implement)的時候,但**多數撰寫程式的人,一開始不太會意識到寫程式是專注於行為上,而是以寫程式的角度來操作繼承跟實作,所以不知不覺中就違反了 LSP 原則!**
我想這一部分多少也有從大學時期所帶出來的知識相關,知識的”活”用跟”硬”用,我想差別就在這了···
---
title: '介面隔離原則 ISP'
description: '依賴你所需要的'
slug: 'oo-interface-segregation-principle'
date: '2020-03-13'
drafted: false
featured: false
topic: 'object-oriented-design-principles'
tags: ['solid']
authors: ['neil-tsai']
---
此篇文章偏重於以圖解方式,簡單帶大家了解 **介面隔離原則** 哦,有興趣就往下看吧!
ISP 為 Interface Segregation Principle 簡寫,均意為介面隔離原則。
👍 若開發途中有持續遵循 ISP 原則的話,其實你會發現程式也漸漸會降低耦合哦!
換句話說,就是軟體模組之間依賴性沒有那麼強,解耦好處可參考之前寫的 [DIP](/posts/oo-dependency-inversion-principle) 文章。
## 定義
以下擷取自 [wikipedia](https://en.wikipedia.org/wiki/Interface_segregation_principle)
> No client should be forced to depend on methods it does not use.
**不應強迫客戶端去依賴它不使用的方法!**
## 見解
一般來說,都會**盡量避免掉具體類別之間的依賴關係**,因為風險實在太多了···
⭐️ 介面就是一個解除耦合的關鍵,如果操作介面得宜,就能改變依賴關係,進而改善系統架構。
❓ 那有什麼時機點可以**很直覺套用 ISP 原則**?回頭再來告訴各位。
## 類別圖探討
這邊依舊拿 [SRP](/posts/oo-single-responsibility-principle) 最後討論出來的方案 C 做說明,如下原圖:

Plan C
進一步再畫成如下圖(**若有其它元件互動情況下**):

Plan C & Other component
有發現哪裡不對勁了嗎 Σ(\*゚д゚ノ)ノ ?
回頭看完定義,再回來看匯出元件(Export Component),確實已經違反了 ISP 原則,為什麼?
假設今天有其它元件(Other Component)也想要利用匯出元件來匯出檔案,所以**理論上工程師修改範圍可能就落在其它(Other)及匯出(Export)元件,但工程師卻忘了匯出元件有被「別的元件(Utility Component)」所使用,變動過大的話,這個所謂「別的元件」就需要重新編譯及部署!**
再者,如果我們站在其它元件(Other Component)的角度來看,我可以給文章(Article)或檔案(file)來匯出,但這個其它元件真的會需要知道「給文章來匯出」這件事嗎?
Ans:**如果說這個其它元件職責就只有「給檔案來匯出」,那它就不應該知道有「給文章來匯出」這件事!**
···
如何讓其它元件避免知道它不應該需要知道的事?**介面(Interface)此時就能派上用場了!**
加上介面來隔離之後,如下圖:

Added interface
紅圈處就是關鍵,這兩個介面(ExportFile、ExportArticle)達成了兩個目的!
1. 資訊隱藏(可參考之前寫的 [OCP](/posts/oo-open-closed-principle) 文章)
2. 避免讓使用匯出元件者知道它不應該需要知道的事
## 使用 ISP 時機點
⭐️ 最直覺的時機點:**使用他人所撰寫之第三方元件**
❓ 你可能會覺得直接用第三方元件有什麼不對?幹嘛多一個介面來搞自己?
## 舉例
假設今天客戶有自己的 LDAP 元件(對主系統來說屬於外部元件),在進入主系統前需要先透過 LDAP 驗證是合法的使用者才可登入,程式碼(以 C# 為例子)你可能會這樣寫:
```csharp
// AuthenticationMethod.cs
using xxx.ldap;
public class AuthenticationMethod {
// 驗證使用者密碼
public bool VerifyUserPassword(string userId, string password) {
// 預設驗證失敗
bool flag = false;
Ldap ldap = new Ldap();
try {
ldap.Open(...);
flag = ldap.VerifyUserPassword(userId, password);
} catch(Exception ex) {
// 錯誤處理...
} finally {
ldap.Close();
}
return flag;
}
}
```
VerifyUserPassword 如果功能要正常運行,勢必需要它人家的 LDAP 元件才行(**間接來看就是AuthenticationMethod 和 Ldap 有耦合關係**)。
❓ 當今天你要對 VerifyUserPassword 撰寫單元測試時,你會發現寫不出來 (((゚Д゚;))) ,為什麼?
回想一下它要怎麼樣能夠正常運行就知道原因了,因為 LDAP 沒辦法實際取得所需資料,沒取得資料也無從驗證是否斷言(Assert)會過···
再想一下,我只是要做單元測試而已,又不是真的要實際取得客戶的 LDAP 資料,所以我應該要想辦法偽造(Fake)一個 LDAP 元件出來,這樣就能順利驗證 VerifyUserPassword (無法取得 LDAP 連線情況下)。
···
那要怎麼偽造出一個 LDAP 元件?**介面(Interface)這時又能派上用場了!**
程式碼:
```csharp
// ILdapBasic.cs
public interface ILdapBasic {
// 開啟連線
void Open(...);
// 驗證使用者密碼
bool VerifyUserPassword(string userId, string password);
// 關閉連線
void Close();
}
```
```csharp
// LdapBasic.cs
using xxx.ldap;
public class LdapBasic : ILdapBasic {
private Ldap _Ldap;
public Ldap Ldap
{
get {
if (_Ldap == null) {
_Ldap = new Ldap();
}
return _Ldap;
}
set {
_Ldap = value;
}
}
// 開啟連線
public void Open(...) {
Ldap.Open(...);
}
// 驗證使用者密碼
public bool VerifyUserPassword(string userId, string password) {
return Ldap.VerifyUserPassword(userId, password);
}
// 關閉連線
public void Close() {
Ldap.Close();
}
}
```
```csharp
// AuthenticationMethod.cs
public class AuthenticationMethod {
private ILdapBasic _LdapBasic;
public ILdapBasic LdapBasic
{
get {
if (_LdapBasic == null) {
_LdapBasic = (ILdapBasic)Assembly.Load(...).CreateInstance(...);
}
return _LdapBasic;
}
set {
_LdapBasic = value;
}
}
// 驗證使用者密碼
public bool VerifyUserPassword(string userId, string password) {
// 預設驗證失敗
bool flag = false;
try {
LdapBasic.Open(...);
flag = LdapBasic.VerifyUserPassword(userId, password);
} catch(Exception ex) {
// 錯誤處理...
} finally {
LdapBasic.Close();
}
return flag;
}
}
```
從上面程式碼可以觀察到幾個有趣的現象:
1. AuthenticationMethod 本來有 xxx.ldap 的引用(include)被拿掉了,已經轉嫁給 LdapBasic 去承擔了(所以 LdapBasic 裡面的實作都會跟 xxx.ldap 息息相關)。
2. AuthenticationMethod 現在是依賴 ILdapBasic 介面,而不是 xxx.ldap 中的具體實作(解耦)。
3. 現在可以透過偽造 ILdapBasic ,進而達到可以於單元測試中給出預期的回應,這樣一來 VerifyUserPassword 即使不用真的連上 LDAP,就能夠進行模擬回應。
## 總結
**使用第三方元件之前,先要評估外部帶給系統的影響,如果說要將系統跟外部做隔離,那就是會建議墊一層介面,介面本身就帶有侷限特性,而實作該介面的具體實作內部應該就都會是跟外部有關的實作!**
## 迷思
❓ 好像寫單元測試會需要一併異動”被測試”程式?
Ans:**如果有發生這種現象,那很有可能你不是在寫單元測試,反而可能是在做整合測試···**
從剛剛套用 ISP 原則的程式碼來看,LdapBasic 屬性只有在功能正常運行時,才會取得真正的實例(\_LdapBasic),而如果在單元測試執行途中,你需要做的是偽造一個實例出來並把它設定給 LdapBasic 屬性,這樣一來就不存在你寫單元測試會一併調整被測試程式這件事情 (´・ω・`)···
## 結尾
感謝各位花時間看完此篇文章,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以讓大家了解 Interface Segregation Principle,一般來說我們都會希望自己的系統好維護,其中透過使程式不去依賴它所不需要的東西,看似一件單純的事情,而真正有意識到的卻是少數,介面本身除了有規範的效果,但同樣也有制約的效果,如果介面操作得宜,將能使系統逐步單純化!
---
title: '開放封閉原則 OCP'
description: '可「防」可「擴」'
slug: 'oo-open-closed-principle'
date: '2020-03-11'
drafted: false
featured: false
topic: 'object-oriented-design-principles'
tags: ['solid']
authors: ['neil-tsai']
---
此篇文章偏重於以圖解方式,簡單帶大家了解 **開放封閉原則** 哦,有興趣就往下看吧!
OCP 為 Open Closed Principle 簡寫,均意為開放封閉原則。
👍 **若開發途中有持續遵循 [SRP](/posts/oo-single-responsibility-principle) 及 [DIP](/posts/oo-dependency-inversion-principle) 原則的話,其實你會發現程式也漸漸會符合 OCP 原則哦!**
❗️ 不過上述屬於比較理想的狀況,主要還是得看設計者當初的設計考量。
## 定義
以下擷取自 wikipedia
> 1. A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
> 2. A module will be said to be closed if is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding).
1️⃣ 模組因為是**可擴充**的情況,所以可以稱之為**開放**。比如像是在**資料結構中新增欄位**或是**功能新增一些元素(原有行為不變動情況下)**。
2️⃣ 模組因為是**可被其它模組使用**的情況,所以可以稱之為**封閉**。(**假設前提是 A 模組已經實作完 B 模組某種程度的需求,換句話說 B 模組不用特別再實作一次相同需求,而是使用 A 模組即可!**)
❓ **其中 2️⃣ 當中提到模組之間如何做到[資訊隱藏(Information hiding)](https://en.wikipedia.org/wiki/Information_hiding)?關鍵就在使用介面!回頭再來告訴各位。**
## 見解
從定義看兩點總結:**對外要可擴充,對內則避免修改,而對象可以是模組、類別、函式···等等。**
而我會用「**行為**」來當依據判斷,你一定也會希望原本正常的行為,經過你的修修改改後也要能夠繼續維持住,而如果真是如此,其實我覺得這也沒有實質打破 OCP 原則。
···
舉例來說,今天我有從**本地上傳檔案至遠端的行為(先假設沒有規定檔案大小)**,所以當使用者需要上傳時,系統就給他上傳,結果某一天遠端伺服器的容量直接炸了。
這時可能就會接到一個新需求,而需求內容則是「**系統上傳檔案前先判斷檔案大小,才決定可不可以上傳至遠端伺服器**」。
而當你**修改程式並加上這一判斷邏輯**後,可能會有兩種狀況發生:
1. 上傳行為**如同既往**(只是多了判斷檔案大小邏輯)
2. 上傳行為**出現異常**
如符合第一點描述,其實我認為這就沒有實質打破 OCP 原則,反之才可能要思考是不是當初分離職責(SRP)就沒有做好···等等其它因素。
## 類別圖 & 元件圖探討
接下來要仔細看囉,這邊開始會探討介面(Interface)的位置變動會造成什麼影響?!
這邊探討的範例都是從 [SRP](/posts/oo-single-responsibility-principle) 最後討論出來的方案 C(Plan C)下去變動。
### 注意看 Utility 介面位置變動!
1️⃣ 如果**將 Utility 介面移動到元件 A**,會發生什麼事?
✔️ 元件之間會符合 DIP 原則!
類別圖:

元件圖:

從兩元件之間的關係來看,元件 A 層次比較高,而元件 B 層次就相對於元件 A 低,所以也可以解讀成「**元件 A 的改變不會影響元件 B**」,對於元件 A 來說,只要給我符合 Utility 介面定義的元件即可(**元件 B 有可替換性**)!
···
2️⃣ 如果**將 Utility 介面移動到元件 B**,會發生什麼事?
✔️ 符合**資訊隱藏**特性!
❌ 元件之間沒有符合 DIP 原則!
類別圖:

元件圖:

與第一種情況不同,元件之間的依賴關係相反了,元件 B 變成比較高層次,而元件 A 相較於元件 B 低,可以解讀成「**元件 A 若沒有元件 B 有可能會無法正常運作**」,**元件 A 如果是系統中較重要的核心,應該都要盡量避免這種依賴關係!**
❗️ 接著從另一個角度來看**資訊隱藏**這件事,為什麼會符合?
**因為元件 A 只知道可以呼叫 export 函式來匯出文章,但是它不曉得實際上是透過 SimpleExporter 完成這件事···**
換句話說,元件 B 很大程度上在給別人使用時,沒有透漏太多資訊給元件 A 知道,只是跟它說我應該怎麼用而已~
···
3️⃣ 如果**拿掉 Utility 介面**,會發生什麼事?
❌ 沒有符合資訊隱藏特性!
❌ 元件之間沒有符合 DIP 原則!
類別圖:

元件圖:

元件圖和第二種情況一樣,但**唯一的不同是拔掉了元件 B 內的 Utility 介面**,這時就**發生了遞移依賴關係**。
😱 遞移依賴:依賴了本不應該依賴的,這種依賴關係應該要消除!
仔細看你會發現**元件 A 知道可以呼叫 export 函式來匯出文章,但它同時也知道是透過 SimpleExporter 完成這件事···**
## 總結
這樣你就清楚為什麼要**單獨把介面放在高層次元件位置**,而**低層次元件通常放易變動的具體實作**,主要就是幫**元件之間做解耦動作**,這樣一來**低層次元件就有機會可以被替換**,而**不影響高層次元件**哦!
但**倘若利用介面不是達到元件之間解耦**,那就有**可能是想要達到資訊隱藏的目的**哦!
## 結尾
感謝各位花時間看完此篇文章,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以讓大家了解 Open Closed Principle,不過話說回來,若要完全遵循 OCP 精神,系統某種程度上會變複雜許多(複雜是指檔案數量可能會變多,更何況是介面會在元件之間游移),所以如果能夠畫出類別圖,甚至是元件圖,都將有助於了解系統整體的架構規劃!
---
title: '單一職責原則 SRP'
description: '改變應只為一個理由'
slug: 'oo-single-responsibility-principle'
date: '2020-03-06'
drafted: false
featured: false
topic: 'object-oriented-design-principles'
tags: ['solid']
authors: ['neil-tsai']
---
此篇文章偏重於以圖解方式,簡單帶大家了解 **單一職責原則** 哦,有興趣就往下看吧!
SRP 為 Single Responsibility Principle 簡寫,均意為單一職責原則。
👍 **開發途中若有遵循此原則將有助於使類別往高內聚低耦合發展哦!**
❓ 為何分離職責這件事很重要,回頭再來告訴各位。
## 定義
以下擷取自 [wikipedia](https://en.wikipedia.org/wiki/Single_responsibility_principle)
> A reason to change.
改變應只為一個理由!
## 見解
從**單一職責**這幾個字來看,你可能會覺得是「一個類別或模組只能做一件事」,我當初也是這樣認為…
讀了《Clean Architecture》 中對於 SRP 的描述,重新認識到應將上面「一個類別或模組只能做一件事」,修飾為「**一個類別或模組應只對於一個角色(Actor)負責**」。
書中提及的例子是關於 Employee 類別職責,如下圖:

當**維護 Employee 類別的不只一種角色**時,會發生什麼事?
先假設情境為工程師被要求需要寫出:
- 符合**會計人員需求**的 calculatePay 方法
- 符合**人資人員需求**的 reportHours 方法
- 符合**資料庫管理員需求**的 save 方法
很明顯這個類別已經違反了 SRP,因為它執掌太多職責了…
**更慘的是假設今天只要上述其中一個需求有了異動,有沒有可能導致其餘兩種人員功能使用上出現狀況?有很大機會!**
書中則有提出幾種解法,詳細可以去買來看看,這邊就不多做說明了。
## 類別圖探討
舉個就近的例子,比如在 Blog 發文這件事,一開始我可能會思考文章需要哪些功能?
應該要可以**編輯、顯示、匯出**文章,如下圖方案 A:

Plan A
Client 端你可能會這樣寫:
```typescript
class Client {
private article: Article;
constructor(article: Article) {
this.article = article;
}
// 編輯文章
public editArticle(content: string): void {
// 編輯文章內容
this.article.editContent(content);
}
// 顯示文章
public printArticle(): void {
// 顯示文章內容
this.article.printContent();
}
// 匯出文章
public exportArticle(): void {
this.article.export();
}
}
```
你可能已經發現了,以**目前來看 Article 類別掌握太多職責,所以已經違反 SRP!**
···
接著我們可以開始思考有哪些角色會來操作這些功能?
- 編輯文章:文章作者(無法自行匯出文章)
- 顯示文章:文章編輯器
- 匯出文章:編輯
**角色並不一定要是真實存在的人,特別注意!**
然後依據剛剛所思考的將方案 A 調整為方案 B,如下兩張圖:
1️⃣ 先將**顯示的責任抽出來給 Printer,要怎麼樣顯示出文章內容就由你來決定吧!**

Plan B - 1
2️⃣ 則將**匯出的責任交給 SimpleExporter,透過什麼樣的方式來下檔就交給你了!**

Plan B - 2
Client 端改寫成這樣:
```typescript
class Client {
private article: Article;
private printer: Printer;
private exporter: SimpleExporter;
constructor(article: Article) {
this.article = article;
this.printer = new PlainTextPrinter();
this.exporter = new SimpleExporter();
}
// 編輯文章
public editArticle(content: string): void {
// 編輯文章內容
this.article.editContent(content);
}
// 顯示文章
public printArticle(): void {
// 顯示文章內容
this.printer.print(this.article.getContent());
}
// 匯出文章
public exportArticle(): void {
this.exporter.export(this.article);
}
}
```
**現在 Article 類別只專注於操作文章,而顯示及匯出的職責都被分離出來了。**
所以當我哪天要調校顯示或匯出功能,我都不用擔心操作文章的功能會被影響哦!
這就是遵循 SRP 所帶來的結果,**一個職責只會因為一個理由而改變**。
## 為什麼要分離職責?
當你將類別分離職責的越清楚,類別將會愈符合**高內聚低耦合**的特性。
高內聚:因類別職責相當清楚(哪天匯出功能發生問題,直接直搗黃龍)。
低耦合:類別需要 include 的數量降低(因分離職責關係,就有機會將特定職責類別集中管理,像是透過外觀模式就能做到減少 include 數量,等等就會提到了)。
## 延伸
從方案 B 的 Client 端程式碼來看,當我一直加入與操作文章、顯示、匯出無關的新職責時,Client 端程式碼有可能會越來越複雜,那有沒有什麼樣的方式可以做個調整?
更何況是當顯示、匯出等細部邏輯都各自寫在不同元件,Client 端應該要怎麼處理?
接著我們談談**外觀模式(Facade pattern)**。
## 外觀模式
接著我們在將方案 B 調整成方案 C,如下圖:

Plan C
Client 端再改寫成這樣:
```typescript
class Client {
private article: Article;
private utility: Utility;
constructor(article: Article) {
this.article = article;
this.utility = new ConcreteUtility();
}
// 編輯文章
public editArticle(content: string): void {
// 編輯文章內容
this.article.editContent(content);
}
// 顯示文章
public printArticle(): void {
// 顯示文章內容
this.utility.print(this.article.getContent());
}
// 匯出文章
public exportArticle(): void {
this.utility.export(this.article);
}
}
```
套用外觀模式(Facade pattern)後,你會發現 Client 端切離了 Printer 和 SimpleExporter 明確的依賴關係,而這樣的依賴關係變成由 ConcreteUtility 來承擔,Client 端只需知道引用(include)具體工具元件達成外部需求就行!
## 參考
💭 [PHP OO 物件導向原則:單一職責原則SRP](https://wadehuanglearning.blogspot.com/2017/08/php-oo.html)
## 結尾
感謝各位花時間看完此篇文章,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以讓大家了解 Single Responsibility Principle,若在開發時就盡可能遵循該原則,將能寫出高內聚低耦合的類別,間接影響就是逐步降低元件之間的耦合關係,棒棒!
---
title: '依賴反轉原則 DIP'
description: '夾心餅乾原則!?'
slug: 'oo-dependency-inversion-principle'
date: '2020-03-04'
drafted: false
featured: false
topic: 'object-oriented-design-principles'
tags: ['solid']
authors: ['neil-tsai']
---
此篇文章偏重於以圖解方式,簡單帶大家了解 **依賴反轉原則** 哦,有興趣就往下看吧!
DIP 為 Dependency Inversion Principle 簡寫,均意為依賴反轉原則。
👍 **開發途中若有遵循此原則將有助於將軟體模組之間做解耦動作!**
❓ 為何軟體模組之間需要解耦,回頭再來告訴各位。
## 定義
以下擷取自 wikipedia
> 1. High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
> 2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
1️⃣ **高層次模組不應該直接依賴低層次模組,它們都應該依賴抽象層!**
2️⃣ **抽象層不應該受到低層次模組影響而改變,反而是低層次模組要去依賴抽象層!**
## 見解
老實說當初在看 DIP 原則,我根本看不懂定義在寫什麼東西 (´\_ゝ`) ···
寫了約有 1 ~ 2 年程式後,才漸漸明白 DIP 原則想要傳達的意涵 _(┐「ε:)_
···
通常一個系統**變化最頻繁的地方就是商業邏輯**,可能**因客戶的需求變動時常有變化**,所以我們可以將商業邏輯視為是比較高層次的(意為**商業邏輯都會寫在比較高層次模組的位置**)。
換句話說,通常**變化不大的計算邏輯或功能型程式**,我們都會**實作在比較低層次模組的位置**哦!
當我們理解高低層次模組概況後,那它們之間**如何互動**?就是我們這次探討的 DIP 原則囉!
···
其實可以**將定義比喻為夾心餅乾**,蛤?Σ(°Д°;

因為有餅乾(抽象層)的關係,兩種餡料(具體層)並不會實際碰到彼此,換句話說,這兩餡料可以各自加上一些佐料(實作各自發展),但**倘若兩餡料需要做到保持各自風味的前提下做結合,就需要餅乾來幫忙**!

**倘若沒有餅乾的保護,兩種餡料就會混合在一起**(程式有相依性,即為**程式耦合**),如上圖。
## 類別圖探討
底下就從類別圖來看 DIP 原則吧!

A rely on B
這是一個典型的 **A 依賴 B** 的類別圖,有幾點可以討論:
1. A 如果沒有 B 的話,A 的功能很大機會受到 B 的牽制
2. 當 B 的異動很大時,A 的功能保險起見都應該要重新測試,嚴重的話甚至改 B 連帶需要改 A
相信大家對於這兩點都是不樂見的,這邊還只是拿出**類別之間的相依關係**討論而已,更進階的層次還有**元件之間的相依性**,但不論是類別還是元件,其實都可以套用 DIP 的相似概念來達到**解耦**目的!
···
接著當我們將上圖套用 DIP 原則之後,會變成怎麼樣呢?

A rely on I
這是一個典型的 **A 依賴 I** 的類別圖,有幾點可以討論:
1. A 不一定要 B,只要是實作於 I 的都可以接受
2. 即使 B 的異動很大,以 A 的角度來看只要給我符合的結果就行,我不管你 B 如何去實作
**主觀來看 A(高層次模組)要能正常運作,一定會需要 B(低層次模組)的協助,而 DIP 將這樣的關係倒反過來,實際上當然不是 B 跑去依賴 A,而是 A 和 B 都去依賴 I(看似 A 依賴 B,但不是那麼明確罷了)。**
## 延伸
但倘若我們以 A 依賴 I 為例子來看, 其實還有一個問題未解··· Σ(;゚д゚)
**那究竟誰要產生去 B 呢?**還是 A 嗎?那這樣有解決根本問題嗎?
接著我們來談談**工廠模式**是如何解決產生 B 的問題。
## 工廠模式

Factory pattern
從上面類別圖可以很清楚知道誰去產生了 B,A 透過工廠(Factory)取得了 B,而 A 依賴 BFactory 介面這也意謂著,實際具體工廠也不一定要 BFactoryImp,只要實作 BFactory 介面的具體工廠都可以接受。
**若以 A 的角度來看,它只會知道我得到了一個 B,是由工廠幫它產生出 B!**
## 疑點
眼尖的話你會發現···

Violate DIP
BFactoryImp 因為依賴 B,所以違反了 DIP 原則,你可能會想這不是廢話嗎?
因為 BFactoryImp 目的就是要產生 B 阿,這要怎麼避免?
**這其實印證了要在一個系統上完全遵守 DIP 原則幾乎是不可能的事情,所以工程師必須要思考如何拿捏,而不是一味地去遵循原則哦!**
## 為什麼需要解耦?
你應該也不會希望測試一模組,還要連帶測試其它模組(更何況這不是你團隊開發的模組),解耦目的終究是希望讓軟體模組各自可以彈性發展而不互相影響,其實這也會間接影響團隊職責切割,很有趣吧!
舉個例子來說,可以試想看看**商業邏輯**跟**儲存資料方式**這兩塊,將**計算的職責交給商業邏輯**,而**實際資料儲存則交給儲存資料方式**。
- 處理商業邏輯的工程師,只需要思考**如何因應需求變化而改變其商業邏輯程式**
- 處理儲存資料方式的工程師,則只需要思考**如何將接到手的資料確實儲存起來**
## 好處
1. **避免掉多數直接改 A 壞 B 的情況**
2. **職責逐漸單一化**,可以**寫出更有價值的單元測試**(程式耦合嚴重,幾乎是寫不出單元測試,要注意!
3. 可達到**快速除錯**的目的,因職責已逐漸單一化
4. 從**元件角度**來看,可以**分開部署**,不需要重新編譯整個專案(不過端看專案本身如何去切層
5. 程式會**愈來愈符合物件導向設計原則**(很神奇吧~
6. 有想到在回來補充 XD···
## 參考
💭 [亂談軟體設計(5):Dependency-Inversion Principle](http://teddy-chen-tw.blogspot.com/2012/01/5dependency-inversion-principle.html)
💭 [PHP OO 物件導向原則:依賴反轉原則DIP](https://wadehuanglearning.blogspot.com/2017/08/php-oo-dip.html)
## 結尾
希望這篇文章可以讓大家了解 Dependency Inversion Principle,若在開發時就盡可能遵循該原則,對未來的幫助絕對不會少,它就是短期看不出什麼效果,但長期來看你會後悔為什麼當初不做 (´\_ゝ`)···
---
title: 'Fluent Interface'
description: '一種程式碼”寫作”風格'
slug: 'coding-style-of-fluent-interface'
date: '2020-03-02'
drafted: false
featured: false
topic: 'api-design-pattern'
tags: ['fluent-interface', 'code-readability']
authors: ['neil-tsai']
---
其實 [Fluent Interface(流暢介面)](https://en.wikipedia.org/wiki/Fluent_interface)不是新概念,早在西元 2005 年就由 Eric Evans 和 [Martin Fowler](https://en.wikipedia.org/wiki/Martin_Fowler_(software_engineer)) 所提出,但我覺得值得讓大家初步了解其實作概念,因為真的很有趣呀ヽ(✿゚▽゚)ノ
## Fluent Interface 介紹
它其實也是一種**[物件導向(Object-oriented)](https://en.wikipedia.org/wiki/Object-oriented_design)API 的實作**,而該實作上會**廣泛地依賴[方法鏈(Method chaining)](https://en.wikipedia.org/wiki/Method_chaining)**。
⭐️ 最終目標:**增加寫[特定領域語言(Domain-specific language)](https://en.wikipedia.org/wiki/Domain-specific_language)的程式碼可讀性!!!**
⭐️ 三種**特性**:
- 透過呼叫方法的回傳值來定義(物件內容)
- 有自我參考特性,傳出及傳入物件等價
- 透過回傳 void 或造成傳出及傳入物件不等價情況則可以終止(意即結尾)
## 未套用 Fluent Interface
舉個例子來說,當**使用者在旅遊網站預訂了一個假期**,背後實際程式(以 TypeScript 為例)可能是這樣寫:
```typescript
interface VocationBuilder {
/**
* 設定開始日期
* @returns void
*/
setBeginDate(date: Date): void;
/**
* 設定結束日期
* @returns void
*/
setEndDate(date: Date): void;
/**
* 設定住哪間旅館
* @returns void
*/
setHotel(hotel: Hotel): void;
/**
* 設定吃哪間餐廳
* @returns void
*/
setRestaurant(restaurant: Restaurant): void;
/**
* 讓使用者能取得假期物件
* @returns Vocation
*/
create(): Vocation;
}
```
```typescript
class ConcreteVocationBuilder implements VocationBuilder {
// 定義假期屬性
private beginDate: Date;
private endDate: Date;
private hotel: Hotel;
private restaurant: Restaurant;
// 設定假期起始日
setBeginDate(date: Date): void {
this.beginDate = date;
}
// 設定假期結束日
setEndDate(date: Date): void {
this.endDate = date;
}
// 設定旅館
setHotel(hotel: Hotel): void {
this.hotel = hotel;
}
// 設定餐廳
setRestaurant(restaurant: Restaurant): void {
this.restaurant = restaurant;
}
// 創造假期
create(): Vocation {
return new Vocation(this.beginDate, this.endDate, this.hotel, this.restaurant);
}
}
```
```typescript
// 假期物件建造者
const vocationBuilder: VocationBuilder = new ConcreteVocationBuilder();
// 起始日期
const beginDate: Date = new Date('2019-12-23');
// 結束日期
const endDate: Date = new Date('2019-12-30');
// 某旅館
const hotel: Hotel = new Hotel('Some Hotel');
// 某餐廳
const restaurant: Restaurant = new Restaurant('Some Restaurant');
// 設定假期物件建造者
vocationBuilder.setBeginDate(beginDate);
vocationBuilder.setEndDate(endDate);
vocationBuilder.setHotel(hotel);
vocationBuilder.setRestaurant(restaurant);
// 創造假期
const vocation: Vocation = vocationBuilder.create();
```
以**工程師的角度**來看,上面程式碼其實看起來沒什麼太大問題,因為**最終目標是取得假期物件來做一些頁面呈現、資料操作**···等等。
倘若我們會**需要和 PM、SA 討論的角度**來看,程式碼可讀性可能就差了,更何況是我如果把 VocationBuilder 的設定事項移到程式碼較前面幾行(行數小位置),最後幾行我才取得假期物件,就變成工程師還要找程式碼上下文 ··· 眼神死 (›´ω`‹ )
## 套用 Fluent Interface
那實際導入流暢介面會變成怎麼樣呢?
```typescript
interface VocationBuilder {
/**
* 設定開始日期
* @param {Date} date
* @returns VocationBuilder
*/
setBeginDate(date: Date): VocationBuilder;
/**
* 設定結束日期
* @param {Date} date
* @returns VocationBuilder
*/
setEndDate(date: Date): VocationBuilder;
/**
* 設定住哪間旅館
* @param {Hotel} hotel
* @returns VocationBuilder
*/
setHotel(hotel: Hotel): VocationBuilder;
/**
* 設定吃哪間餐廳
* @param {Restaurant} restaurant
* @returns VocationBuilder
*/
setRestaurant(restaurant: Restaurant): VocationBuilder;
/**
* 讓使用者能取得假期物件
* @returns Vocation
*/
create(): Vocation;
}
```
```typescript
class ConcreteVocationBuilder implements VocationBuilder {
// 定義假期屬性
private beginDate: Date;
private endDate: Date;
private hotel: Hotel;
private restaurant: Restaurant;
// 設定假期起始日
setBeginDate(date: Date): VocationBuilder {
this.beginDate = date;
return this;
}
// 設定假期結束日
setEndDate(date: Date): VocationBuilder {
this.endDate = date;
return this;
}
// 設定旅館
setHotel(hotel: Hotel): VocationBuilder {
this.hotel = hotel;
return this;
}
// 設定餐廳
setRestaurant(restaurant: Restaurant): VocationBuilder {
this.restaurant = restaurant;
return this;
}
// 創造假期
create(): Vocation {
return new Vocation(this.beginDate, this.endDate, this.hotel, this.restaurant);
}
}
```
```typescript
// 假期物件建造者
const vocationBuilder: VocationBuilder = new ConcreteVocationBuilder();
// 起始日期
const beginDate: Date = new Date('2019-12-23');
// 結束日期
const endDate: Date = new Date('2019-12-30');
// 某旅館
const hotel: Hotel = new Hotel('Some Hotel');
// 某餐廳
const restaurant: Restaurant = new Restaurant('Some Restaurant');
// 創造假期
const vocation: Vocation = vocationBuilder.setBeginDate(beginDate)
.setEndDate(endDate)
.setHotel(hotel)
.setRestaurant(restaurant)
.create();
```
相較於未套用情況,其實**只有調整介面內的方法回傳自己介面本身**,這樣表示**只要是實作該介面的類別都能相容**!
❗️ **不過還是要注意的是上面所提到關於它的特性,需維持傳出及傳入物件等價,才能使其使用上成立。**
## 好處
1. 維持類別封裝(透過呼叫方法操作物件本身)
2. 簡化程式碼撰寫量並增加可讀性(寫程式像在說話一樣,換句話說就是程式碼逐漸 Domain 化)
3. 對於程式的解讀可擴大到整個團隊,而不是只有工程師小圈圈 (´_ゝ`)
## 壞處
1. 某些時候不一定好除錯
## 使用時機
1. 有物件難以設置或使用的情況
2. 想提供給別人使用的 API 逐漸往特定領域語言化(意指於特定領域可以較直覺去使用)
3. 針對同一物件需大量設定或方法呼叫的場合
## Fluent Interface vs. Extension Method
可能有人會問 Fluent Interface 和 C# 3.0 推出的 Extension Method 有什麼不一樣?
Extension Method 其實光是**特性**就和 Fluent Interface 有蠻大差異哦!
⭐️ 三種**特性**:
- 通常回傳常數
- 沒有特定的終止條件,可依需求一再呼叫使用
- 傳出及傳入並無實質等價關係(下一次的傳入會受前一次傳出而有不同,以此類推)
Extension Method 也蠻值得討論的,不過微軟官方文件已經寫得蠻清楚了,詳細可以看看下方連結!
💭 [擴充方法 (C# 程式設計手冊)](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/classes-and-structs/extension-methods)
## 參考
💭 [利用 Swift 5.1 新功能實作 Fluent Interface 讓程式碼更易讀流暢!](https://www.appcoda.com.tw/fluent-interface/)
💭 [[.NET] Fluent Interface: 實作 Method Chaining 又不會有耦合性的作法](https://dotblogs.com.tw/regionbbs/2012/01/12/fluent_interface_in_net)
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以讓大家初步了解 Fluent Interface 及其實作,其實它就是運用一些 OO 的基本觀念去改變程式碼樣貌,進而使程式碼逐漸變得像我們人所說話一樣,就好像我們寫文章一樣,每個人都有自己的”寫作”風格哦!
---
title: 'Command Prompt / Windows Powershell 預設使用 UTF-8 編碼'
description: '解決 Windows 惱人的亂碼問題'
ogImage: './hero.jpg'
slug: 'command-prompt-and-windows-powershell-default-use-utf-8'
date: '2020-02-21'
drafted: false
featured: false
topic: 'command-line'
tags: ['windows', 'command-prompt', 'windows-powershell']
authors: ['neil-tsai']
---
此文主要**協助各位調整 Command Prompt / Windows Powershell 預設使用 UTF-8 編碼**,有興趣就往下看吧!
## 前言
相信每個工程師應該都會碰到**需要下指令**的時候,但多少都會碰到**中文亂碼**這件事,這時就會準備直接打開瀏覽器搜尋 “XXXX 中文亂碼” 等關鍵字,而且這種問題好像比較多都是華人會碰到,畢竟多數程式在設計就會是以 UTF-8 編碼為主,因其受眾也比較多的關係。

Default Code Page
所以今天就來談談 Win 10 環境下,如何調整 Command Prompt / Windows Powershell 預設使用 UTF-8 編碼 。
## 臨時修改
```bash
> chcp 65001
```
僅調整**當下 Process**(Command Prompt / Windows Powershell)的字碼頁(CodePage)為 65001。

CodePage 65001 now actived
## 永久修改
### Command Prompt
💭 [在命令提示視窗(Command Prompt)顯示UTF-8內容](https://blog.darkthread.net/blog/command-prompt-codepage/)
上文中有關 davidhcefx 的回覆解法,的確可以讓 Command Prompt 預設 UTF-8 編碼,而不用每次都還要改字碼頁,下面就稍微圖解一下:
1. Windows 搜尋 `regedit`
2. 前往 `HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor` 該位置

3. 於第二點提到之位置底下新增**字串值**,**數值名稱**為 **Autorun**,**數值資料**為 **chcp 65001>nul**

此時再次創建一個新的 Process,預設字碼頁就是 65001 了。

CodePage 65001 now actived
### Windows Powershell
如果 Windows Powershell 要預設使用 UTF-8 編碼相比於 Command Prompt 就複雜得多… 畢竟微軟出品 (´\_ゝ`)
💭 [Using UTF-8 Encoding (CHCP 65001) in Command Prompt / Windows Powershell (Windows 10)](https://stackoverflow.com/questions/57131654/using-utf-8-encoding-chcp-65001-in-command-prompt-windows-powershell-window)
上文有比較詳細的討論,下面這邊就稍微圖解一下:
1. 確認 `[console]::OutputEncoding`、`[console]::InputEncoding`、`$OutputEncoding` 這些變數

2. 確認 `$PROFILE` 變數

3. 新增 `Microsoft.PowerShell_profile.ps1` 至第二點提到之位置 `C:\Users\UserName\Documents\WindowsPowerShell` 底下

4. `Microsoft.PowerShell_profile.ps1` 內容如下:
_$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = [Text.UTF8Encoding]::UTF8_

此時再次創建一個新的 Process,預設字碼頁就是 65001 了。

CodePage 65001 now actived
回過頭來看 PowerShell 為什麼可以透過 Profile 檔案來客製化 Process 環境?其實也沒有什麼特別的原因,答案盡在 PowerShell 官方文件 [About Profiles](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7),它設計上本來就可以利用 Profile 檔案來客製 Process 環境 !
以上例來看,我僅**侷限在當前的登入者呼叫 PowerShell 要去幫我先跑 Profile 檔案**,若換了另一個登入者呼叫 PowerShell 就不會特別先去跑 Profile 檔案,因為另一位登入者並沒有照我們上述做了那些事情!
當然也可以預設讓所有登入者都去跑同一個 Profile 檔案,這部分就參考官方文件囉~
···
不過還有一件特別需要注意的事情,就是 PowerShell 在執行指令時要特別小心編碼問題!
💭 [PowerShell 執行非 .NET 程式在輸出資料時要注意編碼問題](https://blog.miniasp.com/post/2009/06/23/Unmanaged-Program-Out-File-Encoding-Problem-in-PowerShell)
你可能會覺得很奇怪?阿不是我們都調整好 UTF-8 編碼了嗎?
我創建了一個名為 test 的文字文件檔且編碼為 UTF-8,如下圖:

WTF
如果沒特別給 encoding 參數,就會顯示亂碼,我也是覺得蠻黑人問號 (›´ω`‹ ) …
算是先留下一個洞,未來有再碰到類似問題,再回來解答吧~
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以解決掉大多數人對於 Command Prompt / Windows Powershell 預設使用 UTF-8 編碼的困擾哦◝( ゚∀ ゚ )◟
---
title: '初探 Oracle exp & imp 指令使用方式'
description: 'Oralce 大量資料匯入匯出方式'
ogImage: './hero.png'
slug: 'first-time-meet-oracle-exp-and-imp'
date: '2020-02-07'
drafted: false
featured: false
topic: 'oracle'
tags: ['command', 'exp', 'imp', 'data-access']
authors: ['neil-tsai']
---
此文主要**初步了解 Oracle exp & imp 指令使用方式**哦,有興趣就往下看吧!
## 前言
最近在協助部門將**某一系統於客戶環境的測試機資料,下檔到公司環境測試機**,希望可以重現該系統的行為,以便於我們團隊之後在公司的測試便利性。
但有個直接問題浮現了,畢竟這個系統已上線許久,即便是**測試機資料也是爆多,光是 Table 就有將近 1000 ~ 2000 個,甚至有些 Table 資料筆數超過 100 萬的都有**,這要一個一個過肯定會◢▆▅▄▃崩╰(〒皿〒)╯潰▃▄▅▇◣
不過既然身為軟體工程師,這時候一個轉念(絕對不是什麼懶得做什麼的),便可豁然開朗,原來 Oracle 這麼佛已經有想到**可能使用者會需要大量匯入匯出資料這件事**,咦?它們的專業應該就是在資料庫管理啊(直接被打臉
## Oracle Original Export and Import
> This chapter describes how to use the original Export and Import utilities, invoked with the `exp` and `imp` command, respectively. These are called the original Export and Import utilities to differentiate them from the Oracle Data Pump Export and Import utilities available as of Oracle Database 10g. The Data Pump Export and Import utilities are invoked with the `expdp` and `impdp` commands, respectively.
> [Oracle Docs – Original Export and Import](https://docs.oracle.com/cd/B28359_01/server.111/b28319/exp_imp.htm#g1070082)
引述一段從 Oracle 官方文件對於 **Original Export and Import** 的描述,其中提到使用 **exp** 及 **imp** 指令的均意指為 Original Export and Import,而後於 Oracle Database 10g 推出的 **Data Pump Export and Import** 則是使用 **expdp** 及 **impdp** 等指令,文末會在對比這兩者差異可能有哪些,往下看吧!
## exp & imp 實際情境演練
### 匯出

Use exp command
**匯出特定幾個 Table 的指令([匯出參數可參考官方文件](https://docs.oracle.com/cd/B28359_01/server.111/b28319/exp_imp.htm#CEGFIAGE)):**
```bash
> exp UserName/Password@ServiceName file='ExportPath.dmp' log='ExportLog.log' direct=y statistics=none tables=(Table1, Table2,...)
```
- UserName:使用者名稱
- Password:密碼
- ServiceName:服務名稱
- ExportPath:匯出 dmp 檔路徑(資料)
- ExportLog:匯出 log 檔路徑
- Table1, Table2,…:多個 Table 名稱
**以上需要自行依照需求替換值哦!**
部份參數介紹
- file:如果沒有特別去搭配 filesize 參數的情況下,只會匯出一個 dmp 檔,反之則可以於 file 參數定義多個 dmp 檔名稱(依據所設定 filesize 去切割)
- log:記錄指令執行途中狀況
- direct:[是否直接路徑匯出](https://blog.csdn.net/leshami/article/details/9146023)
- statistics:統計資訊
- tables:語法範本 schemaname.tablename:partition_name

Exported dmp & log file
### 匯入

Use imp command
**匯入特定幾個 Table 的指令([匯入參數可參考官方文件](https://docs.oracle.com/cd/B28359_01/server.111/b28319/exp_imp.htm#i1021478)):**
```bash
> imp UserName/Password@ServiceName file='ImportPath.dmp' ignore=y full=y
```
- UserName:使用者名稱
- Password:密碼
- ServiceName:服務名稱
- ImportPath:欲被匯入的 dmp 檔(資料)
**以上需要自行依照需求替換值哦!**
部份參數介紹
- file:如果沒有特別去搭配 filesize 參數的情況下,可以只匯入一個 dmp 檔,反之則可以於 file 參數定義多個 dmp 檔名稱(依據所設定 filesize 去分次匯入)
- ignore:若 Table 物件已存在,仍然會匯入資料(即便在匯入途中發生錯誤也不會中斷)
- full:匯入整個 dmp 檔
## exp & expdp 指令差別?
其實這要回到上述所提到的 **Original Export and Import**(前者) 和 **Data Pump Export and Import**(後者) 之間的區別,畢竟兩者使用的元件不同。
1. **前者**可用於客戶端及伺服器端,當然客戶端那邊要能夠連線至伺服器端,而**後者**僅能使用於伺服器端
2. 若由 expdp 匯出之檔案,則無法使用 imp 來匯入,反之亦然
3. 若使用 expdp 和 impdp 在輸入指令時,並不像 exp 和 imp 需要先打上身分(UserName/Password@ServiceName)
💭 [expdp和exp 的區別和使用](https://kknews.cc/zh-tw/code/nk83rn2.html)
## 參考
💭 [IMP: ORA-12560 TNS:protocol adapter error](http://oracle-db-faq.blogspot.com/2009/02/imp-ora-12560-tnsprotocol-adapter-error.html)
💭 [15 Oracle Exp Command Examples to Export Database Objects](https://www.thegeekstuff.com/2016/02/oracle-exp-examples/)
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以解決掉大多數人對於 Oracle 匯入匯出大量資料的困擾哦◝( ゚∀ ゚ )◟
---
title: 'Installer Projects'
description: 'ASP.NET 網站簡易安裝方式'
ogImage: './hero.jpeg'
slug: 'easy-to-use-web-setup-project'
date: '2020-01-11'
drafted: false
featured: false
topic: 'visual-studio'
tags: ['visual-studio-extension', 'installer-projects', 'deploy']
authors: ['neil-tsai']
---
## 前言
小弟前幾個月又有接到一個任務,這個要求是「有沒有辦法可以將**網站專案**包成安裝檔?」,因為團隊這邊想要盡可能達到自動化的方式來部署網站(手動情況)···
其實網路上有蠻多選擇,像是有 Advanced installer 、 Windows Installer XML(WiX Toolset)都是不錯的解決方式,但我在想有沒有更單純的方式來處理,於是腦筋動到了微軟 Installer Projects 上,因為只要在 Visual Studio 擴充功能市集就能找到並安裝!

Microsoft Visual Studio Installer Projects
## 大致步驟
1. Visual Studio Marketplace 搜尋 Installer Projects 並安裝
2. 已有建立的網站專案(Web Project)
3. 新增網站安裝專案(Web Setup Project)並加入專案輸出(主要輸出、內容檔···其餘可視專案情況)
4. 取得安裝檔並且安裝
5. 測試網站可否運行
## 簡易教學
Visual Studio Marketplace 搜尋 Installer Projects 並安裝

Extension and Updates ➡ Online ➡ Search Installer Projects
示範建立 ASP.NET Web 應用(以 Web Form 架構為例,同理 MVC 架構)

Create ASP.NET Web Application

Test to run web application by IIS Express
示範建立網站安裝專案

Create Web Setup Project
初始的方案結構

Visual studio solution structure
加入專案輸出(主要輸出、內容檔)

Add project output

Add main output

Add content files
現在的方案結構

Now visual studio solution structure
重建網站安裝專案

Rebuild web setup project
💭 [Difference between a Debug and Release build](http://net-informations.com/faq/net/debug-release.htm)

packaging file···
取得安裝檔

Get .msi & .exe file
💭 [Understanding the Difference Between .exe and .msi](https://www.symantec.com/connect/articles/understanding-difference-between-exe-and-msi)
嘗試安裝(Double click ExampleWebSetup.msi)


到此要稍微注意幾點:
**網站**:選擇已建立的 IIS 網站(IIS Web Site to choose)
**虛擬目錄**:定義 IIS 應用程式名稱(IIS Application Naming)
**應用程式集區**:選擇已建立的 IIS 應用程式集區(IIS Application Pool to choose)

查看 Internet Information Services 是否已建立應用程式

測試連上本地端網站

安裝於 Windows 上的網站資料夾結構可能像是這樣···

## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
其實看到最後你會發現**網站資料夾結構**跟我直接從專案建置後拿出來部署一模一樣,這也就代表說微軟本身就有提供一個比較簡易的方式可以部署網站,而不用擔心從專案建置後拿來部署又漏東漏西。
但從上面看下來這還不是真正的一鍵安裝╮(╯_╰)╭,還有一段路要走···
如果有機會,或許以後再來補補其他包裝的方式···
---
title: 'ODP.NET 位元版本連線異常排除'
description: '解決 ODP.NET 位元版本連線異常問題'
ogImage: './hero.jpeg'
slug: 'odp-net-version-connection-problem-fixed'
date: '2020-01-11'
drafted: false
featured: false
topic: 'oracle'
tags: ['odp-net', 'error-handling', 'data-access']
authors: ['neil-tsai']
---
## 前言
小弟我最近有在協助公司新進人員排除一些 Oracle 資料庫連線問題,有碰到一個用 **Sql Developer 可以連線**,但是**應用程式使用 ODP.NET 卻無法連線**的情況···
## 狀況釐清
1. 應用程式編譯目標平台為 Any CPU
2. 已安裝 ODP.NET 64 位元版本
## ODP.NET 問題
應用程式執行時丟出來的錯誤,感覺上是 Spring 在注入時有發生異常···

Application Throw Exception
## ODP.NET 問題排除
後來問了有經驗的同仁,他說可以確認看看 IIS Application Pool 的**進階設定**,其中裡面的**啟用 32 位元應用程式**是 True 還是 False。

修改前
**如果這個設定被設定為 True,則須要安裝 32 位元版本相容的 ODP.NET,應用程式才可正常運行!**

修改後
因為我的應用程式目標平台不是瞄準在 32 位元,所以就將此開關調整為 False,後來就能正常執行應用程式了。
## 結尾
之前就有稍微研究過 ODP.NET 是怎麼樣去連線 Oracle 資料庫,可參考下文···
💭 [什麼是 Oracle Data Access Components?](/posts/what-is-oracle-data-access-components)
結果還是被 **ODP.NET 位元版本問題**搞到不能自己_(┐「ε:)_
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
本文目的是希望如果有遇到類似問題,可以嘗試藉此排除看看!節省一點時間···
## 參考
💭 [【茶包射手日記】ODP.NET如何找對Oracle Client檔案?](https://blog.darkthread.net/blog/how-odp-net-find-oracle-client/)
---
title: 'HttpRuntime ExecutionTimeout & IIS Site ConnectionTimeout 差異'
description: '深入了解 ExecutionTimeout & ConnectionTimeout'
ogImage: './hero.jpeg'
slug: 'what-is-the-difference-between-executiontimeout-and-connectiontimeout'
date: '2020-01-11'
drafted: false
featured: false
topic: 'iis'
tags: ['execution-timeout', 'connection-timeout']
authors: ['neil-tsai']
---
## 前言
最近在公司協助解決問題時,碰到一個問題描述為「**前端發 Request 1分鐘後就會回傳失敗**」且是在**客戶環境**,聽到的狀況是 **Response 回傳 Http status 為 504**,所以就有**初步懷疑可能是客戶網路有什麼限制···**
💭 [504 Gateway Timeout Error](https://www.lifewire.com/504-gateway-timeout-error-explained-2622941#targetText=The%20504%20Gateway%20Timeout%20error,another%20request%20by%20the%20browser)
不過當然身為工程師,還是得要窮盡可能去嘗試看看自己是否有哪邊沒有做到位,於是乎我先把遇到的狀況同步給對方 IT 幫忙看看是否能排除問題···
回到主題,我一開始還真的不清楚 executionTimeout 和 connectionTimeout 的區別是什麼 Σ(゚Д゚;≡;゚д゚) 查了一些資料才比較明白···
## 👉 ExecutionTimeout
> web.config > system.web > httpRuntime

web.config

Microsoft Docs
**有幾點可以特別注意:**
1. 長度限制為有號 32 位元。
2. 可以決定 Server 端針對這一 Request 最大能處理的時間。
3. Compilation 元素內的 debug 屬性會影響其生效。
4. 因 .NET Framework 版本預設值會有差異。
### 實際小演練
> debug = true

web.config
**開發環境我不希望發生有執行逾時(Execution Timeout)的情況發生**,所以 Compilation 元素內的 debug 屬性設為 True。
> debug = false

web.config
**測試環境我希望發生有執行逾時(Execution Timeout)的情況發生**,所以 Compilation 元素內的 debug 屬性設為 False。

Logged message
如果這個要求(Request)在 Server 超過 executionTimeout 所設置的時間,則會回傳 Http status 500 給前端。
### 參考來源
💭 [httpRuntime Element (ASP.NET Settings Schema)](https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/e1f13641(v=vs.100)?redirectedfrom=MSDN#Anchor_0)
💭 [compilation Element (ASP.NET Settings Schema)](https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/s10awwz0(v=vs.100))
💭 [ASP.NET MVC 開發心得分享 (22):關於 executionTimeout](https://blog.miniasp.com/post/2011/09/08/ASPNET-MVC-Developer-Note-Part-22-About-httpRuntime-executionTimeout)
## 👉 ConnectionTimeout
> IIS 網站(Site) > 進階設定(Advanced Settings) > 連線限制(Connection Limits)

IIS

Microsoft Docs
**有幾點可以特別注意:**
1. 可以決定 client 與 server 建立的 TCP 連線在中斷前可保持非使用中的時間。(簡單說就是這條被建立的連線在非活耀狀態下可以持續的時間,過後則關閉連線)
2. 描述三種狀況會被視為非活耀連線狀態
3. 預設為 120 秒(2 分鐘)
### 參考來源
💭 [Default Limits for Web Sites <limits>](https://docs.microsoft.com/en-us/iis/configuration/system.applicationHost/sites/siteDefaults/limits#005)
💭 [[IIS][ASP.net] 連線逾時,Session Timeout的設定](https://dotblogs.com.tw/shadow/2017/09/14/195114)
💭 [HTTP keep-alive連線](https://zh.wikipedia.org/wiki/HTTP%E6%8C%81%E4%B9%85%E8%BF%9E%E6%8E%A5)
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
希望大家對於 executionTimeout 及 connectionTimeout 的定義可以做出區別並有初步認識,兩者並不是指同一件事情哦···
謎之聲:這個不是看英文就知道了(´・_・`)?
我:···
---
title: 'BlazeMeter'
description: 'JMeter 腳本錄製插件初探'
ogImage: './hero.jpeg'
slug: 'jmeter-test-script-recorder-blazemeter'
date: '2020-01-05'
drafted: false
featured: false
topic: 'browser-extension'
tags: ['chrome-extension', 'jmeter', 'blazemeter', 'stress-test']
authors: ['neil-tsai']
---
如何簡單錄製 JMeter 測試腳本,試試 Chrome 擴充功能的 BlazeMeter 吧,讓我們看下去!!
## 前言
小弟所在的團隊最近將要上線一個新系統,不外乎要做**壓力測試**這件事情,但對於我來說卻是很陌生,甚至連怎麼做都不太清楚,所以藉著這次機會詢問了在北部的同仁,因他近期常駐點在客戶端,對這件事必然需要有一定了解,以便在將來我也能夠處變不驚處理這件事 σ`∀´)σ
但此文主旨會先放在**如何簡單錄製 JMeter 測試腳本**這塊,畢竟壓力測試要做得事情還有很多呢 ◢▆▅▄▃崩╰(〒皿〒)╯潰▃▄▅▇◣
## BlazeMeter 介紹

BlazeMeter Web Site

BlazeMeter Features & Requirements
有幾點可以特別注意:
1. 可記錄**使用者行為**及**這一行為下所發出的要求(Requests)**
2. 兼容 Apache JMeter
3. 可以**不需要測試工具**就能執行測試(這部分本文不會特別說明細節)
4. 有些功能**需要登入**才能使用
5. 部分功能使用上**有最低 Chrome 版本限制**
## BlazeMeter 安裝步驟
Chrome 線上應用程式商店(Chrome Web Store)➡️ 搜尋 BlazeMeter ➡️ 安裝擴充功能 ➡️ 安裝完成

Chrome Extensions – BlazeMeter
## 使用說明
安裝完後瀏覽器右上角會有 BlazeMeter 擴充功能。

BlazeMeter Extensions
點開後長這樣。

Initial Status
1. 停止記錄(Stop recording)
2. 開始記錄(Start recording)
3. 重設選項(Reset all options)
4. 客製選項(Advanced options)
假設我要測試登入流程的話,點擊”紅圈圈”就會開始記錄囉。

Record of Login Flow
看到以下畫面時,表示已經開始記錄了,這時候要開始操作系統流程!

Recording…
嘗試登入行為。

Try to Login
如果覺得已經 OK 了,按下”紅色方塊”停止記錄。

Recording End
已經錄製完畢了!

End of Record
### Run
會將測試腳本上傳至雲上並實際去跑(視需求決定,不一定要這麼做)。

“Run” Function list
### Edit
可以再編輯測試腳本。

“Edit” Function list
### Save
匯出測試腳本。

“Save” options

Export JMeter File

Import JMeter File
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
這次跟大家介紹了**從瀏覽器透過了解使用者行為並匯出測試腳本**是這樣一件單純的事情,節省了不少時間呢(ゝ∀・)b
所以本文目的是希望大家可以快速了解 BlazeMeter 是什麼樣的工具及如何能夠快速去使用它,希望對大家是有幫助的!ヽ(✿゚▽゚)ノ
## 參考
💭 [Day 20 Jmeter 壓力測試工具](https://ithelp.ithome.com.tw/articles/10203900?sc=hot)
---
title: 'ORA-12154: TNS: 無法解析指定的連線 ID'
description: '解決 ORA-12154 連線 ID 解析失敗問題'
ogImage: './hero.jpeg'
slug: 'ora-12154'
date: '2020-01-05'
drafted: false
featured: false
topic: 'oracle'
tags: ['ora-error', 'error-handling', 'data-access']
authors: ['neil-tsai']
---
## 前言
小弟我最近從原本 VM 開發環境要搬回本機開發環境時,是的沒錯不外乎就是把環境恢復到可以開發應用的程度,**其中在處理和 Oracle 資料庫連線時,發生了「ORA-12154: TNS:could not resolve the connect identifier specified」◢▆▅▄▃崩╰(〒皿〒)╯潰▃▄▅▇◣**
## 情境
裝完 ODAC 之後,想說試試看正在開發中的系統能不能連線至 DB Server···
💭 [什麼是 Oracle Data Access Components?](/posts/what-is-oracle-data-access-components)
想當然爾果然是不行 (´・_・`)

Error Message

Oracle Sql Developer
神奇了我的天,讓我們繼續看下去···
## ORA-12154 可能的除錯方向
1. 應用程式設定檔(*.config)connectionStrings 是否有打錯?(理論上會和 Oracle Client 下 network/admin/tnsnames.ora 設定有對應)
2. Oracle Client 下 network/admin/tnsnames.ora 是否檔案存在?
3. Oracle Client 下 network/admin/tnsnames.ora 是否有被存取的權限?
4. Oracle Client 下 network/admin/tnsnames.ora 內容設定是否有誤?
## ORA-12154 狀況排除
### 案例情境
1. 使用 ODP.NET 幫助應用程式可以連線至 Oracle 資料庫
2. 開發環境需要安裝 ODAC
**必須先備註一下,此案例是我遇到的狀況,但應該大同小異哦。**
首先我先確認一下設定檔,看起來沒問題···

*.config
接著來確認 Oracle Cilent 下 network/admin/tnsnames.ora ···

哦,原來是沒有檔案的部分阿 (´・_・`)
因為 ODAC 安裝完後,需要自己從 Sample 資料夾把 sqlnet.ora 和 tnsnames.ora 寫好後丟出來外層資料夾···
## Tnsnames.ora 初步設定

tnsnames.ora
1. 資料來源別名(Data source)
2. 主機位址或 IP(Host name or IP)
3. 連接埠(Port)
4. 資料庫服務名稱(Database service name)
設定完搬到外層後,應用程式就能正常連線囉(如果還不行,可以試試重開機)。

## 延伸
你/妳可能會問為什麼上面都做完之後,應用程式就有辦法連線阿!?
這邊先說一點點哦,有興趣可以上網查查相關資料。
首先我們在開始功能列搜尋「regedit」查看註冊表,以 ODP.NET 來說···
HKEY_LOCAL_MACHINE\SOFTWARE\ORACLE\ODP.NET\[version] 位置下可以看到 **Key 為 DllPath** 標註的 **value 為 Oracle Client 下的 bin 實體路徑**。

因為 **ODP.NET** 實際背後是用 **Oracle.DataAccess.dll** 去操作資料庫行為,最終會去爬這個路徑的相關 DLL 來達到實際操作的目的,但這也是一個**很大的缺點,就是會去依賴註冊表**!!
👍 **所以現在大部分的應用程式如果要連接 Oracle 資料庫,可以改用 Managed ODP.NET 去實作!!**
下面這文就有提到一個修改方式,正是用 Managed ODP.NET 的做法。
💭 [ORA-12154: TNS: 無法解析指定的連線 ID](https://medium.com/@saltoscer/ora-12154-tns-%E7%84%A1%E6%B3%95%E8%A7%A3%E6%9E%90%E6%8C%87%E5%AE%9A%E7%9A%84%E9%80%A3%E7%B7%9A-id-eae0ae67e206)
但他怎麼沒設定幾行就能連線,好像又更簡單了!?
對 Managed ODP.NET 來說,它使用的元件是 **Oracle.ManagedDataAccess.dll** ,但是它對於尋找 TNS 有一套機制且是有順序的。

所以註冊表上只有 **Key 為 TNS_ADMIN** 標註的 **value 為 network/admin/tnsnames.ora 的實體路徑**,但其實會視情況才會使用到,所以 **Managed ODP.NET 不會強制依賴註冊表**!!
以上區別。
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
希望這篇文章可以解決掉大多數人對 ORA-12154 的疑問跟狀況排除◝( ゚∀ ゚ )◟
---
title: 'AxoCover'
description: '.NET 分析程式碼覆蓋率工具'
ogImage: './hero.jpeg'
slug: 'dot-net-code-coverage-tool-axocover'
date: '2020-01-03'
drafted: false
featured: false
topic: 'visual-studio'
tags: ['visual-studio-extension', 'axocover', 'unit-test', 'code-coverage']
authors: ['neil-tsai']
---
## 前言
AxoCover 是小弟最近在使用的**程式碼涵蓋率分析工具**,因為最近在幫既有專案撰寫單元測試,以往公司做法是透過 Jenkins 處理 CI/CD 會一併執行專案所寫的單元測試及分析程式碼覆蓋率並產出一份報告,但**有一個比較尷尬的問題就是需要先把程式碼 push 至遠端,這樣 Jenkins 才會被觸發要去處理這件事情(有設定好的情況下)**,但是**我想在本地端就能先看大致跑出來的涵蓋率結果**,這樣或多或少就能減少耗用 Jenkins 資源,畢竟公司專案很多,確實這個工具也給了我不少幫助!讓我們往下看吧!

SonarQube Coverage Report
**蠻少人討論**這個工具,但實際用了之後感覺回不去的工具(〃∀〃)
## AxoCover 介紹

Microsoft Marketplace AxoCover Download Page
有幾點可以特別注意:
1. 屬 Visual Studio 擴充功能
2. 支援 .NET Framework 專案(不支援 .NET Core 及 Xamarin 專案)
3. 涵蓋狀況呈現於編輯器
4. 可輸出涵蓋率結果於靜態 HTML 上
5. 支援 MSTest、xUnit、NUnit 測試框架
## AxoCover 安裝步驟
打開 Visual Studio ➡️ 工具 ➡️ 擴充功能和更新 ➡️ 線上 ➡️ 搜尋 AxoCover ➡️ 安裝 ➡️ 重開 Visual Studio ➡️ 完成安裝!!

Extension and Updates ➡ Online ➡ Search AxoCover
## AxoCover 使用說明

Tools ➡ AxoCover
點擊後 IDE 右側應該會出現該工具的頁籤,一開始它會去讀取整個 Solution 確認是否有單元測試專案或程式,如果沒有找到會像下面這樣:

AxoCover Not Found Unit Tests
如果發生明明有單元測試專案或程式,但依然沒有被找出來的情況,這時可以試試看**重建專案讓該工具重新讀取**。

AxoCover Founded Unit Tests
## 功能介紹

AxoCover Function list
### Tests
#### Run
編譯後執行單元測試(**不含**涵蓋率分析)。

Build Solution And Run Unit Tests
#### Cover
編譯後執行單元測試(**含**涵蓋率分析)並產生 coverageReport.xml 放於 Solution 下的 .axoCover/runs/run_YYYY–MM–DD_HH–MM–SS 資料夾內。

.axoCover

runs

coverageReport.xml
#### Build
編譯(**不含**執行單元測試**及**涵蓋率分析)
#### Collapse
收合頁籤內容
### Report

Coverage Report
#### Import
可匯入 OpenCover coverage reports(*.xml)格式資料 。
#### Export
依據當前 Coverage Report 匯出靜態 HTML Summary Report(可瀏覽器觀看)。

Click “Export” Generate Summary Report

reports

index.htm

Summary Report Page
#### Sort(Alphabetical 、Uncovered Code、Coverage)
依照字母、未涵蓋程式碼、涵蓋率排序
#### Collapse
收合頁籤內容
### Settings

Settings
#### Visualization
根據需求調整視覺呈現部分。
綠(Green):被覆蓋程式碼(Covered lines)
黃(Yellow):部分覆蓋程式碼(Part of covered lines)
紅(Red):未覆蓋程式碼(Uncovered lines)
小圈圈(Circle):條件判斷涵蓋狀況(Covered condition situation)
空心圈圈(Hollow Circle):未涵蓋條件(Uncovered condition)
實心圈圈(Solid Circle):已涵蓋條件(Covered condition)

Notice Inner Red Area
#### Coverage
根據需求可決定測試涵蓋率分佈到什麼程度。
#### Output directories
可直接移動到輸出目錄或清除目錄資料。
#### Test settings
可設定跑測試的目標平台 x86 或 x64 … 等等相關設定。
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
雖然透過 CI/CD 整合 SonarQube 後一樣能做到觀察程式涵蓋率這件事,但如果能在開發時就能初步知道自己寫的單元測試能涵蓋到什麼程度也不是一件壞事哦(ゝ∀・)b
所以本文目的是希望大家可以快速了解這是什麼樣的工具及如何能夠快速去使用它,希望對大家是有幫助的!ヽ(✿゚▽゚)ノ
## 參考
💭 [[VisualStudio] .NET 分析測試代碼覆蓋率 AxoCover](https://marcus116.blogspot.com/2019/03/visualstudio-net-axocover.html)
---
title: '什麼是 Oracle Data Access Components?'
description: '深入了解 Oracle Data Access Components'
ogImage: './hero.jpeg'
slug: 'what-is-oracle-data-access-components'
date: '2020-01-01'
drafted: false
featured: false
topic: 'oracle'
tags: ['odac', 'oracle-client', 'data-access']
authors: ['neil-tsai']
---
## 前言
小弟剛進公司時,前輩們已有準備開發用 VM (很多設定我們都不需要再處理),接著很快就下專案維運了,但也因此少知道了很多事情,其中一件事就是這個 ODAC (Oracle Data Access Components),那時候就在想···
1. .NET 應用程式怎麼操作 Oracle 資料庫?
2. 為什麼裝完 ODAC 並設置完就可以運作?
3. 到底是要裝 Oracle Client 還是 ODAC 好呢?
相信你/妳應該有類似的疑問,於是這篇文章就誕生了,讓我們往下看吧!
## Oracle Data Access Components 官方描述
> From Oracle Data Access Components Documentation 12c Release 3 (12.1.0.2.1) Overview
>
> Oracle Data Access Components (ODAC) are a set of Windows and .NET data access drivers and tools. They include support for .NET, COM, and ODBC data access; Microsoft Visual Studio tools for developing Oracle database applications; ASP.NET providers; and .NET stored procedure support. ODAC provides comprehensive client support for advanced Oracle database functionality, including performance, high availability, and security, among other features. It is tightly integrated with Visual Studio and Windows to provide a seamless development experience for Windows developers.
簡單來說當我們在開發 .NET 應用程式時會需要對 Oracle 資料庫進行相關操作時則會需要使用到此組件的相關服務協助我們做到這件事。
## Oracle Data Access Components 安裝方案

Oracle 針對 “使用情況” 給出幾種安裝方案
1️⃣ **專案開發期各版本所需(較輕量)**
- **Oracle Developer Tools for Visual Studio**
- Oracle Data Provider for .NET, Managed Driver
- Oracle Providers for ASP.NET
2️⃣ **專案開發期全版本通用及一些 Oracle 資料庫擴充套件(較重量)**
- **Oracle Developer Tools for Visual Studio**
- Oracle Data Provider for .NET
- Oracle Providers for ASP.NET
- Oracle Database Extensions for .NET — available in 12.2 and earlier for upgrade only
- Oracle Provider for OLE DB
- Oracle OLAP Provider for OLE DB — 18c and higher
- Oracle Services for Microsoft Transaction Server
- Oracle ODBC Driver
- Oracle SQL*Plus
- Oracle Instant Client
3️⃣ **專案後期上線輕量部署( XCopy 版本 )**
- Oracle Data Provider for .NET
- Oracle Providers for ASP.NET
- Oracle Provider for OLE DB
- Oracle OLAP Provider for OLE DB — 18c and higher
- Oracle Services for Microsoft Transaction Server
- Oracle Instant Client
從上可以得知[1️⃣、2️⃣]和3️⃣有一蠻明顯的不同點,就是**少了 Oracle Developer Tools for Visual Studio**。
## Oracle Developer Tools for Visual Studio(ODT)
> From Visual Studio Marketplace – Oracle Developer Tools for Visual Studio 2017
>
> Oracle Developer Tools for Visual Studio 2017 is a free VS addin that makes it easy to browse and modify Oracle schema objects and data, edit and debug PL/SQL, generate SQL deployment scripts, perform schema comparisons, tune SQL and .NET app performance, and much more. .
簡單來說就是在 IDE 內讓 Oracle Database 可以像是 SQL Server 那樣簡易操作資料。

資料連接選擇介面

嘗試測試連接
因為對於 Server 端而言確實**不需要**因為需要安裝 Data Provider 就把整個 ODT 給安裝上去,**一是我們很少會在 Server 端開發,二是避免造成管理不易**。
💭 [Oracle ODAC + ASP.NET MVC 佈署](http://kevintsengtw.blogspot.com/2012/11/oracle-odac-aspnet-mvc.html)
## 連線手段
### System.Data.OracleClient(微軟)
元件依然存在 .NET Framework 4.0 內,但該元件已不再被維護,但一定程度內仍能使用該元件功能,但**不建議**再繼續使用囉!!
💭 [System.Data.OracleClient將走入歷史](https://blog.darkthread.net/blog/bye-ado-net-oracleclient/)
### Oracle.DataAccess.Client(甲骨文,ODP.NET)
既然第一種手段已宣告往後不再被維護,則我們將焦點移動到 Oracle 自家推出的 ODP.NET,也因 Oracle 時常會更新,所以相較於微軟推出的元件,自然會持續維護元件程式碼。
**ODP.NET 有幾點可以特別注意:**
1. 不需裝肥肥的 Oracle Client
2. 需配置 tnsnames.ora
3. 使用上有區分 32 bit / 64 bit

tnsnames.ora
### Oracle.ManagedDataAccess.dll(甲骨文,Managed ODP.NET)
因為已有先知道 ODP.NET 會自己去找 32 bit / 64 bit 對應 Oracle Client 版本的機制(找註冊表)。
💭 [【茶包射手日記】ODP.NET如何找對Oracle Client檔案?](https://blog.darkthread.net/blog/how-odp-net-find-oracle-client/)
那假設 Server 端有很多 Oracle Client 版本,可能會因為 ODP.NET 自有的找檔機制,然後又因為 Server 端**人員行為管理不慎,很可能導致預期外的問題···**
所以 Oracle 自家推出了 Managed ODP.NET 來改善上述可能會發生的狀況。
**Managed ODP.NET 有幾點可以特別注意:**
1. 不需安裝 ODAC(相當等於不用去裝 Oracle Client)
2. 一顆 Oracle.ManagedDataAccess.dll 就能搞定連線
3. 不必再區分 32 bit / 64 bit
💭 [Managed ODP.NET簡介](https://blog.darkthread.net/blog/managed-odp-net/)
## 比較
從上述三種連線手段分析下來,推薦使用依序排的話:
Managed ODP.NET ➡ ODP.NET
**近期開發的應用現在都主打 Managed ODP.NET ,因為它本身目標平台放在 Any CPU,而且也因為它自己有一套解析 TNS 名稱的順序,所以不會像 ODP.NET 會去依賴註冊表,提高蠻大的彈性空間,方便集中控管。**
看到這裡你/妳會覺得奇怪,怎麼 System.Data.OracleClient 的手段不在排序上?因為阿······現在已不推薦使用,所以就沒有列上去囉。
## 結尾
感謝各位花時間看完此篇小文,如果本文中有描述錯誤,還請各位指教。
自己對 ODAC 有了新的認識,然後從原本的 VM 開發環境搬回本機開發環境,終於不用再分效能給 VM 了ヽ(・×・´)ゞ
雖然多數內容都已有詳細資料參考,所以本文列出了關於 ODAC 幾點**較重要**的部分,希望可以幫助對 ODAC 有疑惑的人快速去了解,可以**視情況去應用所知,省時又省力呢**◝( ゚∀ ゚ )◟