command-line-api icon indicating copy to clipboard operation
command-line-api copied to clipboard

Excessive Boilerplate code when using Model Binding. Am i doing something wrong?

Open thesn10 opened this issue 1 year ago • 1 comments

I am building a command line app with 14+ options per command. The docs reccommend to use model binding if you have more than 8 options, but when i implemented the Binder, i became more and more unsure if i was doing this correctly, because there was just soo much boilerplate. Am i doing something wrong here? How can i properly handle 8+ arguments?

Excessive boilerplate example

You can see just the sheer amount of unnecessary boilerplate just passing around the options objects

public class ThumbnailOptionsBinder : BinderBase<ThumbnailOptions>
{
    private readonly Option<TimeSpan> _startTimeOption;
    private readonly Option<TimeSpan> _intervalOption;
    private readonly Option<TimeSpan> _endTimeOption;
    private readonly Option<string> _filenameOption;
    private readonly Option<bool> _fastModeOption;
    private readonly Option<string> _webVTTOption;
    private readonly Option<string> _imagePathOption;
    private readonly Option<int> _frameWidthOption;
    private readonly Option<int> _frameHeightOption;
    private readonly Option<int> _columnsOption;
    private readonly Option<int> _rowsOption;
    private readonly Option<int> _borderWidthOption;
    private readonly Option<string> _backgroundGradientStartOption;
    private readonly Option<string> _backgroundGradientEndOption;

    public ThumbnailOptionsBinder(
        Option<TimeSpan> startTimeOption,
        Option<TimeSpan> intervalOption,
        Option<TimeSpan> endTimeOption,
        Option<string> filenameOption,
        Option<bool> fastModeOption,
        Option<string> webVTTOption,
        Option<string> imagePathOption,
        Option<int> frameWidthOption,
        Option<int> frameHeightOption,
        Option<int> columnsOption,
        Option<int> rowsOption,
        Option<int> borderWidthOption,
        Option<string> backgroundGradientStartOption,
        Option<string> backgroundGradientEndOption)
    {
        _startTimeOption = startTimeOption;
        _intervalOption = intervalOption;
        _endTimeOption = endTimeOption;
        _filenameOption = filenameOption;
        _fastModeOption = fastModeOption;
        _webVTTOption = webVTTOption;
        _imagePathOption = imagePathOption;
        _frameWidthOption = frameWidthOption;
        _frameHeightOption = frameHeightOption;
        _columnsOption = columnsOption;
        _rowsOption = rowsOption;
        _borderWidthOption = borderWidthOption;
        _backgroundGradientStartOption = backgroundGradientStartOption;
        _backgroundGradientEndOption = backgroundGradientEndOption;
    }

    protected override ThumbnailOptions GetBoundValue(BindingContext bindingContext) =>
        new ThumbnailOptions
        {
            StartTime = bindingContext.ParseResult.GetValueForOption(_startTimeOption),
            Interval = bindingContext.ParseResult.GetValueForOption(_intervalOption),
            EndTime = bindingContext.ParseResult.GetValueForOption(_endTimeOption),
            Filename = bindingContext.ParseResult.GetValueForOption(_filenameOption),
            FastMode = bindingContext.ParseResult.GetValueForOption(_fastModeOption),
            WebVTT = bindingContext.ParseResult.GetValueForOption(_webVTTOption),
            ImagePath = bindingContext.ParseResult.GetValueForOption(_imagePathOption),
            FrameWidth = bindingContext.ParseResult.GetValueForOption(_frameWidthOption),
            FrameHeight = bindingContext.ParseResult.GetValueForOption(_frameHeightOption),
            Columns = bindingContext.ParseResult.GetValueForOption(_columnsOption),
            Rows = bindingContext.ParseResult.GetValueForOption(_rowsOption),
            BorderWidth = bindingContext.ParseResult.GetValueForOption(_borderWidthOption),
            BackgroundGradientStart = bindingContext.ParseResult.GetValueForOption(_backgroundGradientStartOption),
            BackgroundGradientEnd = bindingContext.ParseResult.GetValueForOption(_backgroundGradientEndOption),
            // Add other rendering options here
        };
}

