• 开发一个现代化的.NetCore控制台程序,包含依赖注入/配置/日志等要素


    前言#

    最近需要开发小工具的场景有点多,上次我用 go 语言开发了一个 hive 导出工具,体验还不错,只是 go 语言的语法实在是喜欢不起来,这次继续试试用 C# 来开发小工具。

    这次小工具的功能很简单,数据库数据迁移,不过这不重要,主要是记录一下更适合 .Net Core 宝宝体质的控制台小工具开发过程😃

    本文中,我为「现代化的控制台应用的开发体验」做了个定义:能像 Web 应用那样很优雅地整合各种组件,恰好 .NetCore 提供的工具可以实现。我使用了 Microsoft.Extensions.* 系列的组件,包括依赖注入、配置、日志,再补充一下环境变量读取、调试等功能的第三方组件。

    本文的小工具非常简单,面向非专业用户,不需要会命令行知识,所以所有功能采用配置文件的方式来控制,如果要开发传统的 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>
    

    虽然是个控制台小工具,但为了更丝滑的开发体验,我搭建了一个简单的项目骨架。

    配置#

    我一开始想要使用的是 dotenv

    在写 python 和 go 的时候大量使用 dotenv ,感觉很方便

    dotenv#

    C# 里使用也很简单,安装 dotenv.net 这个库

    执行 DotEnv.Load(); 就可以把 .env 文件里的配置读取到环境变量里面

    之后就是直接从环境变量中加载就行,比如 Environment.GetEnvironmentVariable() 方法

    Microsoft.Extensions.Configuration#

    用过 AspNetCore 的同学对这个组件应该不陌生

    本来我是打算使用 dotenv 来做配置,不过最后还是使用 json 文件搭配这个配置组件,原因无他,就是这个组件方便好用。

    安装了相关的依赖之后,执行以下代码初始化

    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"]
      }
    }
    

    定义强类型配置实体#

    为了更好的开发体验,我们使用强类型配置

    新建 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 库实现了配置绑定,搭配使用 IOptionsMonitor 或者 IOptionsSnapshot 进行配置注入的时候,可以实现配置热更新。

    services.AddOptions().Configure(e => config.GetSection("DmTableMigration").Bind(e));
    

    在上面的初始化配置时 configBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); ,可以把 reloadOnChange 设置为 true ,即可实现配置文件修改时自动加载。

    如果不需要热更新的话,可以简化注册方式

    services.AddOptions("DmTableMigration");
    

    这样就是程序启动的时候读取配置,后面配置修改也不会生效,注入的时候只能使用 IOptions

    注入配置#

    注入的时候这样写

    private readonly AppSettings _settings = options.Value;
    
    ctor(IOptions options) {
      _settings = options.Value;
    }
    

    ctor 代表构造方法

    日志#

    日志是程序必不可少的一部分

    我使用了 Microsoft.Extensions.Logging 日志框架,这个框架官方的 Provider 没有可以写入文件的,所以我又搭配 Serilog 来记录日志到文件。其实也可以自己实现一个写入文件的 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 实现依赖注入

    AutoFac 也是一种选择,据说功能更多,我还没用过,接下来找时间体验一下。

    注册服务

    var services = new ServiceCollection();
    services.AddLogging(builder => {
        builder.AddConfiguration(config.GetSection("Logging"));
        builder.AddConsole();
        builder.AddSerilog(dispose: true);
    });
    services.AddSingleton(fsql);
    services.AddOptions().Configure(e => config.GetSection("DmTableMigration").Bind(e));
    services.AddScoped();
    

    使用服务

    在 IoC 容器里注册的服务可以拿出来使用,参考以下代码。

    await using (var sp = services.BuildServiceProvider()) {
        var migrationService = sp.GetRequiredService();
        migrationService.Run();
    }
    

    服务有不同的生命周期,比如 scope 类型的服务,可以使用以下代码创建一个 scope ,在里面进行注入。

    await using (var sp = services.BuildServiceProvider()) {
        using (var scope = sp.CreateScope()) {
            var spScope = scope.ServiceProvider;
            var service = spScope.GetRequiredService();
        }
    }
    

    其他关于依赖注入的使用方法可以参考官方文档。

    调试小工具#

    这里还要推荐 Dumpify 这个调试小工具

    使用非常方便,安装 nuget 包之后,在任何对象后面加个 .Dump() 就可以输出它的结构了。

    这个小工具我目前用着觉得很不错~

    编译 & 发布#

    对于这种简单的小工具我习惯把发布配置写在项目配置里

    对于这个小工具,我的发布方案是:包含运行时的 SingleFile + partial Trimmed

    实测打包出来是 22MB 左右,再使用 zip 压缩,最终大小是 9MB ,尺寸控制还算不错了。

    编辑 .csproj 文件,配置如下

    <PropertyGroup>
        <OutputType>ExeOutputType>
        <TargetFramework>net8.0TargetFramework>
        <ImplicitUsings>enableImplicitUsings>
        <Nullable>enableNullable>
        <PublishSingleFile>truePublishSingleFile>
        <PublishTrimmed>truePublishTrimmed>
        <TrimMode>partialTrimMode>
        <PublishRelease>truePublishRelease>
    PropertyGroup>
    

    在 Trim 的时候我也遇到了一点小问题,默认的 TrimMode 是 full ,最大程度缩减发布的程序尺寸,这个时候编译出来大概是 17MB 的样子,不过 JSON 序列化的时候遇到了问题,所以我切换到了 partial 模式,之后程序运行良好。

    关于 AOT#

    至于最近很火的 .Net8 AOT 方案,我也有试过,但并不理想,首先这个小工具是基于依赖注入框架构建的,AOT天生就对依赖注入这种基于反射的技术不太友好,所以在试用 AOT 的时候我就发现了第一步的配置加载就不太行了。

    接着解决了配置加载的问题之后,我又遇到了 JSON 序列化问题,这个也是基于反射实现的,也不好搞。

    我不太想在小工具的开发上花太多时间,所以没有深入研究,不过接下来 AOT 似乎是一个小的热门趋势,也许我会找时间探索一下。

    对了,如果要发布 AOT 的话,只需要做以下配置

    <PropertyGroup>
        <OutputType>ExeOutputType>
        <TargetFramework>net8.0TargetFramework>
        <ImplicitUsings>enableImplicitUsings>
        <Nullable>enableNullable>
        <PublishAot>truePublishAot>
    PropertyGroup>
    

    杂项#

    获取达梦数据库一个 Schema 下的所有表#

    all_objects 这个视图(表?)里获取。

    PS:达梦这种国产数据库,坑挺多的。当然 Oracle 也一样

    logger.LogInformation("获取Table列表");
    
    var list = fsql.Ado.Querystring, 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#

    应该是这个名字吧?Primary Constructor

    当 class 只有一个带参数的构造方法时,可以使用以简化代码。

    原代码

    public class MigrationService {
        AppSettings _settings;
        IFreeSql _fsql;
        ILogger _logger;
        
        MigrationService(IFreeSql fsql, IOptions options, ILogger logger) {
            _settings = options.Value;
            _fsql = fsql;
            _logger = logger;
        }
    }
    

    新语法

    public class MigrationService(IFreeSql fsql, IOptions options, ILogger logger) {
        private readonly AppSettings _settings = options.Value;
    }
    

    小结#

    时间和篇幅关系,本文只能简略介绍「现代化控制台应用」的开发思路,在接下来的探索过程中可能随时会有补充,我会继续在博客里的本文进行补充,如果你是在除了博客园或者StarBlog之外的其他平台看到本文,可以「查看原文」看看本文的最新版。

  • 相关阅读:
    【C++从入门到踹门】第十五篇:set 和 map
    mybatisplus查询特定字段空指针异常
    线性代数学习笔记1:何为线性代数
    Mysql时间操作
    Jmeter如何进行接口测试-实操
    robocode 相关的总结
    驱动——串口工具点灯实验
    3、Shell变量
    升讯威在线客服系统客服端英文界面的技术实现方法,客户落地巴西圣保罗
    Mac系统 sh: allure: command not found
  • 原文地址:https://www.cnblogs.com/deali/p/17816445.html