Spectre.Console-處理依賴注入

2023-06-02 06:04:30

引言

之前說的做自動記錄 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 並沒有什麼用。

放棄 Host

雖然 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,依賴注入的行為會由 TypeRegistrarTypeResolver 控制。

修改註冊器行為

由於 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 物件,使用時需要小心。

參考