NetCore 控制台服务处理依赖注入

NetCore 控制台服务处理依赖注入

zeee 1,529 2020-12-20

众所周知, 在 Asp.Net Core 中, 通过 UserStartup() 方法来注册 Startup 类, 在 Startup 类中可通过 ConfigureServices 方法来注册服务依赖, 通过 Configure 方法来配置请求管道, 比如注册中间件等. 这样一来:

  1. Program 类变得很干净, 只需要注册 WebHost 示例并进行运行即可
  2. 注册依赖, 还是配置管道, 统一在Startup文件配置, 不会影响业务代码. 这样如果相似的服务很多, 也比较容易剥离和抽象

ConsoleApplication 中的依赖注入

然而, 在控制台程序中, 默认的注册方式是这样的:

public static async Task Main(string[] args)
{
    var builder = new HostBuilder()
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            // 配置文件设置
        })
        .ConfigureServices((hostingContext, services) =>
        {
            // 依赖注入设置
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddConfiguration(hostingContext.Configuration);
            logging.AddConsole();
        });

    await builder.RunConsoleAsync();
}

所有的逻辑都写在Program中, 而且如果没有额外处理的话, 甚至全部写在一个匿名方法中. 这显得很不美观.

另外, 尤其当习惯了 Asp.Net Core 的这种依赖注入方式之后, 总觉得有些别扭, 总想着搞个类似的 Startup 类来处理依赖注入的代码, 甚至是程序入口的代码 —— 尤其是作为后台进程运行的服务的时候.

目前, .NetCore 应该没有官方的扩展方法来实现. 不过既然是扩展方法, 事实上也不是太难, 将 ConfigureServices 稍微拆一下, 得到这样一个简易版:

// using Microsoft.Extensions.Hosting
static async Task Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    await host.RunAsync();
}

private static IHostBuilder CreateHostBuilder(string[] args)
{
    // 这里做了许多默认的配置, 具体可以看注释
    var host = Host.CreateDefaultBuilder(args)
        .ConfigureServices((ctx, services)
			=> (new Startup(ctx.Configuration)).ConfigureServices(services))
        .UseConsoleLifetime();

    return host;
}

这里仅仅是将 IServerCollection services 传了过去, 而如果有需要, 第一个参数也是可以传过去使用的. 而这种办法, Startup 类大概如下:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
    }

    public void ConfigureServices(IServiceCollection services)
    {
    }
}

再进一步, 可以将这一步, 按照类似 Asp.Net Core 的样子抽象出来. 这样的话, 就可以做成类库来使用. 整个使用体验跟做 ASP 的体验就基本无二了.

首先, 新建一个扩展方法类, 实现 UseStartup 方法:

public static class HostBuilderExtensions
{
    // 约定ConfigureServices方法名
    private const string ConfigureServicesMethodName = "ConfigureServices";

    public static IHostBuilder UseStartup<TStartup>(
        this IHostBuilder hostBuilder) where TStartup : class
    {
        hostBuilder.ConfigureServices((ctx, serviceCollection) =>
        {
            // 找到以 IServiceCollection 为参数的 ConfigureServices 方法
            var cfgServicesMethod = typeof(TStartup).GetMethod(
                ConfigureServicesMethodName, new Type[] { typeof(IServiceCollection) });

            // 找到构造方法
            var hasConfigCtor = typeof(TStartup).GetConstructor(
                new Type[] { typeof(IConfiguration) }) != null;

            //创建Startup的实例
            var startUpObj = hasConfigCtor ?
                (TStartup)Activator.CreateInstance(typeof(TStartup), ctx.Configuration) :
                (TStartup)Activator.CreateInstance(typeof(TStartup), null);

            // 调用 ConfigureServices 方法
            cfgServicesMethod?.Invoke(startUpObj, new object[] { serviceCollection });
        });

        return hostBuilder;
    }
}

然后, 在 Program 中的CreateHostBuilder方法代码就变成了这样:

public static IHostBuilder CreateHostBuilder(string[] args) =>
		Host.CreateDefaultBuilder(args).UseStartup<Startup>();

以上各种实现, 都可以在 Startup 中进行服务注册:

public void ConfigureServices(IServiceCollection services)
{
    // 注册options, memorycache 等内置服务
    services.AddOptions();
    services.AddMemoryCache();
    
    // 注册自定义生命周期服务
    services.AddScope<IMyService, MyService>();
    
    // 注册 HostService 
    services.AddHostedService<MyBackgroundService>();
}

关于 HostServcie , 即实现了 IHostService 或继承 BackgroundService 的服务, 这些服务会在程序启动后执行其 Start/Execute 方法, 这是实现后台线程/长线程的一种方法.


参考资料: https://github.com/sonicmouse/Host.CreateDefaultBuilder.Example


# csharp # dotnet