farlee2121.System.CommandLine.PropertyMapBinder 1.0.0

.NET Standard 2.0
NuGet\Install-Package farlee2121.System.CommandLine.PropertyMapBinder -Version 1.0.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
dotnet add package farlee2121.System.CommandLine.PropertyMapBinder --version 1.0.0
<PackageReference Include="farlee2121.System.CommandLine.PropertyMapBinder" Version="1.0.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add farlee2121.System.CommandLine.PropertyMapBinder --version 1.0.0
#r "nuget: farlee2121.System.CommandLine.PropertyMapBinder, 1.0.0"
#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package.
// Install farlee2121.System.CommandLine.PropertyMapBinder as a Cake Addin
#addin nuget:?package=farlee2121.System.CommandLine.PropertyMapBinder&version=1.0.0

// Install farlee2121.System.CommandLine.PropertyMapBinder as a Cake Tool
#tool nuget:?package=farlee2121.System.CommandLine.PropertyMapBinder&version=1.0.0

System.CommandLine.PropertyMapBinding

Motivation / what is this

The goal is to create an intuitive handler binding experience for System.CommandLine. A few sub-goals include

  • intuitive binding of complex types
  • blending multiple binding rules for a customizable and consistent binding experience
  • easy extension of the binding pipeline
  • support handler declaraction as a self-contained expression (no reference to symbol instances)

Examples

All examples assume the following definitions are available

Option<int> frequencyOpt = new Option<int>(new string[] { "--frequency", "-f" }, "such description");

RootCommand rootCommand = new RootCommand("Test Test")
{
    new Argument<string>("print-me", "gets printed"),
    frequencyOpt, 
    new Option<IEnumerable<int>>(new string[] { "--list", "-l" }, "make sure lists work")
    {
        Arity = ArgumentArity.ZeroOrMore
    };
};

public static async Task SuchHandler(SuchInput input)
{
    Console.WriteLine($"printme: {input.PrintMe}; \nfrequency: {input.Frequency}; \nlist:{string.Join(",",input.SuchList)}");
}

public class SuchInput {
    public int Frequency { get; set; }
    public string? PrintMe { get; set; }
    public IEnumerable<int> SuchList { get; set; } = Enumerable.Empty<int>();
}

Pipeline

The backbone construct is BinderPipeline.

rootCommand.Handler = new BinderPipeline<SuchInput>()
    .MapFromName("print-me", model => model.PrintMe)
    .MapFromReference(frequencyOpt, model => model.Frequency)
    .MapFromName("-l", model => model.SuchList)
    .ToHandler(SuchHandler);

BinderPipeline is really a collection of IPropertyBinder. Each IPropertyBinder defines a strategy for assigning input to the target object. The pipeline executes each binder in the order they are given. This means later binders will override earlier ones. This also means we can

  • use multiple rules to bind properties
  • define a priority/fallback chain for any given property

Blended Conventions

The pipeline can handle many approaches to binding input. Here's an example using a simple naming convention with an explicit mapping fallback

rootCommand.Handler = new BinderPipeline<SuchInput>()
    .MapFromNameConvention(NameConvention.PascalCaseComparer)
    .MapFromName("-l", model => model.SuchList)
    .ToHandler(SuchHandler);

More conventions can be added to this pipeline. Here are some cases I haven't implemented, but would be fairly easy to add

  • map default values from configuration
  • Ask a user for any missing inputs
    • can be done with the existing setter overload, but prompts could be automated with a signature like .PromptIfMissing(name, selector)
  • match properties based on type

See How to Extend for more detail.

Binding To Existing Models

Sometimes we might want to initialize our input model separately from the input binding process (e.g. default model from configuration).

That's easy enough

SuchInput existingModelInstance = //...
rootCommand.Handler = new BinderPipeline<SuchInput>()
    .ToHandler(SuchHandler, existingModelInstance);

Initializing a model with required data

