Skip to content

Arity & Default Values#

Arity#

Arity describes how many values a user can or must provide for an argument.

ArgumentArity is expressed as a minimum and maximum value

  • shorthand: {min}..{max}. eg. 0..1 min=0 max=1

When...

  • minimum == 0, no values are required
  • maximum > 1, multiple values can be provided
  • maximum == int.MaxValue, an unlimited number of values can be provided

Possible Arities#

none (flags) optional required
single value 0..0 0..1 1..1
list values -- 0..N(>0) M(>0)..N(>=M)
  • arity for single value can be
    • 0..0 Zero
    • 0..1 ZeroOrOne (optional)
    • 1..1 ExactlyOne (required)
  • arity for list values can be
    • 0..N(>0) ZeroOrMore (optional) N must be greater than 0
    • M(>0)..N(>=M) OneOrMore (required) N must be greater than or equal to M.
      • If N equal M, the list must have exactly M values.

Currently, the only way to set the arity for an argument to something other than one of the above is to get the instance of the argument from the CommandContext and assign a new ArgumentArity to the IArgument.Arity. This can be done via middleware or interceptor methods. We've captured the work here but it hasn't been a priority for us. This would be an easy feature to contribute to.

Optional Arguments#

An argument is considered optional when defined as...

  • a Nullable type: bool?, Guid?
  • a Nullable reference type (NRT): object?
  • an optional parameter
  • an IArgumentModel property with a default value where the default value != default(T)
    • the value must be set in the ctor or property assignment. This condition is evaluated immediately after instantiation.

Validation#

CommandDotNet will check if the minimum or maximum arity has been exceeded and raise an error. Here's examples of how arity works.

Single value types#

public void DefaultCommand(Model model,
        bool requiredBool, Uri requiredRefType, 
        bool? nullableBool, Uri? nullableRefType,
        bool optionalBool = false, Uri optionalRefType = null)
{}

public class Model : IArgumentModel
{
    [Operand] public bool RequiredBool { get; set; }
    [Operand] public bool DefaultBool { get; set; } = true;
    [Operand] public Uri RequiredRefType { get; set; }
    [Operand] public Uri DefaultRefType { get; set; } = new ("http://apple.com");
}
snippet source | anchor

$ app.exe --help
Usage: app.exe <RequiredBool> <DefaultBool> <RequiredRefType> <DefaultRefType> <requiredBool> <requiredRefType> [<nullableBool> <nullableRefType> <optionalBool> <optionalRefType>]

