前言
最近串接一個電子簽核服務,要接「簽署完成」的通知。翻遍文件,關於這個通知欄位的說明只有一句話:
notifyUrl(字串、選填):簽署完成後,會發送通知到這個網址。
就這樣,沒了。POST 的內容是什麼?欄位有哪些?有沒有簽章驗證?打失敗會不會重送?文件通通沒寫。
最近串接一個電子簽核服務,要接「簽署完成」的通知。翻遍文件,關於這個通知欄位的說明只有一句話:
notifyUrl(字串、選填):簽署完成後,會發送通知到這個網址。
就這樣,沒了。POST 的內容是什麼?欄位有哪些?有沒有簽章驗證?打失敗會不會重送?文件通通沒寫。
C# 的列舉,骨子裡就是數字。
Sent = 2 編譯後就是一個整數常數,執行期比對、預設 JSON 序列化、存進 DB,全都是 2。
數字對機器很友善,對人就不是這麼回事了:
前端拿到 2 要自己查對照表,DB 撈出一排 2 和 4 根本不知道是什麼。
所以實務上我們一直在做同一件事:給 enum 補一個「給人看、給外部系統用」的字串。
那直接用成員名稱(Sent)不行嗎?不太行。
它受 C# 識別字規則限制(PascalCase、不能有空格或連字號),
而且哪天重構改名,外部契約就跟著爆炸,不適合直接拿去對外。
於是「用 attribute 給 enum 成員掛一個字串」這個需求,就這樣貫穿了 .NET 二十多年的歷史,
而且每一代序列化技術都自己重新發明了一次。
串接第三方 API,十之八九會遇到字串狀態碼。
以大家都串過的第三方金流服務來說,訂單的付款狀態就是一串小寫字串:
created → pending → paid → refunded → (failed / expired)
一開始 domain model 我就直接用 string 存,然後就踩雷了。
把 API 的「操作狀態 success」塞進了「付款狀態」欄位,編譯器一聲都沒吭。
直到後來查資料,才發現欄位裡躺著一個根本不在狀態清單上的值。
那就改成 enum 吧。但一想到要改 enum,通常會冒出三個顧慮:
"pending" 變成數字 2,這就把既有契約給破壞了。而且數字很難讀,前端得另外拿對照表才知道 2 是什麼;要是資料庫也跟著存數字,撈出來一排 2、4,根本不知道自己在看什麼。這篇就來把 .NET 9 新增的 [JsonStringEnumMemberName] 講清楚,一個內建 attribute 就能把這三個顧慮一次解掉。順便整理一下最容易踩雷的「兩條序列化管線」跟 query 參數的陷阱。
.NET 的內建 DI 容器有一個長年的限制:一個介面對應多個實作時,沒辦法「指定要哪一個」。
services.AddScoped<IPaymentProvider, EcPayProvider>();
services.AddScoped<IPaymentProvider, NewebPayProvider>();
// 建構子注入 IPaymentProvider 時,只會拿到「最後註冊的那個」(NewebPayProvider)
在 .NET 8 之前,遇到這種需求只能繞路:注入 IEnumerable<T> 自己篩選、或自己寫工廠類別。
.NET 8 的 Microsoft.Extensions.DependencyInjection 終於內建了具名注入(Keyed Services):
註冊時給每個實作一個 key,取用時憑 key 拿到指定的實作。
這篇整理它的基本用法、相較舊做法的優點,以及兩個我覺得最常用到的場景:
各家金流提供者、同介面的不同儲存策略。
用 Razor 模板(.cshtml)在後端產生 HTML(例如啟用信、密碼重設信、驗證碼信)時,
常見的兩個引擎是 RazorLight 與 Razor.Templating.Core。
模板檔案本身可以完全一樣,連 @model、@if、@Model.Xxx 都不用改,
但專案設定和驅動程式的實作差很多——而且差的點,剛好都源自同一件事:
RazorLight 是執行期(runtime)才編譯模板,Razor.Templating.Core 是建置期(build time)就預編譯好。
這一個差異,連帶解釋了三個「為什麼」:
這篇就把這三件事講清楚,並附上兩邊的程式碼對照。
很多前後分離專案的認證一路都是 JWT:後台管理員登入後拿到 Token,之後每個請求都帶 Authorization: Bearer ...。
用久了容易有個錯覺,好像 ASP.NET Core 的認證就只有 JWT 這條路。
其實不是。ASP.NET Core 的認證系統是可插拔的 Scheme 架構,JWT 只是其中一種掛上去的 Scheme。
這次剛好有個需求可以拿來示範:ERP 廠商要打我們的 API,但他們不是「會登入的使用者」,給不了帳密,也不適合走 JWT 換 Token 那一套。
這種機器對機器、長期有效的靜態金鑰,用 API Key 才對。
這篇想做兩件事:
前後端分離寫久了,總會遇到那個熟悉的紅字:
Access to fetch at 'https://api.example.com' from origin 'https://www.example.com'
has been blocked by CORS policy.
前端工程師看到這行,第一反應通常是轉頭問後端:「你 CORS 開了沒?」
後端工程師則是一臉無辜:「我本機明明可以啊。」
問題就在這裡:CORS 是瀏覽器的安全機制,本機用 Postman、Swagger 打都不會擋,
只有真的從另一個網域用瀏覽器發請求才會觸發。所以「我本機可以」這句話在 CORS 面前完全沒意義。
這篇就把 ASP.NET Core 的 CORS 設定一次講清楚,並把它封裝成一個擴充方法,
搭配設定檔的允許清單,讓 Program.cs 只剩乾淨的一行。
JWT 大家都在用,但「怎麼產生」這件事其實有不只一種寫法。
早期我手刻過一版——自己組 header、payload,自己做 Base64Url 編碼,自己用 HMACSHA256 算簽名。
能動是能動,但每次要加個 claim、換個演算法都得回去改一堆字串拼接,心裡總是毛毛的。
後來改用套件,又面臨一個選擇題:
要用微軟官方的 Microsoft.IdentityModel,還是社群很受歡迎的 jwt-dotnet?
這篇就把這兩套放在一起比。重點是它們在我的專案裡實作的是同一份介面 IJsonWebTokenService,
所以可以靠 DI 一行切換引擎,呼叫端完全無感。
用 [ApiController] 寫 Web API 時,模型驗證幾乎是免費的。在 ViewModel 上掛幾個 [Required]、[StringLength],
請求進到 Action 之前框架就會自動幫你擋下不合法的輸入,連 if (!ModelState.IsValid) 都不用寫。
public class LoginViewModel
{
/// <summary> 帳號 </summary>
[JsonPropertyName("id")]
[Required(ErrorMessage = "帳號為必填")]
public required string Id { get; set; }
/// <summary> 密碼 </summary>
[JsonPropertyName("password")]
[Required(ErrorMessage = "密碼為必填")]
public required string Password { get; set; }
}
少送一個 id,框架自動回你一包 400:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Id": [
"帳號為必填"
]
},
"traceId": "00-200bae18c687dfe47d1ddac560e30c51-965a2222697de138-00"
}
這格式很標準(它是 RFC 7807 / 9457 的 ProblemDetails),但實務上常常和團隊既有的 API 回應規格對不上。
前端早就講好所有回應都長 { code, message, data },結果驗證錯誤偏偏自己長一套,前端得為它寫特例處理。
這篇就把「怎麼把模型驗證錯誤改成自己想要的格式」講清楚,提供兩種主流做法,並分析各自的優缺點與適用情境。
設計資料表時,有個問題其實比想像中關鍵:這張表的主鍵(PK),到底是誰負責生出來的?
最直覺的答案是「資料庫啊」,int IDENTITY 自動加一,或 SQL Server 的 NEWSEQUENTIALID(),
反正 INSERT 下去資料庫就會幫我填好。寫起來省事,大家一開始也都這樣用。
但用久了會踩到一些尷尬的狀況:
SaveChanges() 之後才拿得到。於是另一條路浮上來:讓應用程式自己產生主鍵。我們團隊的開發規範也是這樣訂的,
所有實體的主鍵一律標上 [DatabaseGenerated(DatabaseGeneratedOption.None)],
把產生主鍵的責任從資料庫手上收回來。
這篇就把「資料庫產生 PK」與「應用程式自訂 PK」兩條路講清楚,
並用 EF Core 內建的 SequentialGuidValueGenerator 示範兩種落地寫法。