之前说的做自动记录 Todo 执行过程中消耗的时间的Todo 项目,由于想持续保持程序执行,就放弃了 Spectre.Console.Cli
,后来随着命令越来越多,自己处理觉得很是麻烦,想了想要不试试怎么将这个东西嵌入程序,然后手动传递参数?
本文完整代码可以从项目中获取。
说干就干,研究了一下,发现核心的 CommandApp
并不需要独占的控制台,我们可以随时 new,参数直接将 ReadLine()
获得的参数传递 args 就可以了。
await _commandApp.RunAsync(cmd.Split(' '));
static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddSingleton<TodoHolder>(); services.AddHostedService<TodoCommandService>(); services.AddCommandApp(); });
最后一个是拓展方法:
internal static IServiceCollection AddCommandApp(this IServiceCollection services) { return services.AddSingleton(w => { var app = new CommandApp(); app.Configure(config => { config.CaseSensitivity(CaseSensitivity.None); config.AddBranch<MethodSettings>("del", del => { del.SetDefaultCommand<DelCommand<TodoItem>>(); del.AddCommand<DelCommand<TodoItem>>("todo"); del.AddCommand<DelCommand<Project>>("pro"); del.AddCommand<DelCommand<Tag>>("tag"); }); } return app; } }
一切显得非常美好,但是棘手的问题就来了。Spectre.Console.Cli
自带依赖注入功能,会自动管理 Command 中的依赖关系,如果我们的 Command 需要依赖外部的类,那么需要在 Spectre.Console.Cli
中注册才能正常工作。但是这个东西也不自带注册器,我们在外部 DI 中注册的 TodoHolder
并没有什么用。
虽然 Spectre.Console.Cli
不提供注册的办法,但是提供了一个构造函数,支持接受一个 ITypeRegistrar
作为参数,直接传递 IServiceCollection
就可以,这样在外部注册的类就传递进去了注册系统。官方提供了这个两个类的实现示例:
using Microsoft.Extensions.DependencyInjection; using Spectre.Console.Cli; namespace TodoTrack.Cli { public sealed class TypeRegistrar : ITypeRegistrar { private readonly IServiceCollection _builder; public TypeRegistrar(IServiceCollection builder) { _builder = builder; } public ITypeResolver Build() { return new TypeResolver(_builder.BuildServiceProvider()); } public void Register(Type service, Type implementation) { _builder.AddSingleton(service, implementation); } public void RegisterInstance(Type service, object implementation) { _builder.AddSingleton(service, implementation); } public void RegisterLazy(Type service, Func<object> func) { if (func is null) { throw new ArgumentNullException(nameof(func)); } _builder.AddSingleton(service, (provider) => func()); } } }
using Spectre.Console.Cli; namespace TodoTrack.Cli { public sealed class TypeResolver : ITypeResolver, IDisposable { private readonly IServiceProvider _provider; public TypeResolver(IServiceProvider provider) { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); } public object? Resolve(Type? type) { if (type == null) { return null; } return _provider.GetService(type); } public void Dispose() { if (_provider is IDisposable disposable) { disposable.Dispose(); } } } }
CommandApp
的初始化语句还得改成这个形式:
public static int Main(string[] args) { // Create a type registrar and register any dependencies. // A type registrar is an adapter for a DI framework. var registrations = new ServiceCollection(); registrations.AddSingleton<IGreeter, HelloWorldGreeter>(); var registrar = new TypeRegistrar(registrations); // Create a new command app with the registrar // and run it with the provided arguments. var app = new CommandApp<DefaultCommand>(registrar); return app.Run(args); }
这种方法放弃了 Host 创建 HostedService,依赖注入的行为会由 TypeRegistrar
与 TypeResolver
控制。
由于 Spectre.Console.Cli
是依照 CLI 工具设计的,这类工具往往执行一次就自动退出返回控制台。因此它的注册器会在每次调用时重新创建 IServiceProvider
,如果直接将其改成多次执行,我们会发现所有对象都会重新初始化一遍,和 AddSingleton 之类的行为不同。
修改注册器行为,将其作为一个长期运行的单例执行,这样我们可以继续使用拓展方法注册,并注入到 HostedService 中。
public void Dispose() { //if (_provider is IDisposable disposable) //{ // disposable.Dispose(); //} }
private ITypeResolver _typeResolver; public ITypeResolver Build() { return _typeResolver ??= new TypeResolver(_builder.BuildServiceProvider()); }
这种方式下,外部的 DI 无法识别 CommandApp 内部注册的 Command 对象,使用时需要小心。