Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

General Purpose Operating Parameters for Console Programs

0.00/5 (No votes)
2 Sep 2018 1  
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.

  1. 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.
  2. 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.
  3. Supports Validation by Rules: Validation rules and a validation engine are built into the class.
  4. 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.
  5. 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.

  1. Since the OperatingParametersCollection class implements the Singleton design pattern, you must call the static GetTheSingleInstance method on it to get a reference to the one and only instance. Creating the OperatingParametersCollection 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.
  2. Not only is OperatingParametersCollection a singleton, but it is a generic that has not one, but two generic types, both of which are Enums. Version 7,3 of the C# compiler is the first to support enumerations as constraints for generics.
  3. Properties.Settings.Default.Properties is a SettingsPropertyCollection that makes the application settings available for use by the OperatingParametersCollection. Using this property instead of Properties.Settings.Default decouples the SettingsPropertyCollection 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.
  4. 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.)
  5. ParameterSource.ApplicationSettings is a member of the ParameterSource enumeration, which I defined at namespace scope in the OperatingParameter 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 to True 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.
  6. The ParameterType enumeration drives the switch block in IsValueValid that implements the validation engine.
  7. The ParameterSource and ParameterType enumerations may be defined anywhere, but putting the definitions logically and physically close to the implementation of the IsValueValid method on OperatingParameterBase 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.

  1. 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.
  2. Enumeration type CmdLneArgsBasic.ArgMatching.CaseInsensitive instructs the CmdLneArgsBasic object to treat the parameters as case insensitive. Although the OperatingParametersCollection object is case sensitive, I usually adhere to the established Windows command line convention that parameter names are not.
  3. Since the CmdLneArgsBasic constructor parses the command line, the object is immediately ready to return parameter values.
  4. 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 the System.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 a CmdLneArgsBasic 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.

  1. Operating parameter values that are either initialized or overridden by command line arguments are tagged with the CommandLine member of the ParameterSource enumeration.
  2. 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
Table 1 lists the parameters for the demonstration program.
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 disable ExceptionLogger.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 custom ToString method. I use custom ToString 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.

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here