最近的 Side-Project 計劃用 Minimal API 寫個簡單的 LINE Notify 發送服務,靠單一 EXE 在本機跑 localhost 網站搞定所有事,以貫徹極簡主義。

使用 LINE Notify API 需註冊取得 client_id 及 client_secret,與使用者建立連動時要記錄 access_token,我打算用網頁輸入 client_id 及 client_secret 儲存,取代手動編輯 appsetting.json,至於使用者 access_token,因筆數有限,不值得為此扯上資料庫,用 CSV 或 JSON 儲存是較簡便有效率的解法。最後,我決定借用 .NET Core 的 IConfiguration 機制加上寫入功能來儲存設定,主要有以下考量:

  1. 除了 JSON 檔案,IConfiguration 還有 XML/INI、環境變數、命令列參數... 等其他 IConfigurationProvider,具有很大的擴充彈性。
  2. JSON 設定檔支援多設定檔內容合併(例如:appsettings.json 與 appsettings.Development.json)、指定設定檔為選擇性或必備、檔案異動時自動更新... 等好用功能。
  3. 內建 GetValue<T>() 方法,能以強型別方式存取設定值。

以下是分享我的做法。

ASP.NET Core 專案已參照 Microsoft.Extensions.Configuration.* 相關程式庫可直接使用 IConfiguration,若是 Console 專案,需參照 Microsoft.Extensions.Configuration.Json 及 Microsoft.Extensions.Configuration.Binder。

我定義了一個 LineNotifySettings 型別,包含 ClientId、Secret 及 Dictionary<string, string> Tokens 屬性以保存 client_id、client_secret 及使用者對映的 access_token。設定檔的讀取及更新則交由自訂 Config 類別實作。Config 建構式會先檢查 line-notify-settings.json 是否存在,若沒有就建一個空範本,之後透過 ConfigurationBuilder.AddJsonFile().Build() 建構 出讀取 line-notify-settings.json 的 IConfiguration 介面,指定 reloadOnChange: true 確保 .json 變更時會自動更新。Config 有個屬性 Settings => config.Get<LineNotifySettings>(),每次重新從 IConfiguration 讀取最新的 LineNotifySettings 內容。Config 也提供 SetAuthIdentity()、SaveAccessToken() 等修改設定值的方法,但因 IConfiguration 不包含寫入 API ,故 Save() 要自行將 LineNotifySettings JSON 序列化覆寫 line-notify-settings.json。

Config.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

namespace LineNotifyMinApi
{
    public class Config
    {
        LineNotifySettings Settings => config.Get<LineNotifySettings>();
        string settingFilePath;
        const string settingFileName = "line-notify-settings.json";
        IConfiguration config;
        public Config(string basePath = null!)
        {
            basePath = basePath ?? AppContext.BaseDirectory;
            settingFilePath = Path.Combine(basePath, settingFileName);
            if (!File.Exists(settingFilePath)) Save(new LineNotifySettings());
            config = new ConfigurationBuilder()
                .SetBasePath(basePath)
                .AddJsonFile(settingFileName, optional: false, reloadOnChange: true)
                .Build();
        }

        public string ClientId => Settings.ClientId;
        public string Secret => Settings.Secret;
        public bool IsSet => !string.IsNullOrEmpty(ClientId) && !string.IsNullOrEmpty(Secret);
        public void SetAuthIdentity(string clientId, string secret)
        {
            lock (config)
            {
                var settings = Settings;
                settings.ClientId = clientId;
                settings.Secret = secret;
                Save(settings);
            }
        }

        public string[] ListUsers() => Settings.Tokens.Keys.ToArray();

        public string GetAccessToken(string userName) =>
            Settings.Tokens.ContainsKey(userName) ?
            Settings.Tokens[userName] : null!;

        public void SaveAccessToken(string userName, string token)
        {
            lock (config)
            {
                var settings = Settings;
                if (settings.Tokens.ContainsKey(userName))
                    throw new ApplicationException($"Token [{userName}] already exists");
                settings.Tokens.Add(userName, token);
                Save(settings);
            }
        }
        public void Save(LineNotifySettings settings)
        {
            File.WriteAllText(settingFilePath,
                JsonSerializer.Serialize(settings, new JsonSerializerOptions
                {
                    WriteIndented = true
                }));
        }
    }
    public class LineNotifySettings
    {
        public string ClientId { get; set; } = null!;
        public string Secret { get; set; } = null!;
        public Dictionary<string, string> Tokens { get; set; } = new Dictionary<string, string>();
    }
}

在 Program.cs 隨便寫幾行程式展示它的功能。

using LineNotifyMinApi;

var config = new Config();

int userNo = 1;
while (true) 
{
    Console.Clear();
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine("Tokens: " + string.Join(", ", config.ListUsers()));
    Console.ForegroundColor = ConsoleColor.White;
    Console.Write(@"1.Set ClientId
2.Add AccessToken
3.Refresh
Option: ");
    string opt = Console.ReadLine() ?? "?";
    switch (opt) {
        case "1":
            Console.Write("Client Id: ");
            var clientId = Console.ReadLine();
            config.SetAuthIdentity(clientId!, "NA");
            break;
        case "2":
            config.SaveAccessToken($"User{userNo++}", "");
            Task.Delay(500).Wait(); // Waiting for reloadOnChange
            break;
        case "3":
            break;
        default:
            Console.WriteLine($"Unknown command - {opt}") ;
            break;
    }
}

如以下動畫,SetAuthIdentity() 與 SaveAccessToken() 後會看到 line-notify-settings.json 隨之改變,若我們手動修改 line-notify-settings.json,之後 config.Get<LineNotifySettings>() 也會讀到修改後的內容。這個特性在 Windows Service 模式下挺好用,不必重啟服務就完成設定值修改,方便又省事。

如此,我們成功借用 IConfiguration 介面建立簡單的 JSON 設定值儲存來源,提供以強型別方式讀取及修改設定。初步試用感覺不錯,提供大家參考。

Example of how to use IConfiguration to create a read/write store for applcation's settings.


Comments

Be the first to post a comment

Post a comment


53 + 23 =