.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 的開發經驗,依直覺行事,遇到問題就爬文求解,就是傳說中的 GDD(Google Driven Development)/ SOD(Stackoverflow 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 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, "[]");
 
            const string 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(failedResults.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 開發資源:


Comments

Be the first to post a comment

Post a comment