From 1e618e83b6accdf3fb9b83c826c4edede4f349b3 Mon Sep 17 00:00:00 2001 From: rdoe Date: Mon, 15 May 2017 08:23:36 +0200 Subject: [PATCH 01/11] added consoleApp for Testing refactored console app, _commandLibraries in static class now --- ConsoleApplicationBase.sln | 22 + ConsoleApplicationBase/App.config | 6 + ConsoleApplicationBase/CommandLibrary.cs | 89 ++++ .../Commands/DefaultCommands.cs | 42 ++ ConsoleApplicationBase/Commands/Users.cs | 38 ++ .../ConsoleApplicationBase.csproj | 64 +++ ConsoleApplicationBase/ConsoleCommand.cs | 76 ++++ ConsoleApplicationBase/ConsoleFormatting.cs | 18 + ConsoleApplicationBase/Models/SampleData.cs | 50 +++ ConsoleApplicationBase/Program.cs | 384 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 36 ++ README.md | 89 ++++ 12 files changed, 914 insertions(+) create mode 100644 ConsoleApplicationBase.sln create mode 100644 ConsoleApplicationBase/App.config create mode 100644 ConsoleApplicationBase/CommandLibrary.cs create mode 100644 ConsoleApplicationBase/Commands/DefaultCommands.cs create mode 100644 ConsoleApplicationBase/Commands/Users.cs create mode 100644 ConsoleApplicationBase/ConsoleApplicationBase.csproj create mode 100644 ConsoleApplicationBase/ConsoleCommand.cs create mode 100644 ConsoleApplicationBase/ConsoleFormatting.cs create mode 100644 ConsoleApplicationBase/Models/SampleData.cs create mode 100644 ConsoleApplicationBase/Program.cs create mode 100644 ConsoleApplicationBase/Properties/AssemblyInfo.cs create mode 100644 README.md diff --git a/ConsoleApplicationBase.sln b/ConsoleApplicationBase.sln new file mode 100644 index 0000000..2cedf79 --- /dev/null +++ b/ConsoleApplicationBase.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30723.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApplicationBase", "ConsoleApplicationBase\ConsoleApplicationBase.csproj", "{D21CC334-9E7D-4A29-B6F9-5120351EC703}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/ConsoleApplicationBase/App.config b/ConsoleApplicationBase/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/ConsoleApplicationBase/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ConsoleApplicationBase/CommandLibrary.cs b/ConsoleApplicationBase/CommandLibrary.cs new file mode 100644 index 0000000..b6e7d19 --- /dev/null +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase +{ + public static class CommandLibrary + { + const string _commandNamespace = "ConsoleApplicationBase.Commands"; + public static Dictionary>> Content; + + + static CommandLibrary() + { + Content = new Dictionary>>(); + initialize(); + } + + + static void initialize() + { + // Use reflection to load all of the classes in the Commands namespace: + //var q = from t in Assembly.GetExecutingAssembly().GetTypes() + // where t.IsClass && t.Namespace == _commandNamespace + // select t; + //var commandClasses = q.ToList(); + + var commandClasses = listMatchingAssemblyTypes(Assembly.GetExecutingAssembly()); + + //foreach (var commandClass in commandClasses) + //{ + // // Load the method info from each class into a dictionary: + // var methods = commandClass.GetMethods(BindingFlags.Static | BindingFlags.Public); + // var methodDictionary = new Dictionary>(); + // foreach (var method in methods) + // { + // string commandName = method.Name; + // methodDictionary.Add(commandName, method.GetParameters()); + // } + // // Add the dictionary of methods for the current class into a dictionary of command classes: + // Content.Add(commandClass.Name, methodDictionary); + //} + + addCommands(commandClasses); + } + + /// + /// Returns a list of types that are classes + /// and defined inside namespace specified + /// in _commandNamespace + /// + /// The Assembly to get the classes from + /// + static List listMatchingAssemblyTypes(Assembly assmbl) + { + var q = from t in assmbl.GetTypes() + where t.IsClass && t.Namespace == _commandNamespace + select t; + + return q.ToList(); + } + + + /// + /// Adds public static methods from a list of classes + /// to Content + /// + /// + static void addCommands(List cmdClasses) + { + foreach (var commandClass in cmdClasses) + { + var methods = commandClass.GetMethods(BindingFlags.Static | BindingFlags.Public); + var methodDictionary = new Dictionary>(); + foreach (var method in methods) + { + string commandName = method.Name; + methodDictionary.Add(commandName, method.GetParameters()); + } + // Add the dictionary of methods for the current class into a dictionary of command classes: + Content.Add(commandClass.Name, methodDictionary); + } + } + + } +} diff --git a/ConsoleApplicationBase/Commands/DefaultCommands.cs b/ConsoleApplicationBase/Commands/DefaultCommands.cs new file mode 100644 index 0000000..6c79062 --- /dev/null +++ b/ConsoleApplicationBase/Commands/DefaultCommands.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +// All console commands must be in the sub-namespace Commands: +namespace ConsoleApplicationBase.Commands +{ + // Must be a public static class: + public static class DefaultCommands + { + // Methods used as console commands must be public and must return a string + + public static string DoSomething(int id, string data) + { + return string.Format(ConsoleFormatting.Indent(2) + + "I did something to the record Id {0} and saved the data '{1}'", id, data); + } + + + public static string DoSomethingElse(DateTime date) + { + return string.Format(ConsoleFormatting.Indent(2) + "I did something else on {0}", date); + } + + + public static string DoSomethingOptional(int id, string data = "No Data Provided") + { + var result = string.Format(ConsoleFormatting.Indent(2) + + "I did something to the record Id {0} and saved the data {1}", id, data); + + if(data == "No Data Provided") + { + result = string.Format(ConsoleFormatting.Indent(2) + + "I did something to the record Id {0} but the optinal parameter " + + "was not provided, so I saved the value '{1}'", id, data); + } + return result; + } + } +} diff --git a/ConsoleApplicationBase/Commands/Users.cs b/ConsoleApplicationBase/Commands/Users.cs new file mode 100644 index 0000000..9ff3bfb --- /dev/null +++ b/ConsoleApplicationBase/Commands/Users.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ConsoleApplicationBase.Models; + +namespace ConsoleApplicationBase.Commands +{ + public static class Users + { + public static string Create(string firstName, string lastName) + { + Nullable maxId = (from u in SampleData.Users + select u.Id).Max(); + int newId = 0; + if(maxId.HasValue) + { + newId = maxId.Value + 1; + } + + var newUser = new User { Id = newId, FirstName = firstName, LastName = lastName }; + SampleData.Users.Add(newUser); + return ""; + } + + + public static string Get() + { + var sb = new StringBuilder(); + foreach(var user in SampleData.Users) + { + sb.AppendLine(ConsoleFormatting.Indent(2) + string.Format("Id:{0} {1} {2}", user.Id, user.FirstName, user.LastName)); + } + return sb.ToString(); + } + } +} diff --git a/ConsoleApplicationBase/ConsoleApplicationBase.csproj b/ConsoleApplicationBase/ConsoleApplicationBase.csproj new file mode 100644 index 0000000..3e93f85 --- /dev/null +++ b/ConsoleApplicationBase/ConsoleApplicationBase.csproj @@ -0,0 +1,64 @@ + + + + + Debug + AnyCPU + {D21CC334-9E7D-4A29-B6F9-5120351EC703} + Exe + Properties + ConsoleApplicationBase + ConsoleApplicationBase + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ConsoleApplicationBase/ConsoleCommand.cs b/ConsoleApplicationBase/ConsoleCommand.cs new file mode 100644 index 0000000..09bf122 --- /dev/null +++ b/ConsoleApplicationBase/ConsoleCommand.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using ConsoleApplicationBase.Commands; + +namespace ConsoleApplicationBase +{ + public class ConsoleCommand + { + public ConsoleCommand(string input) + { + // Ugly regex to split string on spaces, but preserve quoted text intact: + var stringArray = + Regex.Split(input, "(?<=^[^\"]*(?:\"[^\"]*\"[^\"]*)*) (?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"); + + _arguments = new List(); + for (int i = 0; i < stringArray.Length; i++) + { + // The first element is always the command: + if (i == 0) + { + this.Name = stringArray[i]; + + // Set the default: + this.LibraryClassName = "DefaultCommands"; + string[] s = stringArray[0].Split('.'); + if (s.Length == 2) + { + this.LibraryClassName = s[0]; + this.Name = s[1]; + } + } + else + { + var inputArgument = stringArray[i]; + + // Assume that most of the time, the input argument is NOT quoted text: + string argument = inputArgument; + + // Is the argument a quoted text string? + var regex = new Regex("\"(.*?)\"", RegexOptions.Singleline); + var match = regex.Match(inputArgument); + + // If it IS quoted, there will be at least one capture: + if (match.Captures.Count > 0) + { + // Get the unquoted text from within the qoutes: + var captureQuotedText = new Regex("[^\"]*[^\"]"); + var quoted = captureQuotedText.Match(match.Captures[0].Value); + + // The argument should include all text from between the quotes + // as a single string: + argument = quoted.Captures[0].Value; + } + _arguments.Add(argument); + } + } + } + + public string Name { get; set; } + public string LibraryClassName { get; set; } + + private List _arguments; + public IEnumerable Arguments + { + get + { + return _arguments; + } + } + } + +} diff --git a/ConsoleApplicationBase/ConsoleFormatting.cs b/ConsoleApplicationBase/ConsoleFormatting.cs new file mode 100644 index 0000000..708431e --- /dev/null +++ b/ConsoleApplicationBase/ConsoleFormatting.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +// code was found at the C# Examples Site: http://www.csharp-examples.net/indent-string-with-spaces/ + +namespace ConsoleApplicationBase +{ + public class ConsoleFormatting + { + public static string Indent(int count) + { + return "".PadLeft(count); + } + } +} diff --git a/ConsoleApplicationBase/Models/SampleData.cs b/ConsoleApplicationBase/Models/SampleData.cs new file mode 100644 index 0000000..b5c45a1 --- /dev/null +++ b/ConsoleApplicationBase/Models/SampleData.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase.Models +{ + // We'll use this for our examples: + public class User + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + } + + + public static class SampleData + { + private static List _userData; + public static List Users + { + get + { + // List will be initialized with data the first time the + // static property is accessed: + if(_userData == null) + { + _userData = CreateInitialUsers(); + } + return _userData; + } + } + + + // Some test data: + static List CreateInitialUsers() + { + var initialUsers = new List() + { + new User { Id = 1, FirstName = "John", LastName = "Lennon" }, + new User { Id = 2, FirstName = "Paul", LastName = "McCartney" }, + new User { Id = 3, FirstName = "George", LastName = "Harrison" }, + new User { Id = 4, FirstName = "Ringo", LastName = "Starr" }, + }; + return initialUsers; + + } + } +} diff --git a/ConsoleApplicationBase/Program.cs b/ConsoleApplicationBase/Program.cs new file mode 100644 index 0000000..858f91c --- /dev/null +++ b/ConsoleApplicationBase/Program.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Reflection; + +namespace ConsoleApplicationBase +{ + class Program + { + const string _commandNamespace = "ConsoleApplicationBase.Commands"; + static Dictionary>> _commandLibraries; + + static void Main(string[] args) + { + Console.Title = typeof(Program).Name; + + //_commandLibraries = CommandLibrary.Content; + + // Any static classes containing commands for use from the + // console are located in the Commands namespace. Load + // references to each type in that namespace via reflection: + //_commandLibraries = new Dictionary>>(); + + // Use reflection to load all of the classes in the Commands namespace: + //var q = from t in Assembly.GetExecutingAssembly().GetTypes() + // where t.IsClass && t.Namespace == _commandNamespace + // select t; + //var commandClasses = q.ToList(); + + //foreach (var commandClass in commandClasses) + //{ + // // Load the method info from each class into a dictionary: + // var methods = commandClass.GetMethods(BindingFlags.Static | BindingFlags.Public); + // var methodDictionary = new Dictionary>(); + // foreach (var method in methods) + // { + // string commandName = method.Name; + // methodDictionary.Add(commandName, method.GetParameters()); + // } + // // Add the dictionary of methods for the current class into a dictionary of command classes: + // _commandLibraries.Add(commandClass.Name, methodDictionary); + //} + Run(); + } + + + static void Run() + { + while (true) + { + var consoleInput = ReadFromConsole(); + if (string.IsNullOrWhiteSpace(consoleInput)) continue; + + try + { + // Create a ConsoleCommand instance: + var cmd = new ConsoleCommand(consoleInput); + + // Execute the command: + string result = Execute(cmd); + + // Write out the result: + WriteToConsole(result); + } + catch (Exception ex) + { + // OOPS! Something went wrong - Write out the problem: + WriteToConsole(ex.Message); + } + } + } + + + static string Execute(ConsoleCommand command) + { + // Validate the class name and command name: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + string badCommandMessage = string.Format("" + + "Unrecognized command \'{0}.{1}\'. " + + "Please type a valid command.", + command.LibraryClassName, command.Name); + + // Validate the command name: + if (!CommandLibrary.Content.ContainsKey(command.LibraryClassName)) + { + return badCommandMessage; + } + var methodDictionary = CommandLibrary.Content[command.LibraryClassName]; + if (!methodDictionary.ContainsKey(command.Name)) + { + return badCommandMessage; + } + + // Make sure the corret number of required arguments are provided: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + var methodParameterValueList = new List(); + IEnumerable paramInfoList = methodDictionary[command.Name].ToList(); + + // Validate proper # of required arguments provided. Some may be optional: + var requiredParams = paramInfoList.Where(p => p.IsOptional == false); + var optionalParams = paramInfoList.Where(p => p.IsOptional == true); + int requiredCount = requiredParams.Count(); + int optionalCount = optionalParams.Count(); + int providedCount = command.Arguments.Count(); + + if (requiredCount > providedCount) + { + return string.Format( + "Missing required argument. {0} required, {1} optional, {2} provided", + requiredCount, optionalCount, providedCount); + } + + // Make sure all arguments are coerced to the proper type, and that there is a + // value for every emthod parameter. The InvokeMember method fails if the number + // of arguments provided does not match the number of parameters in the + // method signature, even if some are optional: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + if (paramInfoList.Count() > 0) + { + // Populate the list with default values: + foreach (var param in paramInfoList) + { + // This will either add a null object reference if the param is required + // by the method, or will set a default value for optional parameters. in + // any case, there will be a value or null for each method argument + // in the method signature: + methodParameterValueList.Add(param.DefaultValue); + } + + // Now walk through all the arguments passed from the console and assign + // accordingly. Any optional arguments not provided have already been set to + // the default specified by the method signature: + for (int i = 0; i < command.Arguments.Count(); i++) + { + var methodParam = paramInfoList.ElementAt(i); + var typeRequired = methodParam.ParameterType; + object value = null; + try + { + // Coming from the Console, all of our arguments are passed in as + // strings. Coerce to the type to match the method paramter: + value = CoerceArgument(typeRequired, command.Arguments.ElementAt(i)); + methodParameterValueList.RemoveAt(i); + methodParameterValueList.Insert(i, value); + } + catch (ArgumentException ex) + { + string argumentName = methodParam.Name; + string argumentTypeName = typeRequired.Name; + string message = + string.Format("" + + "The value passed for argument '{0}' cannot be parsed to type '{1}'", + argumentName, argumentTypeName); + throw new ArgumentException(message); + } + } + } + + // Set up to invoke the method using reflection: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + Assembly current = typeof(Program).Assembly; + + // Need the full Namespace for this: + Type commandLibaryClass = + current.GetType(_commandNamespace + "." + command.LibraryClassName); + + object[] inputArgs = null; + if (methodParameterValueList.Count > 0) + { + inputArgs = methodParameterValueList.ToArray(); + } + var typeInfo = commandLibaryClass; + + // This will throw if the number of arguments provided does not match the number + // required by the method signature, even if some are optional: + try + { + var result = typeInfo.InvokeMember( + command.Name, + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, null, inputArgs); + return result.ToString(); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + } + + + static object CoerceArgument(Type requiredType, string inputValue) + { + var requiredTypeCode = Type.GetTypeCode(requiredType); + string exceptionMessage = + string.Format("Cannnot coerce the input argument {0} to required type {1}", + inputValue, requiredType.Name); + + object result = null; + switch (requiredTypeCode) + { + case TypeCode.String: + result = inputValue; + break; + + case TypeCode.Int16: + short number16; + if (Int16.TryParse(inputValue, out number16)) + { + result = number16; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Int32: + int number32; + if (Int32.TryParse(inputValue, out number32)) + { + result = number32; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Int64: + long number64; + if (Int64.TryParse(inputValue, out number64)) + { + result = number64; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Boolean: + bool trueFalse; + if (bool.TryParse(inputValue, out trueFalse)) + { + result = trueFalse; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Byte: + byte byteValue; + if (byte.TryParse(inputValue, out byteValue)) + { + result = byteValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Char: + char charValue; + if (char.TryParse(inputValue, out charValue)) + { + result = charValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.DateTime: + DateTime dateValue; + if (DateTime.TryParse(inputValue, out dateValue)) + { + result = dateValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Decimal: + Decimal decimalValue; + if (Decimal.TryParse(inputValue, out decimalValue)) + { + result = decimalValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Double: + Double doubleValue; + if (Double.TryParse(inputValue, out doubleValue)) + { + result = doubleValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Single: + Single singleValue; + if (Single.TryParse(inputValue, out singleValue)) + { + result = singleValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt16: + UInt16 uInt16Value; + if (UInt16.TryParse(inputValue, out uInt16Value)) + { + result = uInt16Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt32: + UInt32 uInt32Value; + if (UInt32.TryParse(inputValue, out uInt32Value)) + { + result = uInt32Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt64: + UInt64 uInt64Value; + if (UInt64.TryParse(inputValue, out uInt64Value)) + { + result = uInt64Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + default: + throw new ArgumentException(exceptionMessage); + } + return result; + } + + + public static void WriteToConsole(string message = "") + { + if(message.Length > 0) + { + Console.WriteLine(message); + } + } + + + const string _readPrompt = "console> "; + public static string ReadFromConsole(string promptMessage = "") + { + // Show a prompt, and get input: + Console.Write(_readPrompt + promptMessage); + return Console.ReadLine(); + } + } +} diff --git a/ConsoleApplicationBase/Properties/AssemblyInfo.cs b/ConsoleApplicationBase/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..172a196 --- /dev/null +++ b/ConsoleApplicationBase/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ConsoleApplicationBase")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ConsoleApplicationBase")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b776aaea-d3a1-435e-b7cf-623792acc812")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.md b/README.md new file mode 100644 index 0000000..89b515a --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +A Useful, Interactive, and Extensible .NET Console Application Template for Development and Testing +=================================================================================================== + +This application can serve as a template of framework into which you can easily plug in commands related tothe code you want to demo, excercise, or which forms the business layer of an actual Console application. The "Interactive" part of this Console is already in place - all you need to do is plug command classes and methods into the Commands namespace and you're ready to go. + +The goal here was not to emulate a fully-functioning Shell or terminal. I wanted to be able to: + +* Run the program, be greeted with a prompt (customizable, in this case), and then enter commands corresponding to various methods defined in a specific area of the application. +* Receive feedback (where appropriate), error messages, and such +* Easily add/remove commands +* Generally be able to quickly put together a functioning console application, with predictable and familiar interactive behavior, without re-inventing the wheel every time. + +For more detailed information about how this works, see the Blog post at: [C#: Building a Useful, Extensible .NET Console Application Template for Development and Testing](http://typecastexception.com/post/2014/09/07/C-Building-a-Useful-Extensible-NET-Console-Application-Template-for-Development-and-Testing.aspx) + +Assumptions +----------- + +As it is currently configured, this application makes a few assumptions about architecture. You can easily adapt things to suit your purposes, but out-of-the-box, the following "rules" are assumed: + +* Methods representing Console commands will be defined as `public static` methods which always return a `string`, and are defined on `public static` classes. +* Classes containing methods representing Console commands will be located in the `Commands` namespace, and in the *Commands* folder. +* There will always be a static class named `DefaultCommands` from which methods may be invoked from the Console directly by name. For many console projects, this will likely be sufficient. +* Commands defined on classes other than DefaultCommands will be invoked from the console using the familiar dot syntax: ClassName.CommandName. + +Defining Commands +----------------- + +If you were to define the following (trival example-style) commands in your `DefaultCommands` class, you will be able to execute these from the Console when you run the application. The DefaultCommands class must be present in the project, and must be within the `Commands` namespace (note that the methods must be `static` in order to be available to the console as commands, and the project assumes a `string` return type), + +```csharp +public static string DoSomething(int id, string data) +{ + return string.Format(ConsoleFormatting.Indent(2) + + "I did something to the record Id {0} and saved the data '{1}'", id, data); +} + + +public static string DoSomethingElse(DateTime date) +{ + return string.Format(ConsoleFormatting.Indent(2) + "I did something else on {0}", date); +} + + +public static string DoSomethingOptional(int id, string data = "No Data Provided") +{ + var result = string.Format(ConsoleFormatting.Indent(2) + + "I did something to the record Id {0} and saved the data {1}", id, data); + + if(data == "No Data Provided") + { + result = string.Format(ConsoleFormatting.Indent(2) + + "I did something to the record Id {0} but the optinal parameter " + + "was not provided, so I saved the value '{1}'", id, data); + } + return result; +} +``` + +Executing Commands +------------------ + +The commands above can be executed when you run the application with the following syntax: + +Execute the `DoSomething` command: + +``` +console> DoSomething 55 "My Data" +``` + +Execute the `DoSomethingElse` command: + +``` +console> DoSomethingElse 7/4/2014 +``` +The console recognizes and deals with optional method parameters. + +Execute the `DoSomethingOptional` command inluding optional parameters: + +``` +console> DoSomethingOptional 212 "This is my optional data" +``` + +OR, you could omit the last argument, and the default value defined on the method will be used: + +``` +console> DoSomethingOptional 212 +``` + +I'm happy to take pull requests, suggestions, and ideas. From 7bc416b2a12d9c1378e680f38fbed3d4b226aa65 Mon Sep 17 00:00:00 2001 From: rdoe Date: Mon, 15 May 2017 16:06:13 +0200 Subject: [PATCH 02/11] added new Default command to add external assemblies TODO: Commands can only be invoked once, don't know reason yet --- ConsoleApplicationBase.sln | 10 +- ConsoleApplicationBase/CommandClassInfo.cs | 22 ++ ConsoleApplicationBase/CommandHandler.cs | 308 +++++++++++++++++ ConsoleApplicationBase/CommandLibrary.cs | 58 ++-- .../Commands/DefaultCommands.cs | 7 + .../ConsoleApplicationBase.csproj | 2 + ConsoleApplicationBase/Program.cs | 326 +----------------- TestLib/Properties/AssemblyInfo.cs | 36 ++ TestLib/TestClass.cs | 21 ++ TestLib/TestLib.csproj | 47 +++ 10 files changed, 483 insertions(+), 354 deletions(-) create mode 100644 ConsoleApplicationBase/CommandClassInfo.cs create mode 100644 ConsoleApplicationBase/CommandHandler.cs create mode 100644 TestLib/Properties/AssemblyInfo.cs create mode 100644 TestLib/TestClass.cs create mode 100644 TestLib/TestLib.csproj diff --git a/ConsoleApplicationBase.sln b/ConsoleApplicationBase.sln index 2cedf79..1763b1a 100644 --- a/ConsoleApplicationBase.sln +++ b/ConsoleApplicationBase.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30723.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.26403.7 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApplicationBase", "ConsoleApplicationBase\ConsoleApplicationBase.csproj", "{D21CC334-9E7D-4A29-B6F9-5120351EC703}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestLib", "TestLib\TestLib.csproj", "{2304B2F5-905D-46CD-9BC1-F10973FBEDC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Debug|Any CPU.Build.0 = Debug|Any CPU {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Release|Any CPU.ActiveCfg = Release|Any CPU {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Release|Any CPU.Build.0 = Release|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ConsoleApplicationBase/CommandClassInfo.cs b/ConsoleApplicationBase/CommandClassInfo.cs new file mode 100644 index 0000000..b92a485 --- /dev/null +++ b/ConsoleApplicationBase/CommandClassInfo.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase +{ + public class CommandClassInfo + { + public Assembly OwningAssembly { get;} + public Dictionary> MethodDictionary { get; } + + public CommandClassInfo(Assembly owningAssembly, Dictionary> methodDict) + { + this.OwningAssembly = owningAssembly; + this.MethodDictionary = methodDict; + } + + } +} diff --git a/ConsoleApplicationBase/CommandHandler.cs b/ConsoleApplicationBase/CommandHandler.cs new file mode 100644 index 0000000..4c0fdbc --- /dev/null +++ b/ConsoleApplicationBase/CommandHandler.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase +{ + public class CommandHandler + { + static List methodParameterValueList = new List(); + static IEnumerable paramInfoList; + + public static string Execute(ConsoleCommand command2Execute) + { + // Validate the class name and command name: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + string badCommandMessage = string.Format("" + + "Unrecognized command \'{0}.{1}\'. " + + "Please type a valid command.", + command2Execute.LibraryClassName, command2Execute.Name); + + // Validate the command name: + if (!CommandLibrary.Content.ContainsKey(command2Execute.LibraryClassName)) + { + return badCommandMessage; + } + //var methodDictionary = CommandLibrary.Content[command2Execute.LibraryClassName]; + //if (!methodDictionary.ContainsKey(command2Execute.Name)) + //{ + // return badCommandMessage; + //} + var commandClassInfo = CommandLibrary.Content[command2Execute.LibraryClassName]; + if (!commandClassInfo.MethodDictionary.ContainsKey(command2Execute.Name)) + { + return badCommandMessage; + } + + // Make sure the corret number of required arguments are provided: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + paramInfoList = commandClassInfo.MethodDictionary[command2Execute.Name].ToList(); + + // Validate proper # of required arguments provided. Some may be optional: + var requiredParams = paramInfoList.Where(p => p.IsOptional == false); + var optionalParams = paramInfoList.Where(p => p.IsOptional == true); + int requiredCount = requiredParams.Count(); + int optionalCount = optionalParams.Count(); + int providedCount = command2Execute.Arguments.Count(); + + if (requiredCount > providedCount) + { + return string.Format( + "Missing required argument. {0} required, {1} optional, {2} provided", + requiredCount, optionalCount, providedCount); + } + + // Make sure all arguments are coerced to the proper type, and that there is a + // value for every emthod parameter. The InvokeMember method fails if the number + // of arguments provided does not match the number of parameters in the + // method signature, even if some are optional: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + if (paramInfoList.Count() > 0) + { + // Populate the list with default values: + foreach (var param in paramInfoList) + { + // This will either add a null object reference if the param is required + // by the method, or will set a default value for optional parameters. in + // any case, there will be a value or null for each method argument + // in the method signature: + methodParameterValueList.Add(param.DefaultValue); + } + + // Now walk through all the arguments passed from the console and assign + // accordingly. Any optional arguments not provided have already been set to + // the default specified by the method signature: + for (int i = 0; i < command2Execute.Arguments.Count(); i++) + { + var methodParam = paramInfoList.ElementAt(i); + var typeRequired = methodParam.ParameterType; + object value = null; + try + { + // Coming from the Console, all of our arguments are passed in as + // strings. Coerce to the type to match the method paramter: + value = CoerceArgument(typeRequired, command2Execute.Arguments.ElementAt(i)); + methodParameterValueList.RemoveAt(i); + methodParameterValueList.Insert(i, value); + } + catch (ArgumentException ex) + { + string argumentName = methodParam.Name; + string argumentTypeName = typeRequired.Name; + string message = + string.Format("" + + "The value passed for argument '{0}' cannot be parsed to type '{1}'", + argumentName, argumentTypeName); + throw new ArgumentException(message); + } + } + } + + // Set up to invoke the method using reflection: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + Assembly current = commandClassInfo.OwningAssembly; //typeof(Program).Assembly; + + // Need the full Namespace for this: + Type commandLibaryClass = + current.GetType(CommandLibrary.CommandNamespace + "." + command2Execute.LibraryClassName); + + object[] inputArgs = null; + if (methodParameterValueList.Count > 0) + { + inputArgs = methodParameterValueList.ToArray(); + } + var typeInfo = commandLibaryClass; + + // This will throw if the number of arguments provided does not match the number + // required by the method signature, even if some are optional: + try + { + var result = typeInfo.InvokeMember( + command2Execute.Name, + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, null, inputArgs); + return result.ToString(); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + } + + static object CoerceArgument(Type requiredType, string inputValue) + { + var requiredTypeCode = Type.GetTypeCode(requiredType); + string exceptionMessage = + string.Format("Cannnot coerce the input argument {0} to required type {1}", + inputValue, requiredType.Name); + + object result = null; + switch (requiredTypeCode) + { + case TypeCode.String: + result = inputValue; + break; + + case TypeCode.Int16: + short number16; + if (Int16.TryParse(inputValue, out number16)) + { + result = number16; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Int32: + int number32; + if (Int32.TryParse(inputValue, out number32)) + { + result = number32; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Int64: + long number64; + if (Int64.TryParse(inputValue, out number64)) + { + result = number64; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Boolean: + bool trueFalse; + if (bool.TryParse(inputValue, out trueFalse)) + { + result = trueFalse; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Byte: + byte byteValue; + if (byte.TryParse(inputValue, out byteValue)) + { + result = byteValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Char: + char charValue; + if (char.TryParse(inputValue, out charValue)) + { + result = charValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.DateTime: + DateTime dateValue; + if (DateTime.TryParse(inputValue, out dateValue)) + { + result = dateValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Decimal: + Decimal decimalValue; + if (Decimal.TryParse(inputValue, out decimalValue)) + { + result = decimalValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Double: + Double doubleValue; + if (Double.TryParse(inputValue, out doubleValue)) + { + result = doubleValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Single: + Single singleValue; + if (Single.TryParse(inputValue, out singleValue)) + { + result = singleValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt16: + UInt16 uInt16Value; + if (UInt16.TryParse(inputValue, out uInt16Value)) + { + result = uInt16Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt32: + UInt32 uInt32Value; + if (UInt32.TryParse(inputValue, out uInt32Value)) + { + result = uInt32Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt64: + UInt64 uInt64Value; + if (UInt64.TryParse(inputValue, out uInt64Value)) + { + result = uInt64Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + default: + throw new ArgumentException(exceptionMessage); + } + return result; + } + } +} diff --git a/ConsoleApplicationBase/CommandLibrary.cs b/ConsoleApplicationBase/CommandLibrary.cs index b6e7d19..20d3603 100644 --- a/ConsoleApplicationBase/CommandLibrary.cs +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text; @@ -9,42 +10,25 @@ namespace ConsoleApplicationBase { public static class CommandLibrary { - const string _commandNamespace = "ConsoleApplicationBase.Commands"; - public static Dictionary>> Content; + public static readonly string CommandNamespace = "ConsoleApplicationBase.Commands"; + public static Dictionary Content; static CommandLibrary() { - Content = new Dictionary>>(); + Content = new Dictionary(); initialize(); } static void initialize() { - // Use reflection to load all of the classes in the Commands namespace: - //var q = from t in Assembly.GetExecutingAssembly().GetTypes() - // where t.IsClass && t.Namespace == _commandNamespace - // select t; - //var commandClasses = q.ToList(); - - var commandClasses = listMatchingAssemblyTypes(Assembly.GetExecutingAssembly()); - - //foreach (var commandClass in commandClasses) - //{ - // // Load the method info from each class into a dictionary: - // var methods = commandClass.GetMethods(BindingFlags.Static | BindingFlags.Public); - // var methodDictionary = new Dictionary>(); - // foreach (var method in methods) - // { - // string commandName = method.Name; - // methodDictionary.Add(commandName, method.GetParameters()); - // } - // // Add the dictionary of methods for the current class into a dictionary of command classes: - // Content.Add(commandClass.Name, methodDictionary); - //} + var assembly = Assembly.GetExecutingAssembly(); + var commandClasses = listMatchingAssemblyTypes(assembly); + + addCommands(assembly, commandClasses); - addCommands(commandClasses); + //addCommandsFromAssemblyFile("C:\\Users\\rdoe\\Dropbox\\TEX\\Masterarbeit\\src\\consoleApp\\ConsoleApplicationBase\\TestLib\\bin\\Debug\\TestLib.dll"); } /// @@ -57,19 +41,18 @@ static void initialize() static List listMatchingAssemblyTypes(Assembly assmbl) { var q = from t in assmbl.GetTypes() - where t.IsClass && t.Namespace == _commandNamespace + where t.IsClass && t.Namespace == CommandNamespace select t; return q.ToList(); } - /// /// Adds public static methods from a list of classes - /// to Content /// + /// /// - static void addCommands(List cmdClasses) + static void addCommands(Assembly owningAssembly,List cmdClasses) { foreach (var commandClass in cmdClasses) { @@ -81,7 +64,22 @@ static void addCommands(List cmdClasses) methodDictionary.Add(commandName, method.GetParameters()); } // Add the dictionary of methods for the current class into a dictionary of command classes: - Content.Add(commandClass.Name, methodDictionary); + CommandLibrary.Content.Add(commandClass.Name, new CommandClassInfo(owningAssembly, methodDictionary)); + } + } + + /// + /// adds suitable commands from a specified assembly file + /// + /// + public static void addCommandsFromAssemblyFile(string assemblyFile) + { + if (File.Exists(assemblyFile)) + { + var extAssembly = Assembly.LoadFrom(assemblyFile); + var extCommandCLasses = listMatchingAssemblyTypes(extAssembly); + + addCommands(extAssembly, extCommandCLasses); } } diff --git a/ConsoleApplicationBase/Commands/DefaultCommands.cs b/ConsoleApplicationBase/Commands/DefaultCommands.cs index 6c79062..f99b6d8 100644 --- a/ConsoleApplicationBase/Commands/DefaultCommands.cs +++ b/ConsoleApplicationBase/Commands/DefaultCommands.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -38,5 +39,11 @@ public static string DoSomethingOptional(int id, string data = "No Data Provided } return result; } + + public static string AddExternalAssembly(string assemblyFile) + { + CommandLibrary.addCommandsFromAssemblyFile(assemblyFile); + return "added Assembly File : " + Path.GetFileName(assemblyFile); + } } } diff --git a/ConsoleApplicationBase/ConsoleApplicationBase.csproj b/ConsoleApplicationBase/ConsoleApplicationBase.csproj index 3e93f85..f100082 100644 --- a/ConsoleApplicationBase/ConsoleApplicationBase.csproj +++ b/ConsoleApplicationBase/ConsoleApplicationBase.csproj @@ -41,6 +41,8 @@ + + diff --git a/ConsoleApplicationBase/Program.cs b/ConsoleApplicationBase/Program.cs index 858f91c..eb1dc34 100644 --- a/ConsoleApplicationBase/Program.cs +++ b/ConsoleApplicationBase/Program.cs @@ -9,39 +9,13 @@ namespace ConsoleApplicationBase { class Program { - const string _commandNamespace = "ConsoleApplicationBase.Commands"; + //const string _commandNamespace = "ConsoleApplicationBase.Commands"; static Dictionary>> _commandLibraries; static void Main(string[] args) { Console.Title = typeof(Program).Name; - //_commandLibraries = CommandLibrary.Content; - - // Any static classes containing commands for use from the - // console are located in the Commands namespace. Load - // references to each type in that namespace via reflection: - //_commandLibraries = new Dictionary>>(); - - // Use reflection to load all of the classes in the Commands namespace: - //var q = from t in Assembly.GetExecutingAssembly().GetTypes() - // where t.IsClass && t.Namespace == _commandNamespace - // select t; - //var commandClasses = q.ToList(); - - //foreach (var commandClass in commandClasses) - //{ - // // Load the method info from each class into a dictionary: - // var methods = commandClass.GetMethods(BindingFlags.Static | BindingFlags.Public); - // var methodDictionary = new Dictionary>(); - // foreach (var method in methods) - // { - // string commandName = method.Name; - // methodDictionary.Add(commandName, method.GetParameters()); - // } - // // Add the dictionary of methods for the current class into a dictionary of command classes: - // _commandLibraries.Add(commandClass.Name, methodDictionary); - //} Run(); } @@ -59,7 +33,7 @@ static void Run() var cmd = new ConsoleCommand(consoleInput); // Execute the command: - string result = Execute(cmd); + string result = CommandHandler.Execute(cmd); // Write out the result: WriteToConsole(result); @@ -72,299 +46,7 @@ static void Run() } } - - static string Execute(ConsoleCommand command) - { - // Validate the class name and command name: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - string badCommandMessage = string.Format("" - + "Unrecognized command \'{0}.{1}\'. " - + "Please type a valid command.", - command.LibraryClassName, command.Name); - - // Validate the command name: - if (!CommandLibrary.Content.ContainsKey(command.LibraryClassName)) - { - return badCommandMessage; - } - var methodDictionary = CommandLibrary.Content[command.LibraryClassName]; - if (!methodDictionary.ContainsKey(command.Name)) - { - return badCommandMessage; - } - - // Make sure the corret number of required arguments are provided: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - var methodParameterValueList = new List(); - IEnumerable paramInfoList = methodDictionary[command.Name].ToList(); - - // Validate proper # of required arguments provided. Some may be optional: - var requiredParams = paramInfoList.Where(p => p.IsOptional == false); - var optionalParams = paramInfoList.Where(p => p.IsOptional == true); - int requiredCount = requiredParams.Count(); - int optionalCount = optionalParams.Count(); - int providedCount = command.Arguments.Count(); - - if (requiredCount > providedCount) - { - return string.Format( - "Missing required argument. {0} required, {1} optional, {2} provided", - requiredCount, optionalCount, providedCount); - } - - // Make sure all arguments are coerced to the proper type, and that there is a - // value for every emthod parameter. The InvokeMember method fails if the number - // of arguments provided does not match the number of parameters in the - // method signature, even if some are optional: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - if (paramInfoList.Count() > 0) - { - // Populate the list with default values: - foreach (var param in paramInfoList) - { - // This will either add a null object reference if the param is required - // by the method, or will set a default value for optional parameters. in - // any case, there will be a value or null for each method argument - // in the method signature: - methodParameterValueList.Add(param.DefaultValue); - } - - // Now walk through all the arguments passed from the console and assign - // accordingly. Any optional arguments not provided have already been set to - // the default specified by the method signature: - for (int i = 0; i < command.Arguments.Count(); i++) - { - var methodParam = paramInfoList.ElementAt(i); - var typeRequired = methodParam.ParameterType; - object value = null; - try - { - // Coming from the Console, all of our arguments are passed in as - // strings. Coerce to the type to match the method paramter: - value = CoerceArgument(typeRequired, command.Arguments.ElementAt(i)); - methodParameterValueList.RemoveAt(i); - methodParameterValueList.Insert(i, value); - } - catch (ArgumentException ex) - { - string argumentName = methodParam.Name; - string argumentTypeName = typeRequired.Name; - string message = - string.Format("" - + "The value passed for argument '{0}' cannot be parsed to type '{1}'", - argumentName, argumentTypeName); - throw new ArgumentException(message); - } - } - } - - // Set up to invoke the method using reflection: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - Assembly current = typeof(Program).Assembly; - - // Need the full Namespace for this: - Type commandLibaryClass = - current.GetType(_commandNamespace + "." + command.LibraryClassName); - - object[] inputArgs = null; - if (methodParameterValueList.Count > 0) - { - inputArgs = methodParameterValueList.ToArray(); - } - var typeInfo = commandLibaryClass; - - // This will throw if the number of arguments provided does not match the number - // required by the method signature, even if some are optional: - try - { - var result = typeInfo.InvokeMember( - command.Name, - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, null, inputArgs); - return result.ToString(); - } - catch (TargetInvocationException ex) - { - throw ex.InnerException; - } - } - - - static object CoerceArgument(Type requiredType, string inputValue) - { - var requiredTypeCode = Type.GetTypeCode(requiredType); - string exceptionMessage = - string.Format("Cannnot coerce the input argument {0} to required type {1}", - inputValue, requiredType.Name); - - object result = null; - switch (requiredTypeCode) - { - case TypeCode.String: - result = inputValue; - break; - - case TypeCode.Int16: - short number16; - if (Int16.TryParse(inputValue, out number16)) - { - result = number16; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Int32: - int number32; - if (Int32.TryParse(inputValue, out number32)) - { - result = number32; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Int64: - long number64; - if (Int64.TryParse(inputValue, out number64)) - { - result = number64; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Boolean: - bool trueFalse; - if (bool.TryParse(inputValue, out trueFalse)) - { - result = trueFalse; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Byte: - byte byteValue; - if (byte.TryParse(inputValue, out byteValue)) - { - result = byteValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Char: - char charValue; - if (char.TryParse(inputValue, out charValue)) - { - result = charValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.DateTime: - DateTime dateValue; - if (DateTime.TryParse(inputValue, out dateValue)) - { - result = dateValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.Decimal: - Decimal decimalValue; - if (Decimal.TryParse(inputValue, out decimalValue)) - { - result = decimalValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.Double: - Double doubleValue; - if (Double.TryParse(inputValue, out doubleValue)) - { - result = doubleValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.Single: - Single singleValue; - if (Single.TryParse(inputValue, out singleValue)) - { - result = singleValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.UInt16: - UInt16 uInt16Value; - if (UInt16.TryParse(inputValue, out uInt16Value)) - { - result = uInt16Value; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.UInt32: - UInt32 uInt32Value; - if (UInt32.TryParse(inputValue, out uInt32Value)) - { - result = uInt32Value; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.UInt64: - UInt64 uInt64Value; - if (UInt64.TryParse(inputValue, out uInt64Value)) - { - result = uInt64Value; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - default: - throw new ArgumentException(exceptionMessage); - } - return result; - } - - - public static void WriteToConsole(string message = "") + static void WriteToConsole(string message = "") { if(message.Length > 0) { @@ -374,7 +56,7 @@ public static void WriteToConsole(string message = "") const string _readPrompt = "console> "; - public static string ReadFromConsole(string promptMessage = "") + static string ReadFromConsole(string promptMessage = "") { // Show a prompt, and get input: Console.Write(_readPrompt + promptMessage); diff --git a/TestLib/Properties/AssemblyInfo.cs b/TestLib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..98d4571 --- /dev/null +++ b/TestLib/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die einer Assembly zugeordnet sind. +[assembly: AssemblyTitle("TestLib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("TestLib")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly +// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von +// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. +[assembly: ComVisible(false)] + +// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird +[assembly: Guid("2304b2f5-905d-46cd-9bc1-f10973fbedc4")] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, +// indem Sie "*" wie unten gezeigt eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TestLib/TestClass.cs b/TestLib/TestClass.cs new file mode 100644 index 0000000..59b6d3e --- /dev/null +++ b/TestLib/TestClass.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase.Commands +{ + public static class TestClass + { + public static string RunTestMethod() + { + return "ran test method!"; + } + + public static string RunAnotherMethod(string name) + { + return "Ran another Method named: " + name; + } + } +} diff --git a/TestLib/TestLib.csproj b/TestLib/TestLib.csproj new file mode 100644 index 0000000..01bc5a3 --- /dev/null +++ b/TestLib/TestLib.csproj @@ -0,0 +1,47 @@ + + + + + Debug + AnyCPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4} + Library + Properties + TestLib + TestLib + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + \ No newline at end of file From c8212347a2957515228c1956352d357632f4403d Mon Sep 17 00:00:00 2001 From: rdoe Date: Tue, 16 May 2017 08:32:39 +0200 Subject: [PATCH 03/11] extended CommandLibrary Dictionary with CommandCLassInfo class to store the owning assembly of each command class for future extensibility in regards to external assemblies --- ConsoleApplicationBase/CommandClassInfo.cs | 21 +++++++++++ ConsoleApplicationBase/CommandLibrary.cs | 6 ++-- .../ConsoleApplicationBase.csproj | 1 + ConsoleApplicationBase/Program.cs | 36 +++---------------- 4 files changed, 30 insertions(+), 34 deletions(-) create mode 100644 ConsoleApplicationBase/CommandClassInfo.cs diff --git a/ConsoleApplicationBase/CommandClassInfo.cs b/ConsoleApplicationBase/CommandClassInfo.cs new file mode 100644 index 0000000..85fef97 --- /dev/null +++ b/ConsoleApplicationBase/CommandClassInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase +{ + public class CommandClassInfo + { + public Assembly OwningAssembly { get; } + public Dictionary> MethodDictionary { get; } + + public CommandClassInfo(Assembly owningAssembly, Dictionary> methodDict) + { + this.OwningAssembly = owningAssembly; + this.MethodDictionary = methodDict; + } + } +} diff --git a/ConsoleApplicationBase/CommandLibrary.cs b/ConsoleApplicationBase/CommandLibrary.cs index b6e7d19..d5300af 100644 --- a/ConsoleApplicationBase/CommandLibrary.cs +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -10,12 +10,12 @@ namespace ConsoleApplicationBase public static class CommandLibrary { const string _commandNamespace = "ConsoleApplicationBase.Commands"; - public static Dictionary>> Content; + public static Dictionary Content { get; } static CommandLibrary() { - Content = new Dictionary>>(); + Content = new Dictionary(); initialize(); } @@ -81,7 +81,7 @@ static void addCommands(List cmdClasses) methodDictionary.Add(commandName, method.GetParameters()); } // Add the dictionary of methods for the current class into a dictionary of command classes: - Content.Add(commandClass.Name, methodDictionary); + Content.Add(commandClass.Name, new CommandClassInfo(null ,methodDictionary)); } } diff --git a/ConsoleApplicationBase/ConsoleApplicationBase.csproj b/ConsoleApplicationBase/ConsoleApplicationBase.csproj index 3e93f85..9ea197f 100644 --- a/ConsoleApplicationBase/ConsoleApplicationBase.csproj +++ b/ConsoleApplicationBase/ConsoleApplicationBase.csproj @@ -41,6 +41,7 @@ + diff --git a/ConsoleApplicationBase/Program.cs b/ConsoleApplicationBase/Program.cs index 858f91c..b81e5b9 100644 --- a/ConsoleApplicationBase/Program.cs +++ b/ConsoleApplicationBase/Program.cs @@ -10,38 +10,12 @@ namespace ConsoleApplicationBase class Program { const string _commandNamespace = "ConsoleApplicationBase.Commands"; - static Dictionary>> _commandLibraries; + //static Dictionary>> _commandLibraries; static void Main(string[] args) { Console.Title = typeof(Program).Name; - - //_commandLibraries = CommandLibrary.Content; - - // Any static classes containing commands for use from the - // console are located in the Commands namespace. Load - // references to each type in that namespace via reflection: - //_commandLibraries = new Dictionary>>(); - - // Use reflection to load all of the classes in the Commands namespace: - //var q = from t in Assembly.GetExecutingAssembly().GetTypes() - // where t.IsClass && t.Namespace == _commandNamespace - // select t; - //var commandClasses = q.ToList(); - - //foreach (var commandClass in commandClasses) - //{ - // // Load the method info from each class into a dictionary: - // var methods = commandClass.GetMethods(BindingFlags.Static | BindingFlags.Public); - // var methodDictionary = new Dictionary>(); - // foreach (var method in methods) - // { - // string commandName = method.Name; - // methodDictionary.Add(commandName, method.GetParameters()); - // } - // // Add the dictionary of methods for the current class into a dictionary of command classes: - // _commandLibraries.Add(commandClass.Name, methodDictionary); - //} + Run(); } @@ -88,8 +62,8 @@ static string Execute(ConsoleCommand command) { return badCommandMessage; } - var methodDictionary = CommandLibrary.Content[command.LibraryClassName]; - if (!methodDictionary.ContainsKey(command.Name)) + var commandClassInfo = CommandLibrary.Content[command.LibraryClassName]; + if (!commandClassInfo.MethodDictionary.ContainsKey(command.Name)) { return badCommandMessage; } @@ -98,7 +72,7 @@ static string Execute(ConsoleCommand command) // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ var methodParameterValueList = new List(); - IEnumerable paramInfoList = methodDictionary[command.Name].ToList(); + IEnumerable paramInfoList = commandClassInfo.MethodDictionary[command.Name].ToList(); // Validate proper # of required arguments provided. Some may be optional: var requiredParams = paramInfoList.Where(p => p.IsOptional == false); From 16dd4b3a6bb3f97fce67f671122dc9715041105c Mon Sep 17 00:00:00 2001 From: rdoe Date: Tue, 16 May 2017 08:50:16 +0200 Subject: [PATCH 04/11] CommandLibrary now provides interface to add external assembly by filename added TestLib project to solution storing the owning assembly of each command class is working --- ConsoleApplicationBase.sln | 10 ++++- ConsoleApplicationBase/CommandLibrary.cs | 46 +++++++++++------------ ConsoleApplicationBase/Program.cs | 3 +- TestLib/Properties/AssemblyInfo.cs | 36 ++++++++++++++++++ TestLib/TestClass.cs | 21 +++++++++++ TestLib/TestLib.csproj | 47 ++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 26 deletions(-) create mode 100644 TestLib/Properties/AssemblyInfo.cs create mode 100644 TestLib/TestClass.cs create mode 100644 TestLib/TestLib.csproj diff --git a/ConsoleApplicationBase.sln b/ConsoleApplicationBase.sln index 2cedf79..1763b1a 100644 --- a/ConsoleApplicationBase.sln +++ b/ConsoleApplicationBase.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30723.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.26403.7 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApplicationBase", "ConsoleApplicationBase\ConsoleApplicationBase.csproj", "{D21CC334-9E7D-4A29-B6F9-5120351EC703}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestLib", "TestLib\TestLib.csproj", "{2304B2F5-905D-46CD-9BC1-F10973FBEDC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Debug|Any CPU.Build.0 = Debug|Any CPU {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Release|Any CPU.ActiveCfg = Release|Any CPU {D21CC334-9E7D-4A29-B6F9-5120351EC703}.Release|Any CPU.Build.0 = Release|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ConsoleApplicationBase/CommandLibrary.cs b/ConsoleApplicationBase/CommandLibrary.cs index d5300af..6134094 100644 --- a/ConsoleApplicationBase/CommandLibrary.cs +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text; @@ -22,29 +23,13 @@ static CommandLibrary() static void initialize() { - // Use reflection to load all of the classes in the Commands namespace: - //var q = from t in Assembly.GetExecutingAssembly().GetTypes() - // where t.IsClass && t.Namespace == _commandNamespace - // select t; - //var commandClasses = q.ToList(); + var assembly = Assembly.GetExecutingAssembly(); + var commandClasses = listMatchingAssemblyTypes(assembly); - var commandClasses = listMatchingAssemblyTypes(Assembly.GetExecutingAssembly()); + addCommands(assembly, commandClasses); - //foreach (var commandClass in commandClasses) - //{ - // // Load the method info from each class into a dictionary: - // var methods = commandClass.GetMethods(BindingFlags.Static | BindingFlags.Public); - // var methodDictionary = new Dictionary>(); - // foreach (var method in methods) - // { - // string commandName = method.Name; - // methodDictionary.Add(commandName, method.GetParameters()); - // } - // // Add the dictionary of methods for the current class into a dictionary of command classes: - // Content.Add(commandClass.Name, methodDictionary); - //} + addCommandsFromAssemblyFile("C:\\Users\\rdoe\\Dropbox\\TEX\\Masterarbeit\\src\\consoleApp\\ConsoleApplicationBase\\TestLib\\bin\\Debug\\TestLib.dll"); - addCommands(commandClasses); } /// @@ -66,10 +51,10 @@ static List listMatchingAssemblyTypes(Assembly assmbl) /// /// Adds public static methods from a list of classes - /// to Content /// + /// /// - static void addCommands(List cmdClasses) + static void addCommands(Assembly owningAssembly, List cmdClasses) { foreach (var commandClass in cmdClasses) { @@ -81,7 +66,22 @@ static void addCommands(List cmdClasses) methodDictionary.Add(commandName, method.GetParameters()); } // Add the dictionary of methods for the current class into a dictionary of command classes: - Content.Add(commandClass.Name, new CommandClassInfo(null ,methodDictionary)); + CommandLibrary.Content.Add(commandClass.Name, new CommandClassInfo(owningAssembly, methodDictionary)); + } + } + + /// + /// adds suitable commands from a specified assembly file + /// + /// + public static void addCommandsFromAssemblyFile(string assemblyFile) + { + if (File.Exists(assemblyFile)) + { + var extAssembly = Assembly.LoadFrom(assemblyFile); + var extCommandCLasses = listMatchingAssemblyTypes(extAssembly); + + addCommands(extAssembly, extCommandCLasses); } } diff --git a/ConsoleApplicationBase/Program.cs b/ConsoleApplicationBase/Program.cs index b81e5b9..8206ece 100644 --- a/ConsoleApplicationBase/Program.cs +++ b/ConsoleApplicationBase/Program.cs @@ -138,7 +138,8 @@ static string Execute(ConsoleCommand command) // Set up to invoke the method using reflection: // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Assembly current = typeof(Program).Assembly; + //Assembly current = typeof(Program).Assembly; + Assembly current = commandClassInfo.OwningAssembly; // Need the full Namespace for this: Type commandLibaryClass = diff --git a/TestLib/Properties/AssemblyInfo.cs b/TestLib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..98d4571 --- /dev/null +++ b/TestLib/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die einer Assembly zugeordnet sind. +[assembly: AssemblyTitle("TestLib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("TestLib")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly +// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von +// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. +[assembly: ComVisible(false)] + +// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird +[assembly: Guid("2304b2f5-905d-46cd-9bc1-f10973fbedc4")] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, +// indem Sie "*" wie unten gezeigt eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TestLib/TestClass.cs b/TestLib/TestClass.cs new file mode 100644 index 0000000..59b6d3e --- /dev/null +++ b/TestLib/TestClass.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase.Commands +{ + public static class TestClass + { + public static string RunTestMethod() + { + return "ran test method!"; + } + + public static string RunAnotherMethod(string name) + { + return "Ran another Method named: " + name; + } + } +} diff --git a/TestLib/TestLib.csproj b/TestLib/TestLib.csproj new file mode 100644 index 0000000..01bc5a3 --- /dev/null +++ b/TestLib/TestLib.csproj @@ -0,0 +1,47 @@ + + + + + Debug + AnyCPU + {2304B2F5-905D-46CD-9BC1-F10973FBEDC4} + Library + Properties + TestLib + TestLib + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + \ No newline at end of file From 99d5776482fe013711956e91e3eb78e3a77a20d3 Mon Sep 17 00:00:00 2001 From: rdoe Date: Tue, 16 May 2017 09:12:14 +0200 Subject: [PATCH 05/11] added AddExternalAssembly to DefaultCommands removed adding external assembly on initialisation of CommandLibrary --- ConsoleApplicationBase/CommandLibrary.cs | 2 +- ConsoleApplicationBase/Commands/DefaultCommands.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ConsoleApplicationBase/CommandLibrary.cs b/ConsoleApplicationBase/CommandLibrary.cs index 6134094..931ad6c 100644 --- a/ConsoleApplicationBase/CommandLibrary.cs +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -28,7 +28,7 @@ static void initialize() addCommands(assembly, commandClasses); - addCommandsFromAssemblyFile("C:\\Users\\rdoe\\Dropbox\\TEX\\Masterarbeit\\src\\consoleApp\\ConsoleApplicationBase\\TestLib\\bin\\Debug\\TestLib.dll"); + //addCommandsFromAssemblyFile("C:\\Users\\rdoe\\Dropbox\\TEX\\Masterarbeit\\src\\consoleApp\\ConsoleApplicationBase\\TestLib\\bin\\Debug\\TestLib.dll"); } diff --git a/ConsoleApplicationBase/Commands/DefaultCommands.cs b/ConsoleApplicationBase/Commands/DefaultCommands.cs index 6c79062..e5ef167 100644 --- a/ConsoleApplicationBase/Commands/DefaultCommands.cs +++ b/ConsoleApplicationBase/Commands/DefaultCommands.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -38,5 +39,18 @@ public static string DoSomethingOptional(int id, string data = "No Data Provided } return result; } + + public static string AddExternalAssembly(string assemblyFile) + { + if (File.Exists(assemblyFile)) + { + CommandLibrary.addCommandsFromAssemblyFile(assemblyFile); + return "added Assembly File : " + Path.GetFileName(assemblyFile); + } + else + { + return "Assembly file \'" + Path.GetFileName(assemblyFile) + "\' does not exist"; + } + } } } From e18f336d2cf4201edf87e615583a9c7e2fce38a5 Mon Sep 17 00:00:00 2001 From: rdoe Date: Tue, 16 May 2017 10:46:47 +0200 Subject: [PATCH 06/11] outsourced methods Execute and CoerceArguments to CommanHandler class to make Program class leaner --- ConsoleApplicationBase/CommandHandler.cs | 303 +++++++++++++++++ ConsoleApplicationBase/CommandLibrary.cs | 4 +- .../ConsoleApplicationBase.csproj | 1 + ConsoleApplicationBase/Program.cs | 304 +----------------- 4 files changed, 309 insertions(+), 303 deletions(-) create mode 100644 ConsoleApplicationBase/CommandHandler.cs diff --git a/ConsoleApplicationBase/CommandHandler.cs b/ConsoleApplicationBase/CommandHandler.cs new file mode 100644 index 0000000..2d878b3 --- /dev/null +++ b/ConsoleApplicationBase/CommandHandler.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase +{ + public static class CommandHandler + { + //static List methodParameterValueList = new List(); + //static IEnumerable paramInfoList; + + public static string Execute(ConsoleCommand command2Execute) + { + // Validate the class name and command name: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + string badCommandMessage = string.Format("" + + "Unrecognized command \'{0}.{1}\'. " + + "Please type a valid command.", + command2Execute.LibraryClassName, command2Execute.Name); + + // Validate the command name: + if (!CommandLibrary.Content.ContainsKey(command2Execute.LibraryClassName)) + { + return badCommandMessage; + } + + var commandClassInfo = CommandLibrary.Content[command2Execute.LibraryClassName]; + if (!commandClassInfo.MethodDictionary.ContainsKey(command2Execute.Name)) + { + return badCommandMessage; + } + + // Make sure the corret number of required arguments are provided: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + var methodParameterValueList = new List(); + IEnumerable paramInfoList = commandClassInfo.MethodDictionary[command2Execute.Name].ToList(); + + // Validate proper # of required arguments provided. Some may be optional: + var requiredParams = paramInfoList.Where(p => p.IsOptional == false); + var optionalParams = paramInfoList.Where(p => p.IsOptional == true); + int requiredCount = requiredParams.Count(); + int optionalCount = optionalParams.Count(); + int providedCount = command2Execute.Arguments.Count(); + + if (requiredCount > providedCount) + { + return string.Format( + "Missing required argument. {0} required, {1} optional, {2} provided", + requiredCount, optionalCount, providedCount); + } + + // Make sure all arguments are coerced to the proper type, and that there is a + // value for every emthod parameter. The InvokeMember method fails if the number + // of arguments provided does not match the number of parameters in the + // method signature, even if some are optional: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + if (paramInfoList.Count() > 0) + { + // Populate the list with default values: + foreach (var param in paramInfoList) + { + // This will either add a null object reference if the param is required + // by the method, or will set a default value for optional parameters. in + // any case, there will be a value or null for each method argument + // in the method signature: + methodParameterValueList.Add(param.DefaultValue); + } + + // Now walk through all the arguments passed from the console and assign + // accordingly. Any optional arguments not provided have already been set to + // the default specified by the method signature: + for (int i = 0; i < command2Execute.Arguments.Count(); i++) + { + var methodParam = paramInfoList.ElementAt(i); + var typeRequired = methodParam.ParameterType; + object value = null; + try + { + // Coming from the Console, all of our arguments are passed in as + // strings. Coerce to the type to match the method paramter: + value = CoerceArgument(typeRequired, command2Execute.Arguments.ElementAt(i)); + methodParameterValueList.RemoveAt(i); + methodParameterValueList.Insert(i, value); + } + catch (ArgumentException ex) + { + string argumentName = methodParam.Name; + string argumentTypeName = typeRequired.Name; + string message = + string.Format("" + + "The value passed for argument '{0}' cannot be parsed to type '{1}'", + argumentName, argumentTypeName); + throw new ArgumentException(message); + } + } + } + + // Set up to invoke the method using reflection: + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + Assembly current = commandClassInfo.OwningAssembly; //typeof(Program).Assembly; + + // Need the full Namespace for this: + Type commandLibaryClass = + current.GetType(CommandLibrary.CommandNamespace + "." + command2Execute.LibraryClassName); + + object[] inputArgs = null; + if (methodParameterValueList.Count > 0) + { + inputArgs = methodParameterValueList.ToArray(); + } + var typeInfo = commandLibaryClass; + + // This will throw if the number of arguments provided does not match the number + // required by the method signature, even if some are optional: + try + { + var result = typeInfo.InvokeMember( + command2Execute.Name, + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, null, inputArgs); + return result.ToString(); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + } + + static object CoerceArgument(Type requiredType, string inputValue) + { + var requiredTypeCode = Type.GetTypeCode(requiredType); + string exceptionMessage = + string.Format("Cannnot coerce the input argument {0} to required type {1}", + inputValue, requiredType.Name); + + object result = null; + switch (requiredTypeCode) + { + case TypeCode.String: + result = inputValue; + break; + + case TypeCode.Int16: + short number16; + if (Int16.TryParse(inputValue, out number16)) + { + result = number16; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Int32: + int number32; + if (Int32.TryParse(inputValue, out number32)) + { + result = number32; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Int64: + long number64; + if (Int64.TryParse(inputValue, out number64)) + { + result = number64; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Boolean: + bool trueFalse; + if (bool.TryParse(inputValue, out trueFalse)) + { + result = trueFalse; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Byte: + byte byteValue; + if (byte.TryParse(inputValue, out byteValue)) + { + result = byteValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.Char: + char charValue; + if (char.TryParse(inputValue, out charValue)) + { + result = charValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + + case TypeCode.DateTime: + DateTime dateValue; + if (DateTime.TryParse(inputValue, out dateValue)) + { + result = dateValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Decimal: + Decimal decimalValue; + if (Decimal.TryParse(inputValue, out decimalValue)) + { + result = decimalValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Double: + Double doubleValue; + if (Double.TryParse(inputValue, out doubleValue)) + { + result = doubleValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.Single: + Single singleValue; + if (Single.TryParse(inputValue, out singleValue)) + { + result = singleValue; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt16: + UInt16 uInt16Value; + if (UInt16.TryParse(inputValue, out uInt16Value)) + { + result = uInt16Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt32: + UInt32 uInt32Value; + if (UInt32.TryParse(inputValue, out uInt32Value)) + { + result = uInt32Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + case TypeCode.UInt64: + UInt64 uInt64Value; + if (UInt64.TryParse(inputValue, out uInt64Value)) + { + result = uInt64Value; + } + else + { + throw new ArgumentException(exceptionMessage); + } + break; + default: + throw new ArgumentException(exceptionMessage); + } + return result; + } + } +} diff --git a/ConsoleApplicationBase/CommandLibrary.cs b/ConsoleApplicationBase/CommandLibrary.cs index 931ad6c..5caac0a 100644 --- a/ConsoleApplicationBase/CommandLibrary.cs +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -10,7 +10,7 @@ namespace ConsoleApplicationBase { public static class CommandLibrary { - const string _commandNamespace = "ConsoleApplicationBase.Commands"; + public static readonly string CommandNamespace = "ConsoleApplicationBase.Commands"; public static Dictionary Content { get; } @@ -42,7 +42,7 @@ static void initialize() static List listMatchingAssemblyTypes(Assembly assmbl) { var q = from t in assmbl.GetTypes() - where t.IsClass && t.Namespace == _commandNamespace + where t.IsClass && t.Namespace == CommandNamespace select t; return q.ToList(); diff --git a/ConsoleApplicationBase/ConsoleApplicationBase.csproj b/ConsoleApplicationBase/ConsoleApplicationBase.csproj index 9ea197f..f100082 100644 --- a/ConsoleApplicationBase/ConsoleApplicationBase.csproj +++ b/ConsoleApplicationBase/ConsoleApplicationBase.csproj @@ -42,6 +42,7 @@ + diff --git a/ConsoleApplicationBase/Program.cs b/ConsoleApplicationBase/Program.cs index 8206ece..5f80ca6 100644 --- a/ConsoleApplicationBase/Program.cs +++ b/ConsoleApplicationBase/Program.cs @@ -9,9 +9,6 @@ namespace ConsoleApplicationBase { class Program { - const string _commandNamespace = "ConsoleApplicationBase.Commands"; - //static Dictionary>> _commandLibraries; - static void Main(string[] args) { Console.Title = typeof(Program).Name; @@ -19,7 +16,6 @@ static void Main(string[] args) Run(); } - static void Run() { while (true) @@ -33,7 +29,7 @@ static void Run() var cmd = new ConsoleCommand(consoleInput); // Execute the command: - string result = Execute(cmd); + string result = CommandHandler.Execute(cmd); // Write out the result: WriteToConsole(result); @@ -46,300 +42,7 @@ static void Run() } } - - static string Execute(ConsoleCommand command) - { - // Validate the class name and command name: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - string badCommandMessage = string.Format("" - + "Unrecognized command \'{0}.{1}\'. " - + "Please type a valid command.", - command.LibraryClassName, command.Name); - - // Validate the command name: - if (!CommandLibrary.Content.ContainsKey(command.LibraryClassName)) - { - return badCommandMessage; - } - var commandClassInfo = CommandLibrary.Content[command.LibraryClassName]; - if (!commandClassInfo.MethodDictionary.ContainsKey(command.Name)) - { - return badCommandMessage; - } - - // Make sure the corret number of required arguments are provided: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - var methodParameterValueList = new List(); - IEnumerable paramInfoList = commandClassInfo.MethodDictionary[command.Name].ToList(); - - // Validate proper # of required arguments provided. Some may be optional: - var requiredParams = paramInfoList.Where(p => p.IsOptional == false); - var optionalParams = paramInfoList.Where(p => p.IsOptional == true); - int requiredCount = requiredParams.Count(); - int optionalCount = optionalParams.Count(); - int providedCount = command.Arguments.Count(); - - if (requiredCount > providedCount) - { - return string.Format( - "Missing required argument. {0} required, {1} optional, {2} provided", - requiredCount, optionalCount, providedCount); - } - - // Make sure all arguments are coerced to the proper type, and that there is a - // value for every emthod parameter. The InvokeMember method fails if the number - // of arguments provided does not match the number of parameters in the - // method signature, even if some are optional: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - if (paramInfoList.Count() > 0) - { - // Populate the list with default values: - foreach (var param in paramInfoList) - { - // This will either add a null object reference if the param is required - // by the method, or will set a default value for optional parameters. in - // any case, there will be a value or null for each method argument - // in the method signature: - methodParameterValueList.Add(param.DefaultValue); - } - - // Now walk through all the arguments passed from the console and assign - // accordingly. Any optional arguments not provided have already been set to - // the default specified by the method signature: - for (int i = 0; i < command.Arguments.Count(); i++) - { - var methodParam = paramInfoList.ElementAt(i); - var typeRequired = methodParam.ParameterType; - object value = null; - try - { - // Coming from the Console, all of our arguments are passed in as - // strings. Coerce to the type to match the method paramter: - value = CoerceArgument(typeRequired, command.Arguments.ElementAt(i)); - methodParameterValueList.RemoveAt(i); - methodParameterValueList.Insert(i, value); - } - catch (ArgumentException ex) - { - string argumentName = methodParam.Name; - string argumentTypeName = typeRequired.Name; - string message = - string.Format("" - + "The value passed for argument '{0}' cannot be parsed to type '{1}'", - argumentName, argumentTypeName); - throw new ArgumentException(message); - } - } - } - - // Set up to invoke the method using reflection: - // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - //Assembly current = typeof(Program).Assembly; - Assembly current = commandClassInfo.OwningAssembly; - - // Need the full Namespace for this: - Type commandLibaryClass = - current.GetType(_commandNamespace + "." + command.LibraryClassName); - - object[] inputArgs = null; - if (methodParameterValueList.Count > 0) - { - inputArgs = methodParameterValueList.ToArray(); - } - var typeInfo = commandLibaryClass; - - // This will throw if the number of arguments provided does not match the number - // required by the method signature, even if some are optional: - try - { - var result = typeInfo.InvokeMember( - command.Name, - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, null, inputArgs); - return result.ToString(); - } - catch (TargetInvocationException ex) - { - throw ex.InnerException; - } - } - - - static object CoerceArgument(Type requiredType, string inputValue) - { - var requiredTypeCode = Type.GetTypeCode(requiredType); - string exceptionMessage = - string.Format("Cannnot coerce the input argument {0} to required type {1}", - inputValue, requiredType.Name); - - object result = null; - switch (requiredTypeCode) - { - case TypeCode.String: - result = inputValue; - break; - - case TypeCode.Int16: - short number16; - if (Int16.TryParse(inputValue, out number16)) - { - result = number16; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Int32: - int number32; - if (Int32.TryParse(inputValue, out number32)) - { - result = number32; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Int64: - long number64; - if (Int64.TryParse(inputValue, out number64)) - { - result = number64; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Boolean: - bool trueFalse; - if (bool.TryParse(inputValue, out trueFalse)) - { - result = trueFalse; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Byte: - byte byteValue; - if (byte.TryParse(inputValue, out byteValue)) - { - result = byteValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.Char: - char charValue; - if (char.TryParse(inputValue, out charValue)) - { - result = charValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - - case TypeCode.DateTime: - DateTime dateValue; - if (DateTime.TryParse(inputValue, out dateValue)) - { - result = dateValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.Decimal: - Decimal decimalValue; - if (Decimal.TryParse(inputValue, out decimalValue)) - { - result = decimalValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.Double: - Double doubleValue; - if (Double.TryParse(inputValue, out doubleValue)) - { - result = doubleValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.Single: - Single singleValue; - if (Single.TryParse(inputValue, out singleValue)) - { - result = singleValue; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.UInt16: - UInt16 uInt16Value; - if (UInt16.TryParse(inputValue, out uInt16Value)) - { - result = uInt16Value; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.UInt32: - UInt32 uInt32Value; - if (UInt32.TryParse(inputValue, out uInt32Value)) - { - result = uInt32Value; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - case TypeCode.UInt64: - UInt64 uInt64Value; - if (UInt64.TryParse(inputValue, out uInt64Value)) - { - result = uInt64Value; - } - else - { - throw new ArgumentException(exceptionMessage); - } - break; - default: - throw new ArgumentException(exceptionMessage); - } - return result; - } - - - public static void WriteToConsole(string message = "") + static void WriteToConsole(string message = "") { if(message.Length > 0) { @@ -347,9 +50,8 @@ public static void WriteToConsole(string message = "") } } - const string _readPrompt = "console> "; - public static string ReadFromConsole(string promptMessage = "") + static string ReadFromConsole(string promptMessage = "") { // Show a prompt, and get input: Console.Write(_readPrompt + promptMessage); From 73c2ef34a7f935d4eadce2ee08391b28e438845e Mon Sep 17 00:00:00 2001 From: rdoe Date: Tue, 16 May 2017 14:24:17 +0200 Subject: [PATCH 07/11] Added AppState to allow app control from outside Program added App.config file to store app settings extended CommandLibrary initialize function with auto loading external assemblies specified in config file --- ConsoleApplicationBase/App.config | 17 ++++++++ ConsoleApplicationBase/AppState.cs | 36 ++++++++++++++++ ConsoleApplicationBase/CommandLibrary.cs | 9 +++- .../Commands/DefaultCommands.cs | 6 +++ .../ConsoleApplicationBase.csproj | 11 +++++ ConsoleApplicationBase/Program.cs | 3 +- .../Properties/Settings.Designer.cs | 41 +++++++++++++++++++ .../Properties/Settings.settings | 12 ++++++ 8 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 ConsoleApplicationBase/AppState.cs create mode 100644 ConsoleApplicationBase/Properties/Settings.Designer.cs create mode 100644 ConsoleApplicationBase/Properties/Settings.settings diff --git a/ConsoleApplicationBase/App.config b/ConsoleApplicationBase/App.config index 8e15646..8309f01 100644 --- a/ConsoleApplicationBase/App.config +++ b/ConsoleApplicationBase/App.config @@ -1,6 +1,23 @@  + + +
+ + + + + + + + C:\Users\rdoe\Dropbox\TEX\Masterarbeit\src\consoleApp\ConsoleApplicationBase\TestLib\bin\Debug\TestLib.dll + + + + + \ No newline at end of file diff --git a/ConsoleApplicationBase/AppState.cs b/ConsoleApplicationBase/AppState.cs new file mode 100644 index 0000000..c1eb3a0 --- /dev/null +++ b/ConsoleApplicationBase/AppState.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleApplicationBase +{ + public static class AppState + { + static State _state; + + static AppState() + { + _state = State.IDLE; + } + + public static State GetState() + { + return _state; + } + + public static void SetState(State newState) + { + _state = newState; + } + + } + + public enum State + { + ERROR = -1, + IDLE = 0, + RUNNING = 1 + } +} diff --git a/ConsoleApplicationBase/CommandLibrary.cs b/ConsoleApplicationBase/CommandLibrary.cs index 5caac0a..bf7b245 100644 --- a/ConsoleApplicationBase/CommandLibrary.cs +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using ConsoleApplicationBase.Properties; namespace ConsoleApplicationBase { @@ -26,10 +27,14 @@ static void initialize() var assembly = Assembly.GetExecutingAssembly(); var commandClasses = listMatchingAssemblyTypes(assembly); + //add commands defined inside DefaultCommands addCommands(assembly, commandClasses); - //addCommandsFromAssemblyFile("C:\\Users\\rdoe\\Dropbox\\TEX\\Masterarbeit\\src\\consoleApp\\ConsoleApplicationBase\\TestLib\\bin\\Debug\\TestLib.dll"); - + //add commands from external assemblies listed in App.config + foreach (var assemblyFile in Settings.Default.AssemblyFiles) + { + addCommandsFromAssemblyFile(assemblyFile); + } } /// diff --git a/ConsoleApplicationBase/Commands/DefaultCommands.cs b/ConsoleApplicationBase/Commands/DefaultCommands.cs index e5ef167..ab1bce4 100644 --- a/ConsoleApplicationBase/Commands/DefaultCommands.cs +++ b/ConsoleApplicationBase/Commands/DefaultCommands.cs @@ -52,5 +52,11 @@ public static string AddExternalAssembly(string assemblyFile) return "Assembly file \'" + Path.GetFileName(assemblyFile) + "\' does not exist"; } } + + public static string Exit() + { + AppState.SetState(State.IDLE); + return "Exiting Application..."; + } } } diff --git a/ConsoleApplicationBase/ConsoleApplicationBase.csproj b/ConsoleApplicationBase/ConsoleApplicationBase.csproj index f100082..b7854c0 100644 --- a/ConsoleApplicationBase/ConsoleApplicationBase.csproj +++ b/ConsoleApplicationBase/ConsoleApplicationBase.csproj @@ -33,6 +33,7 @@ + @@ -41,6 +42,7 @@ + @@ -51,9 +53,18 @@ + + True + True + Settings.settings + + + SettingsSingleFileGenerator + Settings.Designer.cs +