.NET Core版本演進到 1.1,2.0 也已進入 Preview 階段,輕巧、高效能與跨平台是 .NET Core 最大的優勢,預估未來將成主流,雖然現階段用在工作上的機率不大,找到機會還是該提早練習體驗,以免時間到了來不及上車。最近用 .NET Core 1.1 寫了一支小程式,順手分享實作心得。
先說小工具程式的用途,需求很簡單:
已有兩個很小的 Web API 服務(就當是 Microservice 概念吧)。其中一個姑且稱之 WebStatus Service,負責監控多台 Web 主機,以 JSON 格式回報主機狀態如下:
[
{
"Name": "Web1",
"Status": "PASS",
"UpdateTime": "10:00:00",
"Message": ""
},
{
"Name": "Web2",
"Status": "FAIL",
"UpdateTime": "10:00:02",
"Message": "No Response"
}
]
另外一個 LineNotify Service 則可透過 LINE Notify 發送通知給相關人員。(就是上回介紹過用 LINE Notify / LINE Login 技術開發的實驗專案)
小工具的任務很單純,依固定間隔執行,從 WebStatus Service 取得主機狀態,若發現有主機異常,就將主機名稱、時間與錯誤訊息以 LINE Notify 通知維運人員。這種長期執行排程,丟到低耗電低成本 Linux 小電腦跑是個好主意,改用 .NET Core 開發就可以具備此一優勢,加上程式很小,拿來體驗新技術試水溫正好,就像盲目約會約看電影最好是一樣道理:苗頭不對默默看完快閃,感覺良好再約晚餐… (謎之聲:啊不就很會?)
先聲明,我對 .NET Core 還沒做過深入研究,主要憑藉過去使用 .NET Framework 的開發經驗,依直覺行事,遇到問題就爬文求解,就是傳說中的 GOD(Google Driven Development)/ SOD(Stackoverflow Driven Development),不是良好示範,但猜想是實務上蠻多人踏進新領域應該都是採取此種入水姿勢,就當成一次探險好了,測試「有經驗的 .NET 開發者是否不需訓練就能上手 .NET Core 1.1?」 :P
我使用 Visual Studio 2017 開發,新増專案時選擇 Visual C# / .NET Core / Console App (.NET Core)
乍看程式寫起來跟 .NET Console App 差不多,但深入到細節就會開始體驗到 .NET Core 與 .NET Framework 的細微差異。
WebStatus 與 LINE Notify WebAPI URL 當然不該寫死在程式裡,放進 config 才是王道。我遇到的第一個問題是 .NET 不再使用 app.config 與 appSettings,改採開放政策,允許開發者依需求透過不同 Provider 使用 INI、JSON、XML、環境變數,甚至 Azure Key Valut 取得設定。對我而言,JSON 是最親切的選項,做法是先安裝 Microsoft.Extensions.Configuration 與 Microsoft.Extensions.Configuration.Json,以 new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build() 讀入 JSON 檔並建立 IConfiguration 物件,再以 Configuration["WebStatusUrl"] 讀取設定。官方文件有蠻淺顯但詳細的介紹。
接下來馬上遇到第二個問題:我呼叫 Web API 最常用的 WebClient 不支援 .NET Core!爬文找到替代品 HttpClient,與 WebClient 相比有不少優點:支援DNS解析快取、Cookie/身分驗證設定,可同時發送多個Request、基於 HttpWebRequest/HttpWebResponse(易於測試)、IO相關方法均採非同步,缺點是不支援FTP,對本專案無影響。延伸閱讀
我寫了一個函式模擬 WebClient.DownloadString(),傳入 URL,取得傳回結果:
static async Task<string> HttpGet(string url)
{
//WebClient 無法跨平台,改用HttpClient http://stackoverflow.com/a/30286173/288936
var hc = new HttpClient();
var resp = await hc.GetAsync(url);
resp.EnsureSuccessStatusCode(); //未傳回HTTP 200即拋出例外
return await Task.Run(() => resp.Content.ReadAsStringAsync());
}
要傳送 Url 參數時發現 .NET Core 沒有 HttpUtility 可用,要換一下元件,改用 System.Net.WebUtility.UrlEncode() 即可。
除此之外,其餘部分倒是挺順利。取得 JSON 後利用 Json.NET 轉回物件陣列(Yes,Json.NET 有 .NET Core 版本!),再用 LINQ .Where(o => o.Status == "FAIL") 挑出異常項目,為了避免故障期間狂發通知轟炸,我用了點技巧,將取得結果以 System.IO.File.WriteAllText() 寫檔保存,每次發出通知前與上次狀況比對,若上回就已異常就不發通知,因此只有在正常變異常,或異常變正常時才會接到訊息。這段邏輯用 LINQ 寫輕鬆愉快又簡潔,我愛死 LINQ 跟 C# 了!
//取得上次執行結果
string lastJsonFile = Path.Combine(Directory.GetCurrentDirectory(), "LastStatus.json");
if (!File.Exists(lastJsonFile)) File.WriteAllText(lastJsonFile, "[]");
conststring failStatus = "FAIL";
//讀取上次結果,篩選異常項目轉成主機名稱字串陣列
var lastFailedNames =
JsonConvert.DeserializeObject<List<Result>>(File.ReadAllText(lastJsonFile))
.Where(o => o.Status == failStatus).Select(o => o.Name).ToArray();
//從本次結果挑出異常項目集合
var failedResults = JsonConvert.DeserializeObject<List<Result>>(json)
.Where(o => o.Status == failStatus);
//用.Where()挑出上次正常本次異常的項目
var newFailed = failedResults.Where(o => !lastFailedNames.Contains(o.Name));
if (newFailed.Any())
{
string msg =
$"【服務異常通報】\n{string.Join("\n",
newFailed.Select(o => $"{o.Name}/{o.UpdateTime}/{o.Message}").ToArray())}";
//UrlEncode 在 System.Net.WebUtility
var notifyRes = HttpGet(lineNotifierUrl + System.Net.WebUtility.UrlEncode(msg)).Result;
}
//利用.Except()濾掉上次與這次都異常的項目,留下來的就是本次已恢復正常的項目
var recovered = lastFailedNames.Except(newFailed.Select(o => o.Name));
if (recovered.Any())
{
string msg = $"【服務恢復通報】\n{string.Join("、", recovered.ToArray())}";
//UrlEncode 在 System.Net.WebUtility
var notifyRes = HttpGet(lineNotifierUrl + System.Net.WebUtility.UrlEncode(msg)).Result;
}
File.WriteAllText(lastJsonFile, json);
就這樣程式寫完了,專案結構如下,在 Visual Studio 可直接執行也能逐行偵錯,開發體驗跟一般的 .NET 專案沒啥兩樣。
我打算直接將編譯結果部署到 Linux 直接執行,此時可使用 Publish 功能:
Publish 設定用預設值即可:
Publish 會將用到的 NuGet 程式庫 dll 以及 Console App 的 dll/pdb (.NET Core Console App 的編譯結果為 dll,不是 exe)集中到 PublishOutput 目錄下:
將這些檔案部署到 Ubuntu 主機,執行 dotnet WebStatusAlarm.dll 即可執行,再配合使用 crontab 設定排程,我的第一支實用級 .NET Core 程式就在 Ubuntu 主機正式商業運轉囉~
綜合來說,本次改用 .NET Core 1.1 寫小工具程式的經驗挺順利的,遇到的問題主要集中在慣用的 .NET Framework 程式庫元件在 .NET Core 不支援,但爬文都蠻快找到答案(Stackoverflow 是大寶庫),熱門的第三方程式庫如 Json.NET、NLog 都已經有 .NET Core 版本,而部署到 Linux 與執行的方法也算簡便。雖然試寫小工具程式的體驗與移轉大型專案不能相提並論,但 .NET 1.1 的成熟度與方便性比我原本預期為高,下回找機會再來玩 ASP.NET Core。
補充兩個 .NET Core 開發資源:
- 如何在 Linux 平台安裝與執行 .NET Core 的官方教學
https://www.microsoft.com/net/core#linuxredhat - 官方版的 .NET Core 入門教學
https://docs.microsoft.com/zh-tw/dotnet/articles/core/