farlee2121.System.CommandLine.PropertyMapBinder
1.0.0
dotnet add package farlee2121.System.CommandLine.PropertyMapBinder --version 1.0.0
NuGet\Install-Package farlee2121.System.CommandLine.PropertyMapBinder -Version 1.0.0
<PackageReference Include="farlee2121.System.CommandLine.PropertyMapBinder" Version="1.0.0" />
paket add farlee2121.System.CommandLine.PropertyMapBinder --version 1.0.0
#r "nuget: farlee2121.System.CommandLine.PropertyMapBinder, 1.0.0"
// 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)
- can be done with the existing setter overload, but prompts could be automated with a signature like
- 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 Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.CommandLine (>= 2.0.0-beta2.21617.1)
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 | 2,474 | 3/23/2022 |
1.0.0-preview1 | 1,095 | 3/5/2022 |
0.1.0-beta1 | 987 | 1/13/2022 |
0.1.0-alpha1 | 149 | 1/9/2022 |