Arguments:

  RequiredBool     <BOOLEAN>
  Allowed values: true, false

  DefaultBool      <BOOLEAN>  [True]
  Allowed values: true, false

  RequiredRefType  <URI>

  DefaultRefType   <URI>      [http://apple.com/]

  requiredBool     <BOOLEAN>
  Allowed values: true, false

  requiredRefType  <URI>

  nullableBool     <BOOLEAN>
  Allowed values: true, false

  nullableRefType  <URI>

  optionalBool     <BOOLEAN>  [False]
  Allowed values: true, false

  optionalRefType  <URI>
snippet source | anchor

Here are the errors when required arguments are missing

$ app.exe 
RequiredBool is required
RequiredRefType is required
requiredBool is required
requiredRefType is required
snippet source | anchor

Notice

there are only 4 operands listed as missing but there are 6 operands listed as required in the usage section. This is because operands are positional so even though the DefaultBool and DafaultRefType are not required based on property definition, they are effectively required because values must be provided for them before the required operands positioned after them. Keep this in mind when designing your commands. Always position optional operands after required operands.

Collection types#

We'll use options for these because we can have only one collection operand per command.

public void DefaultCommand(
        [Option('b')] bool[] requiredBool, [Option('u')] Uri[] requiredRefType,
        [Option] bool[]? nullableBool, [Option] Uri[]? nullableRefType,
        [Option] bool[] optionalBool = null, [Option] Uri[] optionalRefType = null)
snippet source | anchor

$ app.exe --help
Usage: app.exe [options]

Options:

  -b | --requiredBool (Multiple)     <BOOLEAN>
  Allowed values: true, false

  -u | --requiredRefType (Multiple)  <URI>

  --nullableBool (Multiple)          <BOOLEAN>
  Allowed values: true, false

  --nullableRefType (Multiple)       <URI>

  --optionalBool (Multiple)          <BOOLEAN>
  Allowed values: true, false

  --optionalRefType (Multiple)       <URI>
snippet source | anchor

We receive the same errors when required arguments are missing

$ app.exe 
requiredBool is required
requiredRefType is required
snippet source | anchor

Static Helpers#

ArgumentArity contains the following members to encapsulate the common use cases.

public static IArgumentArity Zero => new ArgumentArity(0, 0);
public static IArgumentArity ZeroOrOne => new ArgumentArity(0, 1);
public static IArgumentArity ExactlyOne => new ArgumentArity(1, 1);
public static IArgumentArity ZeroOrMore => new ArgumentArity(0, Unlimited);
public static IArgumentArity OneOrMore => new ArgumentArity(1, Unlimited);
snippet source | anchor

The static method ArgumentArity.Default(IArgument argument) will return one of these static values based on the type

There are several extension methods that make it easier check conditions of a given arity.

/// <summary><see cref="IArgumentArity.Maximum"/> &gt; 1</summary>
public static bool AllowsMany(this IArgumentArity arity) => arity.Maximum > 1;

/// <summary><see cref="IArgumentArity.Maximum"/> &gt;= 1</summary>
public static bool AllowsOneOrMore(this IArgumentArity arity) => arity.Maximum >= 1;

/// <summary><see cref="IArgumentArity.Minimum"/> &gt; 0</summary>
public static bool RequiresAtLeastOne(this IArgumentArity arity) => arity.Minimum > 0;

/// <summary><see cref="IArgumentArity.Minimum"/> == 1 == <see cref="IArgumentArity.Maximum"/></summary>
public static bool RequiresExactlyOne(this IArgumentArity arity) => arity.Minimum == 1 && arity.Maximum == 1;

/// <summary>
/// <see cref="IArgumentArity.Maximum"/> == 0.
/// e.g. <see cref="ArgumentArity.Zero"/>
/// </summary>
public static bool RequiresNone(this IArgumentArity arity) => arity.Maximum == 0;

/// <summary>
/// <see cref="IArgumentArity.Minimum"/> == 0.
/// e.g. <see cref="ArgumentArity.Zero"/>, <see cref="ArgumentArity.ZeroOrOne"/>, <see cref="ArgumentArity.ZeroOrMore"/>
/// </summary>
public static bool AllowsNone(this IArgumentArity arity) => arity.Minimum == 0;

/// <summary>
/// <see cref="IArgumentArity.Maximum"/> == <see cref="ArgumentArity.Unlimited"/> (<see cref="int.MaxValue"/>).
/// e.g. <see cref="ArgumentArity.ZeroOrMore"/>, <see cref="ArgumentArity.OneOrMore"/>
/// </summary>
public static bool AllowsUnlimited(this IArgumentArity arity) => arity.Maximum == ArgumentArity.Unlimited;
snippet source | anchor

Default Values#

Middleware can set default values for arguments. Setting default values will not modify the arity.

Consider there are two categories of default values

  1. Statically defined by the application developer, such as described in the optional arguments section.
    • These can be inferred to mean the user is not required to enter a value.
    • This could also be defined by an attribute or some other static mechanism.
  2. Default values specified by the user to simplify use of the application. For example, pulling default values from Environment Variables and AppSettings.
    • The application requires a value but the user does not have to enter it because it is defaulted by a middleware component.

Any middleware updating the default values from statically defined sources should also update the arity. Use argument.Arity = ArgumentArity.Default(argument) to calculate a new arity for the argument.

Summary by definition type#

defined using defaults optional
parameter optional parameters when Nullable or parameter is optional
property property value immediately after initialization
-- struct when Nullable or default != default(T)
-- class when default != null

Arguments defined as parameters have consistent behavior for struct and class types.

Arguments defined as properties have have differing behavior between struct and class types.