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.