开发一个现代化的.NetCore控制台程序,包含依赖注入/配置/日志等要素
前言
最近需要開發(fā)小工具的場景有點(diǎn)多,上次我用 go 語言開發(fā)了一個 hive 導(dǎo)出工具,體驗(yàn)還不錯,只是 go 語言的語法實(shí)在是喜歡不起來,這次繼續(xù)試試用 C# 來開發(fā)小工具。
這次小工具的功能很簡單,數(shù)據(jù)庫數(shù)據(jù)遷移,不過這不重要,主要是記錄一下更適合 .Net Core 寶寶體質(zhì)的控制臺小工具開發(fā)過程??
本文中,我為「現(xiàn)代化的控制臺應(yīng)用的開發(fā)體驗(yàn)」做了個定義:能像 Web 應(yīng)用那樣很優(yōu)雅地整合各種組件,恰好 .NetCore 提供的工具可以實(shí)現(xiàn)。我使用了 Microsoft.Extensions.* 系列的組件,包括依賴注入、配置、日志,再補(bǔ)充一下環(huán)境變量讀取、調(diào)試等功能的第三方組件。
本文的小工具非常簡單,面向非專業(yè)用戶,不需要會命令行知識,所以所有功能采用配置文件的方式來控制,如果要開發(fā)傳統(tǒng)的 CLI 工具,可以使用 System.CommandLine 這個庫。
依賴
本項目使用到的依賴如下
<ItemGroup>
<PackageReference Include="dotenv.net" Version="3.1.3" />
<PackageReference Include="Dumpify" Version="0.6.0" />
<PackageReference Include="FreeSql" Version="3.2.802" />
<PackageReference Include="FreeSql.Provider.Dameng" Version="3.2.802" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
</ItemGroup>
雖然是個控制臺小工具,但為了更絲滑的開發(fā)體驗(yàn),我搭建了一個簡單的項目骨架。
配置
我一開始想要使用的是 dotenv
在寫 python 和 go 的時候大量使用 dotenv ,感覺很方便
dotenv
C# 里使用也很簡單,安裝 dotenv.net 這個庫
執(zhí)行 DotEnv.Load(); 就可以把 .env 文件里的配置讀取到環(huán)境變量里面
之后就是直接從環(huán)境變量中加載就行,比如 Environment.GetEnvironmentVariable() 方法
Microsoft.Extensions.Configuration
用過 AspNetCore 的同學(xué)對這個組件應(yīng)該不陌生
本來我是打算使用 dotenv 來做配置,不過最后還是使用 json 文件搭配這個配置組件,原因無他,就是這個組件方便好用。
安裝了相關(guān)的依賴之后,執(zhí)行以下代碼初始化
var configBuilder = new ConfigurationBuilder();
configBuilder.AddEnvironmentVariables();
configBuilder.SetBasePath(Environment.CurrentDirectory);
configBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);
var config = configBuilder.Build();
這樣就得到了 IConfigurationRoot 對象
編寫配置文件
熟悉的 appsettings.json ,對于寫 AspNetCore 的人來說:DNA,動了!
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
},
"ConnectionStrings": {
"Default": "server=host;port=1234;user=user;password=pwd;database=db;poolsize=5"
},
"DmTableMigration": {
"Schema": "schema",
"DbLink": "link_test",
"Fake": true,
"ExcludeTables": ["table1", "table2"]
}
}
定義強(qiáng)類型配置實(shí)體
為了更好的開發(fā)體驗(yàn),我們使用強(qiáng)類型配置
新建 AppSettings.cs
public class AppSettings {
public string Schema { get; set; }
public string DbLink { get; set; }
public bool Fake { get; set; }
public List<string> ExcludeTables { get; set; } = new();
}
注冊 Options
這里使用了 Microsoft.Extensions.Configuration.Binder 庫實(shí)現(xiàn)了配置綁定,搭配使用 IOptionsMonitor<T> 或者 IOptionsSnapshot<T> 進(jìn)行配置注入的時候,可以實(shí)現(xiàn)配置熱更新。
services.AddOptions().Configure<AppSettings>(e => config.GetSection("DmTableMigration").Bind(e));
在上面的初始化配置時 configBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); ,可以把 reloadOnChange 設(shè)置為 true ,即可實(shí)現(xiàn)配置文件修改時自動加載。
如果不需要熱更新的話,可以簡化注冊方式
services.AddOptions<AppSettings>("DmTableMigration");
這樣就是程序啟動的時候讀取配置,后面配置修改也不會生效,注入的時候只能使用 IOptions<T>
注入配置
注入的時候這樣寫
private readonly AppSettings _settings = options.Value;
ctor(IOptions<AppSettings> options) {
_settings = options.Value;
}
ctor 代表構(gòu)造方法
日志
日志是程序必不可少的一部分
我使用了 Microsoft.Extensions.Logging 日志框架,這個框架官方的 Provider 沒有可以寫入文件的,所以我又搭配 Serilog 來記錄日志到文件。其實(shí)也可以自己實(shí)現(xiàn)一個寫入文件的 Provider ,等有時間我來搞一下。
PS:.NetCore 平臺推薦的日志組件有 NLog 和 Serilog,我覺得 Serilog 更方便,NLog 非要寫什么 xml 配置,讓我想起了在 spring 里被 xml 支配的恐懼,拒絕 ×
Serilog配置
直接在程序里配置就行了
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.File("logs/migration-logs.log")
.CreateLogger();
Logging配置
同時輸出日志到控制臺和 Serilog
Serilog 又配置了日志寫入文件
services.AddLogging(builder => {
builder.AddConfiguration(config.GetSection("Logging"));
builder.AddConsole();
builder.AddSerilog(dispose: true);
});
依賴注入
使用 Microsoft.Extensions.DependencyInjection 實(shí)現(xiàn)依賴注入
AutoFac 也是一種選擇,據(jù)說功能更多,我還沒用過,接下來找時間體驗(yàn)一下。
注冊服務(wù)
var services = new ServiceCollection();
services.AddLogging(builder => {
builder.AddConfiguration(config.GetSection("Logging"));
builder.AddConsole();
builder.AddSerilog(dispose: true);
});
services.AddSingleton(fsql);
services.AddOptions().Configure<AppSettings>(e => config.GetSection("DmTableMigration").Bind(e));
services.AddScoped<MigrationService>();
使用服務(wù)
在 IoC 容器里注冊的服務(wù)可以拿出來使用,參考以下代碼。
await using (var sp = services.BuildServiceProvider()) {
var migrationService = sp.GetRequiredService<MigrationService>();
migrationService.Run();
}
服務(wù)有不同的生命周期,比如 scope 類型的服務(wù),可以使用以下代碼創(chuàng)建一個 scope ,在里面進(jìn)行注入。
await using (var sp = services.BuildServiceProvider()) {
using (var scope = sp.CreateScope()) {
var spScope = scope.ServiceProvider;
var service = spScope.GetRequiredService<MigrationService>();
}
}
其他關(guān)于依賴注入的使用方法可以參考官方文檔。
調(diào)試小工具
這里還要推薦 Dumpify 這個調(diào)試小工具
使用非常方便,安裝 nuget 包之后,在任何對象后面加個 .Dump() 就可以輸出它的結(jié)構(gòu)了。
這個小工具我目前用著覺得很不錯~
編譯 & 發(fā)布
對于這種簡單的小工具我習(xí)慣把發(fā)布配置寫在項目配置里
對于這個小工具,我的發(fā)布方案是:包含運(yùn)行時的 SingleFile + partial Trimmed
實(shí)測打包出來是 22MB 左右,再使用 zip 壓縮,最終大小是 9MB ,尺寸控制還算不錯了。
編輯 .csproj 文件,配置如下
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<PublishRelease>true</PublishRelease>
</PropertyGroup>
在 Trim 的時候我也遇到了一點(diǎn)小問題,默認(rèn)的 TrimMode 是 full ,最大程度縮減發(fā)布的程序尺寸,這個時候編譯出來大概是 17MB 的樣子,不過 JSON 序列化的時候遇到了問題,所以我切換到了 partial 模式,之后程序運(yùn)行良好。
關(guān)于 AOT
至于最近很火的 .Net8 AOT 方案,我也有試過,但并不理想,首先這個小工具是基于依賴注入框架構(gòu)建的,AOT天生就對依賴注入這種基于反射的技術(shù)不太友好,所以在試用 AOT 的時候我就發(fā)現(xiàn)了第一步的配置加載就不太行了。
接著解決了配置加載的問題之后,我又遇到了 JSON 序列化問題,這個也是基于反射實(shí)現(xiàn)的,也不好搞。
我不太想在小工具的開發(fā)上花太多時間,所以沒有深入研究,不過接下來 AOT 似乎是一個小的熱門趨勢,也許我會找時間探索一下。
對了,如果要發(fā)布 AOT 的話,只需要做以下配置
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
雜項
獲取達(dá)夢數(shù)據(jù)庫一個 Schema 下的所有表
從 all_objects 這個視圖(表?)里獲取。
PS:達(dá)夢這種國產(chǎn)數(shù)據(jù)庫,坑挺多的。當(dāng)然 Oracle 也一樣
logger.LogInformation("獲取Table列表");
var list = fsql.Ado.Query<Dictionary<string, object>>(
$"SELECT OBJECT_NAME FROM all_objects WHERE owner='{_settings.Schema}' AND object_type='TABLE'");
var tableList = list.Select(e => e["OBJECT_NAME"].ToString() ?? "")
.Where(e => !string.IsNullOrWhiteSpace(e))
.Where(e => !_settings.ExcludeTables.Contains(e))
.ToList();
logger.LogInformation("Table列表:{List}", string.Join(",", tableList));
C# 新語法 Primary Ctor
應(yīng)該是這個名字吧?Primary Constructor
當(dāng) class 只有一個帶參數(shù)的構(gòu)造方法時,可以使用以簡化代碼。
原代碼
public class MigrationService {
AppSettings _settings;
IFreeSql _fsql;
ILogger<MigrationService> _logger;
MigrationService(IFreeSql fsql, IOptions<AppSettings> options, ILogger<MigrationService> logger) {
_settings = options.Value;
_fsql = fsql;
_logger = logger;
}
}
新語法
public class MigrationService(IFreeSql fsql, IOptions<AppSettings> options, ILogger<MigrationService> logger) {
private readonly AppSettings _settings = options.Value;
}
小結(jié)
時間和篇幅關(guān)系,本文只能簡略介紹「現(xiàn)代化控制臺應(yīng)用」的開發(fā)思路,在接下來的探索過程中可能隨時會有補(bǔ)充,我會繼續(xù)在博客里的本文進(jìn)行補(bǔ)充,如果你是在除了博客園或者StarBlog之外的其他平臺看到本文,可以「查看原文」看看本文的最新版。
總結(jié)
以上是生活随笔為你收集整理的开发一个现代化的.NetCore控制台程序,包含依赖注入/配置/日志等要素的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手撕Vuex-提取模块信息
- 下一篇: 向量数据库Chroma极简教程