1. 起手式:创建一个控制台应用的灵魂骨架
在Visual Studio 2022新建项目时,Console App模板已默认采用.NET 6的顶级语句特性。我们将其改造为更规范的结构:
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var services = new ServiceCollection();
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider();
var app = serviceProvider.GetRequiredService<MyConsoleApp>();
await app.RunAsync();
static void ConfigureServices(IServiceCollection services)
{
// 后续将在此处注入依赖项
}
这是现代控制台应用的典型架构模式,通过依赖注入容器管理对象生命周期。注意异步RunAsync的设计,为后期扩展HTTP请求等异步操作留出空间。
2. 命令行参数解析:让程序学会听懂人话
使用System.CommandLine库构建专业级参数解析:
// CommandParser.cs
using System.CommandLine;
public static class CommandParser
{
public static RootCommand BuildRootCommand()
{
// 定义文件路径参数(必需)
var fileOption = new Option<FileInfo>(
aliases: new[] { "--file", "-f" },
description: "待处理文件路径")
{
IsRequired = true
};
// 定义日志级别选项(带默认值)
var logLevelOption = new Option<LogLevel>(
aliases: new[] { "--log", "-l" },
getDefaultValue: () => LogLevel.Information,
description: "设置日志输出级别");
var rootCommand = new RootCommand("文件处理器");
rootCommand.AddOption(fileOption);
rootCommand.AddOption(logLevelOption);
rootCommand.SetHandler(async (file, logLevel) =>
{
var services = new ServiceCollection();
ConfigureServices(services, logLevel);
var serviceProvider = services.BuildServiceProvider();
var app = serviceProvider.GetRequiredService<MyConsoleApp>();
await app.RunAsync(file);
}, fileOption, logLevelOption);
return rootCommand;
}
}
通过这种设计,参数类型自动转换、必填验证、默认值设置一气呵成。命令行帮助文档也能自动生成:app.exe --help即可展示专业级帮助信息。
3. 依赖注入:模块化开发的万能胶
改造之前的ConfigureServices方法:
// Program.cs
static void ConfigureServices(IServiceCollection services, LogLevel logLevel = LogLevel.Information)
{
services.AddLogging(builder =>
{
builder.AddConsole()
.AddDebug()
.SetMinimumLevel(logLevel);
});
services.AddSingleton<IFileProcessor, PdfFileProcessor>();
services.AddTransient<MyConsoleApp>();
}
这里巧妙地将日志级别配置与命令行参数绑定。注入策略选择值得注意:
Singleton适用于无状态服务(如文件处理器)Transient适合需要隔离的入口类
实际业务类示例:
public class MyConsoleApp
{
private readonly IFileProcessor _processor;
private readonly ILogger<MyConsoleApp> _logger;
// 构造函数注入体现依赖倒置原则
public MyConsoleApp(IFileProcessor processor, ILogger<MyConsoleApp> logger)
{
_processor = processor;
_logger = logger;
}
public async Task RunAsync(FileInfo file)
{
_logger.LogInformation("开始处理文件:{FileName}", file.Name);
try
{
await _processor.ProcessAsync(file);
_logger.LogDebug("文件处理细节记录");
}
catch (FileFormatException ex)
{
_logger.LogError(ex, "文件格式异常: {Message}", ex.Message);
Environment.ExitCode = 2;
}
}
}
4. 日志系统:程序运行的显微镜
日志配置的学问远不止添加控制台输出:
// 扩展日志配置示例
services.AddLogging(builder =>
{
builder.AddConsole(options =>
{
options.FormatterName = "custom"; // 自定义日志格式
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
})
.AddConfiguration(configuration.GetSection("Logging")) // 读取配置文件
.AddEventLog() // Windows事件日志
.AddFilter("System.Net.Http", LogLevel.Warning); // 过滤特定命名空间日志
});
通过过滤器可精准控制日志输出,例如:
- 生产环境过滤
Debug日志 - 限制第三方库的冗杂日志
- 关键模块启用详细日志
5. 应用场景与技术选型权衡
典型使用场景:
- 自动化部署工具(依赖参数解析验证)
- 定时任务处理器(需要健壮的日志系统)
- 数据处理流水线(依赖注入管理组件)
技术方案对比:
| 技术点 | 优势 | 注意事项 |
|---|---|---|
| System.CommandLine | 官方维护、强类型解析、自动生成帮助文档 | 学习曲线较陡,需理解命令树结构 |
| 内置DI容器 | 轻量级、零配置依赖、生命周期管理简单 | 功能较基础,复杂场景需要Autofac等第三方容器 |
| ILogger | 统一抽象、灵活的日志提供器配置、结构化日志支持 | 默认配置简单,高级功能需要深入研究 |
6. 避坑指南:前人踩过的雷区
生命周期管理
注意IDisposable对象的处理,特别是Singleton服务应避免持有需及时释放的资源异步日志陷阱
部分日志提供器(如文件日志)可能存在异步写入延迟,重要操作后应手动刷新:
logger.LogInformation("关键操作完成");
await (logger as IAsyncLogger)?.FlushAsync();
- 参数验证艺术
虽可使用Option<T>的参数校验,复杂校验建议在处理器中实现:
rootCommand.SetHandler(async (file) =>
{
if (file.Length > 100_000_000)
{
Console.WriteLine("文件大小不能超过100MB");
Environment.ExitCode = 3;
return;
}
// ...
}, fileOption);
7. 实战总结:打造工业级控制台应用
通过本方案可实现:
- 参数解析代码减少70%
- 日志配置灵活切换(开发/生产环境)
- 服务组件可测试性提升
- 异常退出码标准化管理
完整示例的运行流程:
dotnet run -- -f input.pdf --log Debug
# 查看帮助
dotnet run -- --help
评论