Develop a modern .NetCore console program, including dependency injection/configuration/logging and other elements

1Foreword

There are a lot of scenarios where gadgets need to be developed recently. Last time I developed a hive export tool using the go language. The experience was pretty good, but I really don’t like the syntax of the go language. This time I will try to use C# to develop gadgets.

The function of the gadget this time is very simple, database data migration, but this is not important. The main purpose is to record the development process of the console gadget that is more suitable for .Net Core baby’s physique

In this article, I define a “modern console application development experience”: the ability to integrate various components as elegantly as a web application, which happens to be possible with the tools provided by .NetCore. I used the Microsoft.Extensions.* series of components, including dependency injection, configuration, logging, and added third-party components with functions such as environment variable reading and debugging.

The gadget in this article is very simple and is intended for non-professional users. It does not require command line knowledge, so all functions are controlled through configuration files. If you want to develop traditional CLI tools, you can use the System.CommandLine library.

2 dependencies

The dependencies used in this project are as follows

<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>

Although it is a console gadget, for a smoother development experience, I built a simple project skeleton.

3Configuration

What I initially wanted to use was dotenv

I use dotenv extensively when writing python and go, which feels very convenient.

dotenv

It is also very simple to use in C#, just install the library dotenv.net

Execute DotEnv.Load(); to read the configuration in the .env file into the environment variable

Then just load it directly from the environment variable, such as the Environment.GetEnvironmentVariable() method

Microsoft.Extensions.Configuration

Students who have used AspNetCore should be familiar with this component.

Originally, I planned to use dotenv for configuration, but in the end I still used json files with this configuration component. The reason is that this component is convenient and easy to use.

After installing the relevant dependencies, execute the following code to initialize

var configBuilder = new ConfigurationBuilder();
configBuilder.AddEnvironmentVariables();
configBuilder.SetBasePath(Environment.CurrentDirectory);
configBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);
var config = configBuilder.Build();

This will get the IConfigurationRoot object

Writing configuration files

The familiar appsettings.json, for those who write AspNetCore: DNA, moved!

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

Define strongly typed configuration entities

For a better development experience, we use strong type configuration

Create new 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();
}

Register Options

The Microsoft.Extensions.Configuration.Binder library is used here to implement configuration binding, and is used together with IOptionsMonitor or IOptionsSnapshot. When configuration is injected, configuration hot update can be achieved.

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

In the above initial configuration configBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);, you can set reloadOnChange to true, you can automatically load the configuration file when it is modified.

If hot update is not required, the registration method can be simplified

services.AddOptions<AppSettings>("DmTableMigration");

In this way, the configuration is read when the program starts, and subsequent configuration modifications will not take effect. You can only use IOptions when injecting.

Inject configuration

When injecting, write like this

private readonly AppSettings _settings = options.Value;

ctor(IOptions<AppSettings> options) {
  _settings = options.Value;
}

ctor represents the construction method

4Log

Logs are an essential part of the program

I used the Microsoft.Extensions.Logging logging framework. The official Provider of this framework does not have the ability to write files, so I paired it with Serilog to record logs to files. In fact, you can also implement a Provider for writing files yourself. I will do it when I have time.

PS: The log components recommended by the .NetCore platform include NLog and Serilog. I think Serilog is more convenient. What kind of XML configuration does NLog have to write? It reminds me of the fear of being dominated by XML in spring. I refuse ×

Serilog configuration

Just configure it directly in the program

Log.Logger = new LoggerConfiguration()
  .MinimumLevel.Information()
  .WriteTo.File("logs/migration-logs.log")
  .CreateLogger();

Logging configuration

Output logs to the console and Serilog at the same time

Serilog is configured with log writing files

services.AddLogging(builder => {
  builder.AddConfiguration(config.GetSection("Logging"));
  builder.AddConsole();
  builder.AddSerilog(dispose: true);
});

5Dependency Injection

Use Microsoft.Extensions.DependencyInjection to implement dependency injection

AutoFac is also an option. It is said to have more functions. I haven’t used it yet. I will try it out when I find time.

Registration service

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();

Use the service

Services registered in the IoC container can be used, refer to the following code.

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

Services have different life cycles, such as scope type services. You can use the following code to create a scope and inject it into it.

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

For other methods of using dependency injection, please refer to the official documentation.

6Debugging Gadgets

Here we also recommend the debugging tool Dumpify

It is very convenient to use. After installing the nuget package, add .Dump() after any object to output its structure.

I’m currently using this little tool and I think it’s very good~

7Compile & amp; Publish

For this simple gadget, I am used to writing the release configuration in the project configuration.

For this gadget, my release plan is: SingleFile + partial Trimmed with runtime

The actual measured package size is about 22MB, and then compressed using zip, the final size is 9MB, and the size control is pretty good.

Edit the .csproj file and configure it as follows

<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>

I also encountered a small problem when using Trim. The default TrimMode is full, which minimizes the size of the published program. At this time, the compiled size is about 17MB. However, I encountered problems during JSON serialization, so I switched to partial mode, the program runs fine after that.

About AOT

As for the recently popular .Net8 AOT solution, I have tried it, but it is not ideal. First of all, this small tool is built based on the dependency injection framework. AOT is inherently not friendly to dependency injection, a reflection-based technology, so in When I tried AOT, I found that the configuration loading in the first step was not working well.

After solving the problem of configuration loading, I encountered the problem of JSON serialization, which was also implemented based on reflection and was not easy to deal with.

I didn’t really want to spend too much time on gadget development, so I didn’t delve into it, but AOT seems to be a small hot trend going forward, so maybe I’ll find time to explore it.

By the way, if you want to publish AOT, you only need to do the following configuration

<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
</PropertyGroup>

8Miscellaneous

Get all tables under a Schema of Dameng database

Get it from the view (table?) of all_objects.

PS: Domestic databases like Dameng have many pitfalls. Of course Oracle is the same

logger.LogInformation("Get Table list");

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: {List}", string.Join(",", tableList));

C# new syntax Primary Ctor

That should be the name, right? Primary Constructor

Can be used to simplify code when a class has only one constructor with parameters.

Original code

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;
    }
}

new syntax

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

9 Summary

Due to time and space constraints, this article can only briefly introduce the development ideas of “Modern Console Application”. It may be supplemented at any time during the subsequent exploration process. I will continue to supplement this article in the blog. If you are in addition to the blog If you read this article on other platforms besides StarBlog, you can “View Original” to see the latest version of this article.