- A+
Spectre.Console
大家可能都不陌生,写控制台程序美化还是不错的,支持着色,表格,图标等相当nice,如果对这个库不熟悉我强烈推荐你了解一下,对于写一些CLI小工具还是相当方便的, 本文主要讲讲 Spectre.Console.Cli
的服务注入, TA是 Spectre.Console 库的一部分,用于创建命令行界面(CLI)应用程序。它提供了一个强大且易于使用的API来定义命令、参数和选项,同时支持 Spectre.Console 的丰富输出格式化功能。
一个官方极简的例子,定义一个GreetCommand:
public class GreetCommand : Command<GreetCommand.Settings> { public class Settings : CommandSettings { [CommandArgument(0, "<name>")] [Description("The name of the person to greet.")] public string Name { get; set; } [CommandOption("-r|--repeat <times>")] [Description("The number of times to repeat the greeting.")] [DefaultValue(1)] public int Repeat { get; set; } } public override int Execute(CommandContext context, Settings settings) { for (int i = 0; i < settings.Repeat; i++) { Console.WriteLine($"Hello, {settings.Name}!"); } return 0; } }
接下来,在程序的入口点配置Command
public class Program { public static int Main(string[] args) { var app = new CommandApp(); app.Configure(config => { config.AddCommand<GreetCommand>("greet"); }); return app.Run(args); } }
对于Spectre.Console.Cli的常规使用我这里不做过多介绍,感兴趣的同学可以移步官方文档,本文主要讲一下在CLI程序中如何注入服务
那么我们需要在GreetCommand中注入服务应该怎么做呢? 比如下面的一个服务:
public class HelloService(ILogger<HelloService> logger) { public Task<string> SayHello(string name, int age) { //注入的logger logger.LogInformation("SayHello called with name:{name},age:{age}", name, age); return Task.FromResult($"Hello,My name is {name}, I`m {age} years old!"); } }
其实Spectre.Console.Cli
内置了最简单的方式,我们只需要在app.Configure
中完成:
var services = new ServiceCollection(); //添加服务 services.AddSingleton<HelloService>(); //添加日志 services.AddLogging(config => { config.AddConsole(); }); var sp = services.BuildServiceProvider(); app.Configure(config => { //添加Commands config.AddCommand<OneCommand>("one"); config.AddCommand<AnotherCommand>("another"); //注册Services config.Settings.Registrar.RegisterInstance(sp.GetRequiredService<HelloService>()); });
注册的服务就可以直接使用了:
public class HelloCommand(HelloService helloService) : AsyncCommand<HelloCommand.HelloSettings> { private readonly HelloService _helloService = helloService; public class HelloSettings : CommandSettings { [CommandArgument(0, "<name>")] [Description("The target to say hello to.")] public string Name { get; set; } = null!; } public override async Task<int> ExecuteAsync(CommandContext context, HelloSettings settings) { var message = await _helloService.SayHello(settings.Name, settings.Age); AnsiConsole.MarkupLine($"[blue]{message}[/]"); return 0; } }
另外的一个注入方式是实现ITypeRegistrar
,官方提供MSDI的用例,自己也可以实现Autofac
等其他DI,下面是MSDI的实现:
namespace Infrastructure { public sealed class MsDITypeRegistrar(IServiceCollection services) : ITypeRegistrar { private readonly IServiceCollection _services = services ?? throw new ArgumentNullException(nameof(services)); public ITypeResolver Build() { return new TypeResolver(_services.BuildServiceProvider()); } public void Register(Type service, Type implementation) { _services.AddSingleton(service, implementation); } public void RegisterInstance(Type service, object implementation) { _services.AddSingleton(service, implementation); } public void RegisterLazy(Type service, Func<object> factory) { _services.AddSingleton(service, (provider) => factory()); } } internal sealed class TypeResolver(IServiceProvider provider) : ITypeResolver { public object? Resolve(Type? type) { if (provider is null || type is null) { return null; } return ActivatorUtilities.GetServiceOrCreateInstance(provider, type); } } }
使用的话只需要实例化CommandApp
时候传入MsDITypeRegistrar即可:
var services = new ServiceCollection(); //添加服务... var app = new CommandApp(new MsDITypeRegistrar(services)); app.Configure(config => { //... }); return app.Run(args);
除了上面的方式,我们其实还可以使用ICommandInterceptor
切面的方式来完成注入的操作:
下面我们定义一个AutoDIAttribute
特性,实现一个AutoDIInterceptor
的拦截器,后者主要给标记了AutoDI
的属性服务赋值
[AttributeUsage(AttributeTargets.Property, Inherited = true)] public class AutoDIAttribute : Attribute{ } /// <summary> /// 自动注入的拦截器 /// </summary> internal class AutoDIInterceptor(IServiceProvider serviceProvider) : ICommandInterceptor { public void Intercept(CommandContext context, CommandSettings settings) { var type = settings.GetType(); var properties = type.GetProperties(); foreach (var property in properties) { var isAutoInject = property.GetCustomAttributes<AutoDIAttribute>(true).Any(); if (isAutoInject) { var service = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, property.PropertyType); property.SetValue(settings, service); } } } }
接下来在CommandSettings中标记需要自动注入服务的属性,如下面的HelloService
:
internal class AutoDICommand : AsyncCommand<AutoDICommand.AnotherInjectSettings> { public class AnotherInjectSettings : CommandSettings { /// <summary> /// 使用切面装载的服务 /// </summary> [AutoDI] public HelloService HelloService { get; set; } = null!; [Description("user name")] [DefaultValue("vipwan"), CommandOption("-n|--name")] public string Name { get; set; } = null!; [Description("user age")] [DefaultValue(12), CommandOption("-a|--age")] public int Age { get; set; } } public override async Task<int> ExecuteAsync(CommandContext context, AnotherInjectSettings settings) { var message = await settings.HelloService.SayHello(settings.Name, settings.Age); AnsiConsole.MarkupLine($"[green]{message}[/]"); return 0; } }
然后在app.Configure
中使用AutoDIInterceptor
切面:
var services = new ServiceCollection(); //添加服务 services.AddSingleton<HelloService>(); var sp = services.BuildServiceProvider(); app.Configure(config => { //设置自动注入的拦截器 config.SetInterceptor(new AutoDIInterceptor(sp)); config.AddCommand<AutoDICommand>("di"); //... });
然后测试运行程序:
dotnet run -- di -n "vipwan"
大功告成:
以上就介绍了几种在Spectre.Console.Cli
注入服务的方式,当然没有最优的只有最适合自己的,如果代码存在不足,或者你有更好的建议 欢迎留言交流!