class Program
{
    static async Task<int> Main(string[] args)
    {
        var rootCommand = new RootCommand();

        var startTimeOption = new Option<TimeSpan>("--start-time", () => TimeSpan.FromSeconds(60), "Start time");
        var intervalOption = new Option<TimeSpan>("--interval", () => TimeSpan.FromSeconds(5), "Interval");
        var endTimeOption = new Option<TimeSpan>("--end-time", () => TimeSpan.FromMinutes(4), "End time");
        var filenameOption = new Option<string>("--filename", () => Path.GetFullPath("./thumbnail_systemdrawing.bmp"), "Output filename");
        var fastModeOption = new Option<bool>("--fast-mode", "Use fast mode");
        var webvttOption = new Option<string>("--webvtt", "WebVTT file");
        var imagePathOption = new Option<string>("--image-path", "Image path for WebVTT");
        var frameWidthOption = new Option<int>("--frame-width", () => 192, "Frame width");
        var frameHeightOption = new Option<int>("--frame-height", () => 108, "Frame height");
        var columnsOption = new Option<int>("--columns", () => 4, "Columns for tiling");
        var rowsOption = new Option<int>("--rows", () => 4, "Rows for tiling");
        var borderWidthOption = new Option<int>("--border-width", () => 8, "Border width");
        var backgroundGradientStartOption = new Option<string>("--background-gradient-start", "Background gradient start color");
        var backgroundGradientEndOption = new Option<string>("--background-gradient-end", "Background gradient end color");

        rootCommand.Add(startTimeOption);
        rootCommand.Add(intervalOption);
        rootCommand.Add(endTimeOption);
        rootCommand.Add(filenameOption);
        rootCommand.Add(fastModeOption);
        rootCommand.Add(webvttOption);
        rootCommand.Add(imagePathOption);
        rootCommand.Add(frameWidthOption);
        rootCommand.Add(frameHeightOption);
        rootCommand.Add(columnsOption);
        rootCommand.Add(rowsOption);
        rootCommand.Add(borderWidthOption);
        rootCommand.Add(backgroundGradientStartOption);
        rootCommand.Add(backgroundGradientEndOption);

        var thumbnailOptionsBinder = new ThumbnailOptionsBinder(
            startTimeOption,
            intervalOption,
            endTimeOption,
            filenameOption,
            fastModeOption,
            webvttOption,
            imagePathOption,
            frameWidthOption,
            frameHeightOption,
            columnsOption,
            rowsOption,
            borderWidthOption,
            backgroundGradientStartOption,
            backgroundGradientEndOption
        );

        rootCommand.SetHandler(async (thumbnailOptions) =>
        {
            await Run(thumbnailOptions);
        }, thumbnailOptionsBinder);

        return await rootCommand.InvokeAsync(args);
    }
}

Problems of this code:

  • Excessive Code Redundancy: The constructor of the Binder and the subsequent setup of options in the Program class are excessively redundant. This violates the DRY (Don't Repeat Yourself) principle and makes the codebase unnecessarily complex.

  • Maintenance Nightmare: Any change or addition to the command line options requires modifications in multiple places, making maintenance a nightmare. Developers should not have to tediously update both the constructor and the Program class for each option.

  • Readability and Clarity: The readability of the code is compromised due to the extensive setup of options and bindings, making it hard for developers to quickly grasp the functionality.

Am i doing something wrong here? How can i properly handle 8+ arguments? Is this really how the model binding should be used? If, yes, then I urge the maintainers to provide a more elegant and maintainable solution for setting up command line options and binders, as this much boilerplate just doesnt seem like the right way forward. Thanks

thesn10 avatar Jan 31 '24 20:01 thesn10

How about this? Your whole app:


Cli.Ext.ConfigureServices(services =>
{
    var loggerFactory = LoggerFactory.Create(
        builder => builder
            .AddConsole()
            .SetMinimumLevel(LogLevel.Information));

    services.AddSingleton(loggerFactory);
});


return await Cli.RunAsync<ThumbnailCommand>(args);


[CliCommand]
public class ThumbnailCommand
{
    private readonly ILoggerFactory loggerFactory;

    public ThumbnailCommand(ILoggerFactory loggerFactory)
    {
        this.loggerFactory = loggerFactory;
    }

    [CliOption]
    public TimeSpan StartTime { get; set; }

    [CliOption]
    public TimeSpan Interval { get; set; }

    [CliOption]
    public TimeSpan EndTime { get; set; }

    [CliOption]
    public FileInfo OutputFile { get; set; }

    [CliOption]
    public FileInfo InputFile { get; set; }

    [CliOption]
    public bool FastMode { get; set; }

    [CliOption]
    public FileInfo WebVTT { get; set; }

    [CliOption]
    public string ImagePath { get; set; }

    [CliOption]
    public int FrameWidth { get; set; }

    [CliOption]
    public int FrameHeight { get; set; }

    [CliOption]
    public int Columns { get; set; }

    [CliOption]
    public int Rows { get; set; }

    [CliOption]
    public int BorderWidth { get; set; }

    [CliOption]
    public string BackgroundGradientStart { get; set; }

    [CliOption]
    public string BackgroundGradientEnd { get; set; }

    public async Task<int> RunAsync()
    {
        var opts = new ThumbGenOptions()
            .WithStartTime(StartTime);

        //...

        await thumbnailGenerator.ExecuteAsync(opts);

        return 0;
    }
}

This is possible with DotMake.CommandLine which makes System.CommandLine easy to use and maintain. Check it out!

calacayir avatar Feb 13 '24 21:02 calacayir