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. 避坑指南:前人踩过的雷区

  1. 生命周期管理
    注意IDisposable对象的处理,特别是Singleton服务应避免持有需及时释放的资源

  2. 异步日志陷阱
    部分日志提供器(如文件日志)可能存在异步写入延迟,重要操作后应手动刷新:

logger.LogInformation("关键操作完成");
await (logger as IAsyncLogger)?.FlushAsync();
  1. 参数验证艺术
    虽可使用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