﻿# ASP.NET Core MVC 的 Razor 語法對於 @ 符號的編碼陷阱

前幾天我們公司有個網站準備上一個新版，工程師發出 PR (Pull request) 之後請我進行程式碼審核(Code Review)，結果我發現他本次的修正項目不太合理，他改了一個前端套件的資料夾名稱。我當下覺得不妙，一個已經測試數月，也準備上線的網站，怎麼還會在最後一刻修改資料夾名稱呢？我反問工程師為什麼要改這些地方時，卻得到一個我不是很滿意的回答，因此退件請他重改。這不是一個什麼嚴重的技術問題，改改資料夾名稱就正常了，但是卻又激起我追根究底的柯南精神，真相只有一個，我要找出來！🕵️‍♂️

![A wide abstract conceptual image representing coding challenges in ASP NET Core MVC with Razor syntax, featuring a maze-like structure of interconnect](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/2107ba543bf205f87899-8be6dcef-25d9-4465-9b2c-3f8fc17ba433.webp)

### 問題描述

在談論技術問題之前，我想先說說這個問題的來龍去脈。

-   小龍：保哥，我剛剛發了一個PR，再麻煩你了。
    
-   保哥：好喔，給我 PR 網址我看看。你改了些什麼？
    
-   小龍：因為最新版部署後，後台有點問題，無法建立資料，有看到是套件資料夾名稱的問題，目前已修正。
    