Some models may want to enforce guarantees about data through the constructor, or some fields may not allow modification after initialization.

This can be handled similarly to System.Commandline's SetHandler.

IModelFactory<SuchInput> modelFactory = ModelFactory.FromSymbolMap((int frequency, string printMe) => new InputModel(frequency, printMe), frequencyOpt, printMeArg);
rootCommand.Handler = new BinderPipeline<SuchInput>()
    .ToHandler(SuchHandler, modelFactory);

The same can be accomplished with option and argument aliases

IModelFactory<SuchInput> modelFactory = ModelFactor.FromNameMap((int frequency, string printMe) => new InputModel(frequency, printMe), "-f", "print-me");
rootCommand.Handler = new BinderPipeline<SuchInput>()
    .ToHandler(SuchHandler, modelFactory);

How to extend

Extending the pipeline is fairly easy.

The core contract is

public interface IPropertyBinder<InputModel>
{
    InputModel Bind(InputModel InputModel, InvocationContext context);
}

IPropertyBinder takes an instance of the target input class and the invocation context provided by the parser.

Input definitions (i.e. options and arguments) can be found in context.ParserResult.CommandResult.Symbol.Children and values can be fetched by functions like context.ParseResult.GetValueForOption.

Examples exist for symbol name and property path and simple name conventions.

The other key step is to register extension methods on BinderPipeline. The main behaviors to consider

  • the extension should add it's binder to the end of the pipeline (e.g. pipeline.Add(yourBinder))
  • The extension should return the modified copy of the pipeline (i.e. always has return type BinderPipeline<T>)
// Example pipeline extension
public static class BinderPipelineExtensions{
    public static BinderPipeline<InputModel> MapFromNameConvention<InputModel>(this BinderPipeline<InputModel> pipeline, NameConventionComparer comparer)
    {
        pipeline.Add(new NameConventionBinder<InputModel>(comparer)); // this adds an IPropertyBinder<T>
        return pipeline; // be sure to return the pipeline for further chaining
    }
}

How to handle Dependency Injection?

Short: Invoke the dependency container in the handler function (i.e ToHandler(handlerFunction))

This position of the library is to keep dependency injection separate from model binding. Some reasons include

  • Keeping the two activities separate simplifies error diagnosis and improves code clarity
  • Dependency containers can easily be invoked from within the handlers
  • Input values may need registered with the dependency container, which requires the input model to be complete before the dependency container
  • The input model should only model the possible input. It is not responsible for composition or behavior.
    • The handler function exists to bridge between the input model and consumers.

Status of project

A successful experiment. The core builder experience is likely stable, but the API could still change given feedback/experience.

The library is usable, has tests, but has no guarantees (including support).

Product Versions
.NET net5.0 net5.0-windows net6.0 net6.0-android net6.0-ios net6.0-maccatalyst net6.0-macos net6.0-tvos net6.0-windows
.NET Core netcoreapp2.0 netcoreapp2.1 netcoreapp2.2 netcoreapp3.0 netcoreapp3.1
.NET Standard netstandard2.0 netstandard2.1
.NET Framework net461 net462 net463 net47 net471 net472 net48
MonoAndroid monoandroid
MonoMac monomac
MonoTouch monotouch
Tizen tizen40 tizen60
Xamarin.iOS xamarinios
Xamarin.Mac xamarinmac
Xamarin.TVOS xamarintvos
Xamarin.WatchOS xamarinwatchos
Compatible target framework(s)
Additional computed target framework(s)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on farlee2121.System.CommandLine.PropertyMapBinder:

Package Downloads
farlee2121.System.CommandLine.PropertyMapBinder.NameConventionBinder

A System.CommandLine.PropertyMapBinder extension for mapping console input to properties by simple naming conventions

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 294 3/23/2022
1.0.0-preview1 193 3/5/2022
0.1.0-beta1 101 1/13/2022
0.1.0-alpha1 83 1/9/2022