Skip to content

Argument Models#

Argument models provide a way to define arguments in a class.

This command to send an e-mail...

send --subject hi -a "myFile.txt" --body "just wanted you to review these files" bilal@bilal.com john@john.com

can be defined with a method as ...

public void SendEmail(
    [Option]string subject, 
    [Option]string body, 
    [Operand]string from, 
    [Operand]string to)
{

}

or with an argument model as ...

public class Email : IArgumentModel
{
    [Option]
    public string Subject {get;set;}

    [Option]
    public string Body {get;set;}

    [Operand]
    public string From {get;set;}

    [Operand]
    public string To {get;set;}
}

public void SendEmail(Email email)
{

}

Composition#

An IArgumentModel can be composed from other IArgumentModels allowing easy reuse of common arguments.

public class DryRun : IArgumentModel
{    
    [Option(LongName="dryrun")]
    public bool IsDryRun {get;set;}
}

public class SendEmailArgs : IArgumentModel
{    
    public DryRun DryRun {get;set;}

    public Email Email {get;set;}
}

public void SendEmail(SendEmailArgs args)
{

}

Benefits of argument models#

  • Common arguments can be extracted to models to enforce behaviors across commands. This ensures the same short name, long name, description, etc are consistent across all commands using this model.
  • A middleware could be created to cancel a UnitOfWork when a dry-run is requested.
  • FluentValidation framework can be used to validate the model.

Take DryRun for example. Ask 5 different developers to add a dryrun option and you'll end up with 5 different casings for it. Add it to an IArgumentModel and everyone can use and the commands will have a consistent argument.

When you have the same model, you can add middleware that can check for the existing of that model and perform logic based on that. Using the DryRun example, a UnitOfWork middleware could determine whether to commit or abort a transaction based on the value of the model.

Guaranteeing the order of operands#

Prior to version 3.2.0, operand 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.

This is not an issue with Option because options are named, not positional.

As of version 3.2.0, CommandDotNet can guarantee operands will maintain their position as defined within a class.

How to use#

The OperandAttribute now defines 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.

Set AppSettings.GuaranteeOperandOrderInArgumentModels = true to have CommandDotNet raise an exception when the order cannot be determined.

Warning

This will default to true in the next major release. If your app defines operands in argument models, set this to true now to avoid breaking changes.

Order cannot be determined when

  1. AppSettings.DefaultArgumentMode == ArgumentMode.Operand (the default) and the property is not attributed with [Operand]
  2. When a nested argument model containing an operand is not decorated with [OrderByPositionInClass]

When the guarantee is enabled, our SendEmail example above will fail with

Operand property must be attributed with OperandAttribute or OrderByPositionInClassAttribute to guarantee consistent order. Property: ExampleApp.SendEmailArgs.Email

We can fix by attributing the Email property like so...

public class SendEmailArgs : IArgumentModel
{    
    public DryRun DryRun {get;set;}

    [OrderByPositionInClass]
    public Email Email {get;set;}
}

Recommendation#

  • When possible, do not define operands in nested argument models.
  • Always attribute operands in properties with [Operand].