Argument Models#
Argument models provide a way to define arguments in a class, allowing arguments to be reused and more easily passed to other methods.
Let's consider this notify command.
public void Notify(string message, List<string> recipients,
[Option] bool dryrun, [Option('v')] bool verbose, [Option('q')] bool quiet)
{
// send notification
}
$ myapp.exe Notify --help
Usage: myapp.exe Notify [options] <message> <recipients>
Arguments:
message <TEXT>
recipients (Multiple) <TEXT>
Options:
--dryrun
-v | --verbose
-q | --quiet
In some cases, dryrun, verbose, and quite will be infrastructure concerns. For example...
- A UnitOfWork middleware could commit or abort a transaction based on the
dryrunvalue. - A Logging middleware could set the log level based on the
verboseandquietvalues. - We may want to assert
verboseandquietare mutually exclusive.
If we have multiple commands that use these same values, it's possible for them to be configured differently across commands. Take dryrun for example. Ask 5 different developers to add a dryrun option and you'll end up with 5 different casings for it. dryrun, dry-run, DryRun, Dryrun, ....
Use argument models to more easily reuse arguments, enforce consistency and make arguments easier to access from middleware.
Here's how they're configured
public void Notify(
NotificationArgs notificationArgs,
DryRunOptions dryRunOptions,
VerbosityOptions verbosityOptions)
{
// send notification
}
public class NotificationArgs : IArgumentModel
{
[Operand]
public string Message { get; set; } = null!;
[Operand]
public List<string> Recipients { get; set; } = null!;
}
public class DryRunOptions : IArgumentModel
{
[Option("dryrun")]
public bool IsDryRun { get; set; } = false;
}
public class VerbosityOptions : IArgumentModel, IValidatableObject
{
[Option('v')]
public bool Verbose { get; set; }
[Option('q')]
public bool Quite { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// use the CommandDotNet.DataAnnotations package to run this validation
if (Verbose && Quite)
yield return new ValidationResult("Verbose and Quiet are mutually exclusive. Choose one or the other.");
}
}
$ myapp.exe Notify --help
Usage: myapp.exe Notify [options] <Message> <Recipients>
Arguments:
Message <TEXT>
Recipients (Multiple) <TEXT>
Options:
--dryrun
-v | --Verbose
-q | --Quite
Notice the help output is the same, except for the casing of the names derived from the properties. The casing can be fixed by providing a name in the Operand and Option attributes, using lowercase property names, or using the NameCasing package to convert all command and argument names to the same case.
Tip
See Nullable Reference Types for avoiding "Non-nullable property is uninitialized" warnings in your argument models
Composition#
An IArgumentModel can be composed from other IArgumentModels allowing easy reuse of common arguments.
Using same example from above, we configure the arguments into a single model like this...
public void Notify(NotificationArgs notificationArgs)
{
// send notification
}
public class NotificationArgs : IArgumentModel
{
[Operand]
public string Message { get; set; } = null!;
[Operand]
public List<string> Recipients { get; set; } = null!;
public DryRunOptions DryRunOptions { get; set; } = null!;
public VerbosityOptions VerbosityOptions { get; set; } = null!;
}
Accessing from interceptors and middleware#
Instead of defining the model in each command method, the model could be defined in an interceptor of the root command and be available for all commands.
public Task<int> Interceptor(InterceptorExecutionDelegate next, CommandContext ctx,
DryRunOptions dryRunOptions, VerbosityOptions verbosityOptions)
{
IEnumerable<IArgumentModel> models = ctx.InvocationPipeline.All
.SelectMany(s => s.Invocation.FlattenedArgumentModels);
return next();
}
public void Notify(NotificationArgs notificationArgs)
{
// send notification
}
public class NotificationArgs : IArgumentModel
{
[Operand]
public string Message { get; set; } = null!;
[Operand]
public List<string> Recipients { get; set; } = null!;
}
public class DryRunOptions : IArgumentModel
{
[Option("dryrun", AssignToExecutableSubcommands = true)]
public bool IsDryRun { get; set; } = false;
}
public class VerbosityOptions : IArgumentModel
{
[Option('v', AssignToExecutableSubcommands = true)]
public bool Verbose { get; set; }
[Option('q', AssignToExecutableSubcommands = true)]
public bool Quite { get; set; }
}
$ myapp.exe Notify --help
Usage: myapp.exe Notify [options] <Message> <Recipients>
Arguments:
Message <TEXT>
Recipients (Multiple) <TEXT>
Options:
--dryrun
-v | --Verbose
-q | --Quite
Notice the use of AssignToExecutableSubcommands=true in the Option attributes.
This configures the option as if defined in the command methods.
Without this setting, the user would provide the options in the command hosting the interceptor method.
Notice in the interceptor method how the list of all argument models can be retrieved from the pipeline of the targetted command. Middleware can use this to fetch an argument model.
Guaranteeing the order of arguments#
Prior to version 4, argument position is not guaranteed to be consistent because the .Net Framework does not guarantee the order properties are reflected.
The GetProperties method does not return properties in a particular order, such as alphabetical or declaration order. Order can differ on each machine the app is deployed to. Your code must not depend on the order in which properties are returned because that order is no guaranteed.
For Operands, which are positional arguments, this can result in commands with operands in a non-deterministic order.
This is not reliability issue with Option because options are named, not positional. The only impact is the order options appear in help.
As of version 4, CommandDotNet can guarantee all arguments will maintain their position as defined within a class as long as the properties are decorated with OperandAttribute, OptionAtribute or OrderByPositionInClassAttribute.
What is OrderByPositionInClassAttribute?
[OrderByPositionInClass] is used to decorate properties in argument models that contain nested operands. It tells CommandDotNet to preserve the order of the properties as they appear in the class definition. Since operands are positional, their order matters for correct parsing.
When to use it:
- When a property of type
IArgumentModelcontains operands AND the property is not decorated with[Operand]or[Positional]
Not needed for:
- Properties already decorated with
[Operand]or[Positional](these already capture line numbers for ordering) - Nested models containing only options (options are named, so order doesn't affect parsing)
Example: public NotificationArgs NotificationArgs { get; set; } where NotificationArgs is an IArgumentModel containing operands needs [OrderByPositionInClass] to maintain order. However, [Operand] public NotificationArgs NotificationArgs { get; set; } does NOT need it because [Operand] already handles ordering.
How to use#
The OperandAttribute and OptionAtribute define an optional constructor parameter called __callerLineNumber. This uses the CallerLineNumberAttribute to auto-assign the line number in the class. Do Not provide a value for this parameter.
CommandDotNet will raise an exception when the order cannot be determined, which occurs when either
AppSettings.Arguments.DefaultArgumentMode == ArgumentMode.Operand(the default) and the property is not attributed- When a nested argument model containing an operand is not decorated with
[OrderByPositionInClass]
An example of invalidly nesting an IArgumentModel that contains operands
public class NotifyModel : IArgumentModel
{
public NotificationArgs NotificationArgs { get; set; }
public DryRunOptions DryRunOptions { get; set; }
public VerbosityOptions VerbosityOptions { get; set; }
}
And the error received because NotificationArgs contains operands
$ myapp.exe Notify --help
CommandDotNet.InvalidConfigurationException: Operand property must be attributed with OperandAttribute or OrderByPositionInClassAttribute to guarantee consistent order. Properties:
CommandDotNet.DocExamples.Arguments.Arguments.Argument_Models+Program_WithInvalidhNestedOperands+NotifyModel.NotificationArgs
This is the correct way to nest a model with operands
public class NotifyModel : IArgumentModel
{
[OrderByPositionInClass]
public NotificationArgs NotificationArgs { get; set; }
public DryRunOptions DryRunOptions { get; set; }
public VerbosityOptions VerbosityOptions { get; set; }
}
Recommendation#
- When possible, do not define operands in nested argument models.
- Always attribute operands in properties with
[Operand].