Intégration de Spectre.Console.Cli et gestion du cycle de vie DI dans les applications .NET persistantes

Intégration dans une boucle interactive

Lors du développement d'outils en ligne de commande, il est parfois nécessaire de maintenir l'application active plutôt que de l'exécuter de manière éphémère. Si l'utilisation classique de Spectre.Console.Cli est conçue pour des exécutions uniques, il est tout à fait possible de l'intégrer dans une boucle interactive. L'astuce consiste à instancier le CommandApp et à lui transmettre dynamiquement les entrées utilisateur.

string userInput = Console.ReadLine();
await _commandApp.RunAsync(userInput.Split(' '));

Conflits d'injection de dépendances

Le problème survient lorsque l'on souhaite combiner cette approche avec l'injection de dépendances (DI) native de .NET. Considérons la configuration standard d'un hôte générique :

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((context, services) =>
        {
            services.AddSingleton<taskmanager>();
            services.AddHostedService<interactivecliservice>();
            services.ConfigureCommandApp();
        });</interactivecliservice></taskmanager>

Avec la méthode d'extension suivante pour initialiser l'application :

internal static IServiceCollection ConfigureCommandApp(this IServiceCollection services)
{
    return services.AddSingleton(sp =>
    {
        var app = new CommandApp();
        app.Configure(config =>
        {
            config.CaseSensitivity(CaseSensitivity.None);
            config.AddBranch<tasksettings>("remove", removeCmd =>
            {
                removeCmd.SetDefaultCommand<removetaskcommand>();
                removeCmd.AddCommand<removetaskcommand>("item");
                removeCmd.AddCommand<removeprojectcommand>("project");
            });
        });
        return app;
    });
}</removeprojectcommand></removetaskcommand></removetaskcommand></tasksettings>

Bien que cette configuration semble correcte, Spectre.Console.Cli possède son propre système de résolution de types. Les services enregistrés dans le conteneur externe (comme TaskManager) ne seront pas automatiquement injectés dans les comandes, car le conteneur interne de Spectre.Console n'y a pas accès par défaut.

Implémentation d'un pont DI personnalisé

Pour résoudre ce problème, Spectre.Console.Cli permet d'injecter un ITypeRegistrar personnalisé. Cela nous permet de créer un adaptateur vers Microsoft.Extensions.DependencyInjection. Voici une implémentation révisée de ce pont :

using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;

public sealed class CliTypeRegistrar : ITypeRegistrar
{
    private readonly IServiceCollection _services;

    public CliTypeRegistrar(IServiceCollection services)
    {
        _services = services;
    }

    public ITypeResolver Build()
    {
        return new CliTypeResolver(_services.BuildServiceProvider());
    }

    public void Register(Type service, Type implementation)
    {
        _services.AddTransient(service, implementation);
    }

    public void RegisterInstance(Type service, object implementation)
    {
        _services.AddSingleton(service, implementation);
    }

    public void RegisterLazy(Type service, Func<object> func)
    {
        ArgumentNullException.ThrowIfNull(func);
        _services.AddSingleton(service, _ => func());
    }
}

Et le résolveur de types correspondant :

using Spectre.Console.Cli;

public sealed class CliTypeResolver : ITypeResolver, IDisposable
{
    private readonly IServiceProvider _serviceProvider;

    public CliTypeResolver(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
    }

    public object Resolve(Type type)
    {
        return type == null ? null : _serviceProvider.GetService(type);
    }

    public void Dispose()
    {
        if (_serviceProvider is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

L'initialisation de l'application doit alors être modifiée pour utiliser ce registrateur :

public static int Main(string[] args)
{
    var services = new ServiceCollection();
    services.AddSingleton<igreeter customgreeter="">();
    
    var registrar = new CliTypeRegistrar(services);
    var app = new CommandApp<DefaultCommand>(registrar);
    
    return app.Run(args);
}</igreeter>

Correction du cycle de vie pour les services d'arrière-plan

Étant donné que Spectre.Console.Cli est optimisé pour des outils CLI éphémères, son registrateur est conçu pour recréer et détruire le IServiceProvider à chaque exécution. Dans le contexte d'une application persistante (comme un HostedService), cela pose un problème majeur : les services enregistrés comme Singleton seront réinitialisés à chaque invocation de la commande.

Pour adapter ce comportement à une application à longue durée de vie, il est nécessaire de modifier le résolveur pour qu'il cnoserve le fournisseur de services et évite de le détruire prématurément.

Premièrement, on neutralise la libération du fournisseur dans le CliTypeResolver :

public void Dispose()
{
    // Ne pas disposer le ServiceProvider racine dans un contexte d'application persistante.
    // La gestion du cycle de vie doit être laissée à l'hôte principal.
}

Deuxièmement, on met en cache l'instance du résolveur dans le CliTypeRegistrar pour garantir qu'un seul IServiceProvider soit utilisé tout au long de la vie de l'application :

private ITypeResolver _cachedResolver;

public ITypeResolver Build()
{
    return _cachedResolver ??= new CliTypeResolver(_services.BuildServiceProvider());
}

Note importante : Avec cette architecture, le conteneur DI externe ne peut pas résoudre les types de commandes internes enregistrés directement dans la configuration de Spectre.Console.Cli. Il faut veiller à mainteinr une séparation claire entre les dépendances de l'application et celles spécifiques au moteur de commandes.

Étiquettes: spectre-console dependency-injection dotnet CLI microsoft-extensions-dependencyinjection

Publié le 11 juin à 19h31