General Purpose Operating Parameters for Console Programs





5.00/5 (2 votes)
This article demonstrates a class library that supports command line parameters with default values stored in application settings.
Introduction
This article demonstrates a class library, implemented as a Microsoft .NET DLL that targets the Microsoft .NET Framework, version 4.6.1 and the C# compiler, version 7.3. If you have any version of Visual Studio 2017, and your installation is up to date, you should have the correct framework and compiler. Otherwise, the project won't build, because it is configured to require the correct C# compiler version.
Background
The library demonstrated in this article and included in the accompanying demonstration archive represents the culmination of work that began over 10 years ago, and is the latest of many attempts to achieve the following goals in a way that is as close to "plug and play" as I can make it.
- Data Driven: The library is entirely data driven; parameters are defined in a text resource that is embedded in the entry assembly to which the parameters apply.
- Supports Localization: Display names shown on output may be defined as text strings that are read from the string reosurces embedded in the entry assembly or a satellite assembly that corresponds to a second user interface language. Display names are optional; the internal parameter name is substituted for omitted display names.
- Supports Validation by Rules: Validation rules and a validation engine are built into the class.
- Extensible Validation Engine: Since it is implemented as an abstract method, the validation engine can be replaced if you need rules not covered by the sample.
- Implemented as a Class Library: Implementing it as a class library simplifies incorporation into new applications; you need only to add a reference and create the embedded text file resource that defines your parameters.
Using the code
Begin by setting references to WizardWrx.Core.dll
and WizardWrx.OperatingParameterManager.dll
, and adding corresponding using
statements to Program.cs
.
using WizardWrx.Core;
using WizardWrx.OperatingParameterManager;
Next, create the OperatingParametersCollection
object.
OperatingParametersCollection<ParameterType , ParameterSource> operatingParametersColl = OperatingParametersCollection<ParameterType , ParameterSource>.GetTheSingleInstance (
Properties.Settings.Default.Properties ,
Properties.Resources.PARAMETER_DISPLAY_NAME_TEMPLATE ,
ParameterSource.ApplicationSettings );
The above statement requires a bit of explaining.
- Since the
OperatingParametersCollection
class implements the Singleton design pattern, you must call the staticGetTheSingleInstance
method on it to get a reference to the one and only instance. Creating theOperatingParametersCollection
as a singleton means that other routines defined in the same or other assemblies can use the parameters without littering your application with duplicate copies, not to mention saving the considerable overhead required to initialize them. - Not only is
OperatingParametersCollection
a singleton, but it is a generic that has not one, but two generic types, both of which areEnums
. Version 7,3 of the C# compiler is the first to support enumerations as constraints for generics. Properties.Settings.Default.Properties
is aSettingsPropertyCollection
that makes the application settings available for use by theOperatingParametersCollection
. Using this property instead ofProperties.Settings.Default
decouples theSettingsPropertyCollection
from the assembly's default namespace, so that the library can support the settings of any application, at the expense of the individual configuration properties, which the library cannot use without defeating its main objective.Properties.Resources.PARAMETER_DISPLAY_NAME_TEMPLATE
is a string that I put into the resource strings of the entry assembly, along with strings that represent names of two of the three parameters. (I omitted the third to demonstrate what happens when a display name is omitted. In the real world, I would define display name strings for all of them.)ParameterSource.ApplicationSettings
is a member of theParameterSource
enumeration, which I defined at namespace scope in theOperatingParameter
class source file, which is in the library. This value is stored with a parameter that has a default value defined in the application settings to indicate the source from whence the value came.HasDefaultValueInAppSettings
, a public Boolean property, is set toTrue
to indicate that the parameter has a default value. Another member of the enumeration,CommandLine
, is intended for marking parameters that get their values from named parameters in the command line.- The
ParameterType
enumeration drives the switch block inIsValueValid
that implements the validation engine. - The
ParameterSource
andParameterType
enumerations may be defined anywhere, but putting the definitions logically and physically close to the implementation of theIsValueValid
method onOperatingParameterBase
simplifes its definition a bit, while keeping the definitions of the two enumerations that take the place of the two generics together.
The next object to be defined is a WizardWrx.Core.CmdLneArgsBasic
object, which is defined as follows.
CmdLneArgsBasic cmdArgs = new CmdLneArgsBasic (
operatingParametersColl.GetParameterNames ( ) ,
CmdLneArgsBasic.ArgMatching.CaseInsensitive );
The constructor takes two arguments.
- Instance method
operatingParametersColl.GetParameterNames ( )
returns an array of strings, each of which represents the internal name of an operating parameter. This list is fed into the constructor, which uses it to initialize the collection that it uses to parse the command line and return parameter values as needed. - Enumeration type
CmdLneArgsBasic.ArgMatching.CaseInsensitive
instructs theCmdLneArgsBasic
object to treat the parameters as case insensitive. Although theOperatingParametersCollection
object is case sensitive, I usually adhere to the established Windows command line convention that parameter names are not. - Since the
CmdLneArgsBasic
constructor parses the command line, the object is immediately ready to return parameter values. - Astute readers will notice that the args array is conspicuously absent from the argument list that goes into the
CmdLneArgsBasic
constructor. This is possible because the constructor gets them from theSystem.Environment
singleton, which makes them available to both console and graphical mode programs. This feature makes it easy to write Windows programs that take command line arguments, of which I have many in my personal tool kit. Another benefit of this constructor design is that aCmdLneArgsBasic
constructor can be called from anywhere, since it ignores the args array.
The third preparatory step sets operating parameters that either have no default, or had their default values overrridden by command line parameters.
operatingParametersColl.SetFromCommandLineArguments (
cmdArgs ,
ParameterSource.CommandLine );
Method SetFromCommandLineArguments
feeds the command line arguments collection stored in the CmdLneArgsBasic
object into a routine that iterates the collection, matches names with defined parameters, and sets or updates their values.
- Operating parameter values that are either initialized or overridden by command line arguments are tagged with the
CommandLine
member of theParameterSource
enumeration. - If the value supplied by the command line parameter overrides a default that was set from the application configuration, the original value is preserved by copying it into the public
ParamSource
property.
The fourth, and last, preparatory step is to validate the parameters.
foreach ( string strParamName in operatingParametersColl.GetParameterNames ( ) )
{
OperatingParameter<ParameterType , ParameterSource> operatingParameter = operatingParametersColl.GetParameterByName ( strParamName );
bool fParamIsValid = operatingParameter.IsValueValid<ParameterType> ( );
Console.WriteLine (
Properties.Resources.MSG_VALIDATION_DETAIL , // Format Control String: Parameter {0} value of {1} is {2}.
operatingParameter.DisplayName , // Format Item 0: Parameter {0}
Utl.RenderStringValue (
operatingParameter.ParamValue ) , // Format Item 1: value of {1}
fParamIsValid ); // Format Item 2: is {2}
} // foreach ( string strParamName in operatingParametersColl.GetParameterNames ( ) )
A simple ForEach
loop iterates the array of parameter names returned by GetParameterNames
, then gets a reference to each OperatingParameter
object in turn by calling GetParameterByName
. The paramter value is validated by calling instance method IsValueValid<ParameterType> ( )
, which returns True
or False
, and sets the value of its ParamState
property to ParameterState.Validated
when it returns True
.
Since it applies to every object derived from it, the ParameterState enumeration is defined on the OperatingParameterBase
class, which belongs to the library.
Though the routine that sets a value could validate it, I chose to decouple the two functions, so that the parameters can be loaded and listed before any of them are validated. Separating the validation gives you the option of stopping after the first invalid parameter is identified, or completing the evaluation before stopping.
Strictly speaking, there is a fifth step, but it's a design time step, which is defining the parameters and creating the text file that drives the whole process.
InternalName | ParamType |
---|---|
OperatingParameter1 | ExistingDirectory |
OperatingParameter2 | ExistingFile |
OperatingParameter3 | NewFile |
The table is constructed as a TAB delimited text file that contains two columns, a label row, and a detail row for each parameter. Since this demonstration program defines 3 parameters, its table contains 3 rows. I usually lay out such files in Microsoft Excel worksheets, which conveniently convert to tab delimited text when copied onto the Windows Clipboard, from which the text is pasted into a Visual Studio text editor window to create the embedded resource. Since the parser knows how to handle Byte Order Marks, the UTF-8 BOM added by the Visual Studio text editor causes no interference.
Before the text file can be used, it must be embedded into the assembly as a text resource; this is accomplished by setting its Build Action to Embedded Resource. The build engine takes it from there.
Points of Interest
There is so much going on under the hood that I must either provide an exhaustive list, at the expense of making this article dreaffully long and boring, or hit a few high points, and invite you to explore on your own.
- The main routine reports a great deal of information, some of which is only incidentally related to the demonstration. However, I left it because a lot of it is interesting in its own right.
- The first executable statement gets a reference to a
ConsoleAppStateManager
singleton, which exposes many methods and properties that I use to assemble new character-mode programs quickly. The first of these is the very next statement, which displays a startup banner that includes the name and version of the program, along with the current local and UTC time. - The third executable statement sets an enumeration property that has a
FlagsAttribute
on it; you may wish to disableExceptionLogger.OutputOptions.EventLog
, which causes all exceptions to be written to the Windows Application Event Log. Think twice about doing so, though, because those exception reports have saved me much agony more times than I can count by guaranteeing that an exception report that flew past me before I could make a note of it was preserved in the event log. - The majority of the main routine executes inside a big Try/Catch block, which guaranteed that all exceptions were duly caught and logged in the Windows Event Log and on the Standard Error stream.
- The first block of output (
Console.WriteLine
) statement displays some of the interesting incidentals.- Namespace of Entry Point Routine lists the namespace of the class that defines the entry point routine, which happens to be the routine that is executing right now. Gathering this information was part of the research that went into developing this library, although I found a way to accmplish my objective without reference to it.
- Namespace of Entry Point Routine of the assembly that exports class demonstrates unequivocally that, so far as the Microsoft .NET Framework is concerned, a class library has no entry point.
Utl.ListInternalResourceNames
is a little library routine that lists the names of the resource collections defined in the executing assembly.AppSettingsForEntryAssembly
is a little class that I created to act as a wrapper around the application settings, to simplify parking references to them in other assemblies. The next two little blocks use its methods to retrrieve values indirectly, by way of the library assembly.- The last method called before the demonstration commences is
appSettingsForEntryAssembly.ListAllAppSettings
, which lists all settings.
- Static utility method
Utl.ListPropertiesPerDefaultToString
is a very flexible way to list the properties reported by a class through its customToString
method. I use customToString
methods to display essential properties in the Locals and Watch windows without expanding the object to display its properties. -
The catch block uses a cascading block of IF statements to set the status code when the exception message begins with one of a series of prefixes.
I make no apologies for the fact that there is a great deal more happening under the covers than space permits me to cover. However, every class, property, and method has XML help that shows up in IntelliSense, most of which is meticulously cross referenced. I've also included the PDB files for every custom assembly, nothing is obfuscated, and it's all free to use under a standard 3-clause BSD license. A few weeks after I published this article, I finally got the source code in WizardWrx .NET API, which contains most of the libraries used by this project updated. Yesterday, I updated the library to include a handful of new methods and a new string constant. Since this library is unaffected by the changes, all of which are new code, and I haven't rebuilt the library since first publication, the article archive is unchanged.
Road Map
The road map for the library is straightforward. I expect future versions to add types to the ParameterType
enumeration to support additional rules in an improved IsValueValid
routine. Among the additions under consideration are additional validations against the file system and evaluation against a basic regular expression.
History
Monday, 03 September 2018 is the release date of this first edition.
Monday, 08 October 2018, I added a link to the source code of the WizardWrx .NET API.