mayuki / Cocona

Micro-framework for .NET console application. Cocona makes it easy and fast to build console applications on .NET.
MIT License
3.22k stars 83 forks source link

Feature Request: Early Access to Parsed Command-Line Options for Service Configuration #117

Open SebaVDP opened 8 months ago

SebaVDP commented 8 months ago

Problem Statement:

In the current Cocona framework, command-line options are accessible primarily within command methods.
This design limits scenarios where global command-line options need to influence the application's setup and service configuration before any command execution.

Proposed Feature:

I propose a feature that allows global command-line options to be parsed and made available during the ServiceCollection initialization phase.
This would enable the use of command-line options for configuring services and other setup processes that occur before command execution.

Use Case Example:

A practical example is configuring a DatabaseFactory class that requires a connection string to create a DBConnection.
The connection string should ideally be provided as a global command-line option.
However, in the current Cocona framework, there's no straightforward way to pass this connection string from the command-line options to the DatabaseFactory service without involving command methods.

Current situation implementation:

public interface IOracleConnectionFactory
{
    OracleConnection Create();
}

public class OracleConnectionFactory : IOracleConnectionFactory
{
    private string? _connectionString;

    public OracleConnection Create()
    {
        if (_connectionString is null)
            throw new InvalidOperationException("Connection string is not set.");
        return new OracleConnection(_connectionString);
    }

    public void SetConnectionString(string connectionString)
    {
        _connectionString = connectionString;
    }
}

public interface IMyRepository
{
    void DoSomethingWithDb();
}

public class MyRepository : IMyRepository
{
    public readonly IOracleConnectionFactory _connectionFactory;

    public MyRepository(IOracleConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public void DoSomethingWithDb()
    {
        using var connection = _connectionFactory.Create();
        connection.Open();
        // do something with the connection
    }
}

public class MyCommands
{
    private readonly OracleConnectionFactory _connectionFactory;
    private readonly IMyRepository _myRepository;

    public MyCommands(OracleConnectionFactory connectionFactory, IMyRepository myRepository)
    {
        _connectionFactory = connectionFactory;
        _myRepository = myRepository;
    }

    public void DoSomethingCommand([Option('c', Description = "The connection string")] string connectionString)
    {
        _connectionFactory.SetConnectionString(connectionString);
        _myRepository.DoSomethingWithDb();
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var builder = CoconaApp.CreateBuilder();
        builder.Services.AddSingleton<OracleConnectionFactory>();
        builder.Services.AddSingleton<IOracleConnectionFactory>(sp => sp.GetRequiredService<OracleConnectionFactory>());
        builder.Services.AddTransient<IMyRepository, MyRepository>();
        var app = builder.Build();
        app.AddCommands<MyCommands>();
        app.Run();
    }
}

In this scenario, the DoSomethingCommand method in MyCommands is responsible for setting the connection string in the OracleConnectionFactory. This approach mixes the concerns of command logic and service configuration, which is not ideal.

Ideal implementation


public interface IOracleConnectionFactory
{
    OracleConnection Create();
}

public class OracleConnectionFactory : IOracleConnectionFactory
{
    private string _connectionString;

    public OracleConnectionFactory(GlobalOptions globalOptions)
    {
        _connectionString = globalOptions.ConnectionString;
    }

    public OracleConnection Create()
    {
        return new OracleConnection(_connectionString);
    }

}

public interface IMyRepository
{
    void DoSomethingWithDb();
}
public class MyRepository : IMyRepository
{
    public readonly IOracleConnectionFactory _connectionFactory;
    public MyRepository(IOracleConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public void DoSomethingWithDb()
    {
        using var connection = _connectionFactory.Create();
        connection.Open();
        // do something with the connection
    }
}

public class MyCommands
{
    private readonly IMyRepository _myRepository;

    public MyCommands(IMyRepository myRepository )
    {
        _myRepository = myRepository;
    }

    public void DoSomethingCommand()
    {
        _myRepository.DoSomethingWithDb();
    }
}

public class GlobalOptions
{
    [Option('c', Description = "The connection string")]
    public string ConnectionString { get; set; }
}

public class Program
{
    public static void Main(string[] args)
    {
        var builder = CoconaApp.CreateBuilder();
        builder.AddGlobalOptions<GlobalOptions>()
        builder.Services.AddSingleton<IOracleConnectionFactory, OracleConnectionFactory>);
        builder.Services.AddTransient<IMyRepository, MyRepository>();
        var app = builder.Build();
        app.AddCommands<MyCommands>();
        app.Run();
    }
}

Conclusion: This feature would greatly enhance Cocona's usability for applications where command-line options are integral to the configuration and setup.
It aligns with the principles of separation of concerns and would be a valuable addition to Cocona's capabilities.

jtsai-osa commented 7 months ago

Yes I too would love this please