spectreconsole / spectre.console

A .NET library that makes it easier to create beautiful console applications.
https://spectreconsole.net
MIT License
9.45k stars 499 forks source link

Allow accessing the command settings through registered services. #1221

Closed JKamsker closed 7 months ago

JKamsker commented 1 year ago

Is your feature request related to a problem? Please describe. I have a service that is registered with di in IServiceCollection. It would be nice if that service could access the settings, so that it doesnt have to be initialized by the command itself. The settings implement a specific baseclass or interface.

Describe the solution you'd like

public class ConnectionService
{
    private readonly IHasConnectionString _settings;

    public MyService(ICommandSettings<IHasConnectionString> settings)
    {
        _settings = settings.Value;
    }
}

or

public class ConnectionService
{
    private readonly IHasConnectionString _settings;

    public MyService(ICommandSettings settings)
    {
        _settings = settings.Value<IHasConnectionString>();
    }
}

or through DI:

public class ConnectionService
{
    private readonly IHasConnectionString _settings;

    public MyService(IHasConnectionString settings)
    {
        _settings = settings.Value;
    }
}

// Programm.cs
var services = new ServiceCollection();
var registrar = new TypeRegistrar(services);
var app = new CommandApp(registrar);

app.Configure(c =>
{
    // This will register the settings type as IHasConnectionString if it is said type
    c.ConfigureSettings<IHasConnectionString>();
});

With this approach, ConnectionService cannot be instantiated without Settings implementing IHasConnectionString.

Describe alternatives you've considered I've looked into the source of SpectreConsole and did not find any alternative way. Only other option would be to parse the settings myself.

FrankRay78 commented 8 months ago

Hi @JKamsker, I'm triaging the CLI issues and wanted to understand this one a bit better. My feeling is perhaps what you are asking for doesn't fit into the current spectre.console DI implementation pattern, but equally the existing functionality is sufficient.

I've knocked up the following example, as a test bed. Basically we have a service, service config, command, and command settings:

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

namespace ConsoleApp1;

public interface IConnectionSettings
{
    string ConnectionString { get; set; }
}

public interface IConnectionService
{
    void ConnectToDatabase();
}

/// <summary>
/// Settings provider for the ConnectionService
/// </summary>
public sealed class ConnectionSettings : IConnectionSettings
{
    public string ConnectionString { get; set; }
}

/// <summary>
/// Example database connection service
/// </summary>
public class ConnectionService : IConnectionService
{
    private readonly IConnectionSettings settings;

    public ConnectionService(IConnectionSettings settings)
    {
        this.settings = settings;
    }

    public void ConnectToDatabase()
    {
        // Establish a connection to the database
        AnsiConsole.WriteLine($"Connecting to database, connection string: {settings.ConnectionString}");
    }
}

public sealed class DatabaseCommandSettings : CommandSettings
{
    [CommandOption("--DryRun <THE_VALUE>")]
    public bool DryRun { get; set; }
}

public sealed class DatabaseCommand : Command<DatabaseCommandSettings>
{
    public override int Execute(CommandContext context, DatabaseCommandSettings settings)
    {
        AnsiConsole.WriteLine($"DryRun: {settings.DryRun}");
        return 0;
    }
}

public class Program
{
    static void Main(string[] args)
    {
        var connectionSettings = new ConnectionSettings() { ConnectionString = "server=127.0.0.1;user=bob;password=squarepants;" };

        var registrations = new ServiceCollection();
        registrations.AddSingleton<IConnectionSettings>(connectionSettings);
        registrations.AddSingleton<IConnectionService, ConnectionService>();
        var registrar = new TypeRegistrar(registrations);

        var app = new CommandApp<DatabaseCommand>(registrar);
        app.Run(args);
    }
}

(nb, the above uses the following implementations: TypeRegistrar, TypeResolver)

In the above example, the command and service are unrelated, and executing the application with ConsoleApp1.exe --DryRun just runs the command, ie:

image

I suspect this issue is asking for DatabaseCommandSettings to somehow be injected into ConnectionService - is that correct @JKamsker?

I cannot think of an easy way to do that, given the TypeResolver is populated prior to the CommandSettings being created off the args, and then provided to the command when the Execute method is called.

Equally, the following modification to the DatabaseCommand will inject the ConnectionService into the DatabaseCommand:

public sealed class DatabaseCommand : Command<DatabaseCommandSettings>
{
    IConnectionService connectionService;

    public DatabaseCommand(IConnectionService connectionService)
    {
        this.connectionService = connectionService;
    }

    public override int Execute(CommandContext context, DatabaseCommandSettings settings)
    {
        AnsiConsole.WriteLine($"DryRun: {settings.DryRun}");
        connectionService.ConnectToDatabase();
        return 0;
    }
}

This allows the command to propagate whatever command settings it wishes when making calls on the service.

image

Does this fully satisfy the issue, or have I misunderstood the ask?

If so, can you please annotate my code example above, with the specific usage pattern being requested?

JKamsker commented 7 months ago

The problem was that i wanted to get the connectionstring from the settings. Ig that issue can be resolved now, because interceptors got enhanced.

My solution: