Skip to content

Data Types#

Arguments can be defined with any type that...

  • is a primitive type:
  • has a TypeConverter
  • contains a string constructor
  • has a public static Parse(string) method or public static Parse(string, {optional paremeters})

The constructor and static Parse method may contain additional optional parameters but must contain only a single required string parameter.

public class Username
{
    public string Value { get; }

    public Username(string value) => Value = value;
    public Username(string value, DateTime? validUntil = null) => Value = value;

    public static Username Parse(string value) => new(value);
    public static Username Parse(string value, DateTime? validUntil = null) => new(value, validUntil);
}
snippet source | anchor

Any of those constructors or Parse methods will allow conversion from string input, as shown in this example

public void Login(IConsole console, Username username, Password password)
{
    console.WriteLine($"u:{username.Value} p:{password}");
}
snippet source | anchor

$ myapp.exe Login roy rogers
u:roy p:*****
snippet source | anchor

Includes, but not limited to:

  • string,char,enum,bool
  • short,int,long,decimal,double
  • Guid,Uri,FileInfo,DirectoryInfo

Also supports Nullable<T> and IEnumerable<T> (T[], List<T>, etc.) where T can be converted from string.

Adding support for other types#

Options for supporting other types

  • If you control the type, consider adding a constructor with a single string parameter.
  • Create a TypeConverter for your type
  • Create a type descriptor

Type Descriptors#

Type descriptors are your best choice when you need

  • to override an existing TypeConverter
  • conditional logic based on argument metadata (custom attributes, etc)
  • the converter only for parsing parameters and not the business logic of your application, ruling out a TypeConvertor

Implement IArgumentTypeDescriptor or instantiate a DelegatedTypeDescriptor and register with AppSettings.ArgumentTypeDescriptors.Register(...).

If the type has a limited range of acceptable values, the descriptor should also implement IAllowedValuesTypeDescriptor. See EnumTypeDescriptor for an example.

public class EnumTypeDescriptor : 
    IArgumentTypeDescriptor,
    IAllowedValuesTypeDescriptor
{
    public bool CanSupport(Type type) => 
        type.IsEnum;

    public string GetDisplayName(IArgument argument) => 
        argument.TypeInfo.UnderlyingType.Name;

    public object ParseString(IArgument argument, string value) => 
        Enum.Parse(argument.TypeInfo.UnderlyingType, value);

    public IEnumerable<string> GetAllowedValues(IArgument argument) => 
        Enum.GetNames(argument.TypeInfo.UnderlyingType);
}
snippet source | anchor

Use DelegatedTypeDescriptor just to override the display text or factory method for the type.

new DelegatedTypeDescriptor<string>(Resources.A.Type_Text, v => v),
snippet source | anchor

See StringCtorTypeDescriptor and ComponentModelTypeDescriptor for examples to create your own.

public class ComponentModelTypeDescriptor : IArgumentTypeDescriptor
{
    public bool CanSupport(Type type)
    {
        var typeConverter = TypeDescriptor.GetConverter(type);
        return typeConverter.CanConvertFrom(typeof(string));
    }

    public string GetDisplayName(IArgument argument)
    {
        return argument.TypeInfo.UnderlyingType.Name;
    }

    public object? ParseString(IArgument argument, string value)
    {
        var typeConverter = argument.Arity.AllowsMany()
            ? TypeDescriptor.GetConverter(argument.TypeInfo.UnderlyingType)
            : TypeDescriptor.GetConverter(argument.TypeInfo.Type);
        return typeConverter.ConvertFrom(value)!;
    }
snippet source | anchor