-   保哥：咦？你為什麼要改資料夾名稱？還有 `&#64;` 是什麼？這是 `@` 符號的 HTML Entity 表示法吧？為什麼要改？你確定這裡有問題嗎？有這個字元就讀不到嗎？(如下圖)
    
    ![SNAG-0113](https://stwillblogassets.blob.core.windows.net/files/images/external/github.com/0570b8bb48966aae88bd-c6ddaad2-4dd1-48c7-9f9c-1256ba5f1f8e.jpg)
    
-   小龍：我有在正式環境的網頁按檢視原始碼，發現打不開這個 JS 檔案，但是我在本機跟測試機都是正常的，所以我想說先修看看。
    
-   保哥：你改錯了，這不可能是靠改資料夾名稱來解決問題，我先退你的 PR，你先研究一下！
    

### 解析問題

這個問題我大概花了 8 分鐘就想到問題的解法，寫文章的時間比這個多多了！😂

其實工程師小龍已經有多年開發經驗，這點小問題確實也不難解決，但是靠「改資料夾名稱」的解法，我覺得有點不太專業。就算這關過去了，意味著未來所有這類問題都只能靠改資料夾名稱解決，這樣就失去了一次寶貴的學習機會。

在解析問題的過程中，我一貫的態度都是以「合理性」出發，不合邏輯的推論一律先拒絕接受，再去小心求證可能的根本原因。我這 8 分鐘的思考與驗證過程大概是這樣：

1.  這是一個 ASP.NET Core MVC 專案，小龍發出的 PR 主要是調整 JavaScript 與 CSS 的載入路徑，修改的檔案主要以 `*.cshtml` 為主。
    
2.  既然修改的地方是以下這段 JavaScript 載入的地方，又跟 `&#64;` 有關係，為什麼在**本機電腦**與**測試環境**可以正常讀取，但**正式環境**卻不行？
    
    ```html
    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
    ```
    
    > 這段 HTML 實際上瀏覽器解析出來的路徑為 `/lib/@microsoft/signalr/dist/browser/signalr.js`
    
3.  由於 `&#64;` 主要是由「瀏覽器」進行解析，並不是透過 ASP.NET Core MVC 的 Razor 引擎進行解析，所以這個問題應該是「瀏覽器」的問題。但為什麼在**本機電腦**與**測試環境**可以正常讀取，但**正式環境**卻不行？
    
4.  難道跟 `~` 符號有關係？因為這個符號不是給「瀏覽器」看的，而是 Razor 引擎解析的。這個 `~` 符號用來表示網站的「根目錄」，輸出成 HTML 的時候會自動轉換成網站實際部署的**路徑**或**虛擬目錄**。為什麼在**本機電腦**與**測試環境**可以正常讀取，但**正式環境**卻不行？難道跟 ASP.NET Core 的 `ASPNETCORE_ENVIRONMENT` 有關？我檢查了 `Program.cs` 的地方，確認沒有特殊的 Middleware 對這些路徑有額外處理，理論上不太可能跟 `~` 有關。
    
5.  接著我去**正式環境**的網站看一下實際輸出的 HTML 內容，這裡發現了異狀。Razor 還真的把 `&#64;` 又再進行了一次 HTML 編碼，所以路徑變成了 `&amp;#64;` 開頭，這樣瀏覽器當然就讀不到檔案了，因為路徑是錯誤的。
    
    ```html
    <script src="/lib/&amp;#64;microsoft/signalr/dist/browser/signalr.js"></script>
    ```
    
    > 這段 HTML 實際上瀏覽器解析出來的路徑為 `/lib/&#64;microsoft/signalr/dist/browser/signalr.js` (這是錯誤的路徑)
    
6.  既然是 Razor 額外將 `&#64;` 多做了一次 HTML 編碼，那就不要用 HTML Entity 表示法不就好了？在 Razor 裡面可以直接用 `@@` 來對 `@` 符號進行跳脫處理，這樣問題不就解決了！
    
    ```html
    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js"></script>
    ```
    
    > 注意: 在 Razor 語法中，`@` 有特殊含意，如果寫成 `@microsoft` 會等於要輸出 `microsoft` 這個變數，因此這樣寫法將會無法編譯。因此一定要寫成 `@@microsoft` 才能正確輸出 `@microsoft` 這個字串。
    

是的，只要把 `&#64;` 改寫成 `@@` 就能解決問題！👍

### 疑難待解

問題是解決了沒錯，但是我的疑惑沒有被解開！為什麼在**本機電腦**與**測試環境**可以正常讀取，但**正式環境**卻不行？

我等晚上有空的時候，再次深入追查此問題，這才發現，他原本的程式是這樣寫的：

```html
<environment include="Development">
    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
</environment>

<environment exclude="Development">
    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
</environment>
```

然後他的 PR 改成這樣：

```html
<environment include="Development">
    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js"></script>
</environment>

<environment exclude="Development">
    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
</environment>
```

這下子我明白了，原來小龍沒有意識到網頁中有透過一個 [Environment Tag Helper](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/environment-tag-helper?WT.mc_id=DT-MVP-4015686) 來處理「正式環境」的 JavaScript 載入，所以在**測試璃**與**正式璃**的 JS 載入行為確實不太一樣。

重點是：他把兩個環境的 JS 載入路徑都改成 `@@` 了，其實原本在「開發環境」的那段 JS 載入是沒有錯誤的，但他還是一併修改了，反正一起改了也是可以正常載入！(沒有問題的 Code 為什麼也要改呢？) 😅

所以這個問題的根本原因是，當在「非開發環境」的時候，ASP.NET Core 的 [Script Tag Helper](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/script-tag-helper?WT.mc_id=DT-MVP-4015686) 會因為設定 `asp-append-version="true"` 的關係，去對 JS 檔案加入一個以檔案內容為雜湊的版本號，這樣可以避免瀏覽器快取問題。

-   當在「開發環境」時：
    
    當 Razor 語法為：
    
    ```html
    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js"></script>
    ```
    
    HTML 的輸出為：
    
    ```html
    <script src="/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
    ```
    
    這段輸出是正確的。
    
-   當在「正式環境」時：
    
    當 Razor 語法為：
    
    ```html
    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
    ```
    
    HTML 的輸出為：
    
    ```html
    <script src="/lib/&amp;#64;microsoft/signalr/dist/browser/signalr.js"></script>
    ```
    
    由於加上 `asp-append-version="true"` 之後，ASP.NET Core 的 [Script Tag Helper](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/script-tag-helper?WT.mc_id=DT-MVP-4015686) 無法解析 `&#64;` 這種 HTML Entity 表示法，所以才導致路徑錯誤，抓不到檔案。重點是，Razor 引擎在執行時竟然自動忽略了這個錯誤，輸出了一個無效的 HTML Script 標籤！
    
    當我們將 Razor 語法修改為以下格式：
    
    ```html
    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
    ```
    
    HTML 的輸出就會變成：
    
    ```html
    <script src="/lib/@microsoft/signalr/dist/browser/signalr.js?v=rK9Zx8UOZ-yR2hZ9Ov8dBexROapvuO2mE9jAx7ZIquA"></script>
    ```
    
    因為 ASP.NET Core 的 [Script Tag Helper](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/script-tag-helper?WT.mc_id=DT-MVP-4015686) 可以正確解析實體檔案的路徑，因此輸出的 JS 檔案路徑就成功的加入了一個 `v=` 查詢字串，加入了這個實體 JS 檔案的雜湊值，這才是正確運行的結果！
    

### 結語

在此，我宣告本案偵結，順利找出問題背後的真相！👍

這次事件我也跟小龍分享：「你下次要改 Bug 之前，要先想想合理性。不要球來就打，看到什麼改什麼。」

其實我們身為一個軟體開發人員，有時候不光是要解決眼前的問題，閒暇之餘更應該要思考問題的根本原因，這樣才能避免未來類似的問題再度發生。

我多年顧問與帶人的經驗也告訴我，由於「資深工程師」經驗較多，鬼故事看多了，看什麼都不覺得奇怪，自己心中也有一把尺，很知道這些「常見的問題」該怎樣解決，變成一種老師傅才知道的技藝。但是這些經驗的累積所形成的「觀念」，其實不一定正確，雖然特定類型的問題可以很快的解決，但是當問題稍微有點變化時，就會再度卡關，等實驗出「心得」，就可以再次培養出新的技藝，然後一脈相承的傳給後輩。這樣真的好嗎？😅

現在應該越來越多人靠 AI 在寫 Code，但如何有效的整理軟體開發的**上下文**(Context)是需要**深入思考**的 (俗稱**通靈**)，這部分 AI 很難幫忙，短期內都不可能真正取代軟體工程師。很多時候問題發生時並不是我們眼睛看到的那樣，多想個一兩步，利用人類強大的邏輯推理能力分析一下脈絡，甚至靠 AI 發覺一些新知與解題方向，再搭配一點驗證知識正確性的能力，我們才有可能在 AI 賦能的未來持續維持競爭力！🚀

### 相關連結

-   [Razor syntax reference for ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?WT.mc_id=DT-MVP-4015686)
-   [Script Tag Helper in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/script-tag-helper?WT.mc_id=DT-MVP-4015686)
-   [Environment Tag Helper in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/environment-tag-helper?WT.mc_id=DT-MVP-4015686)
