diff --git a/.gitignore b/.gitignore index 176c449..f848412 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ obj/ _ReSharper*/ [Tt]est[Rr]esult* *.vssscc -$tf*/ \ No newline at end of file +$tf*/ +.vs/ 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/App.config b/ConsoleApplicationBase/App.config index 8e15646..73313cb 100644 --- a/ConsoleApplicationBase/App.config +++ b/ConsoleApplicationBase/App.config @@ -1,6 +1,23 @@  + + +
+ + + + + + + + ..\..\..\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/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/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 new file mode 100644 index 0000000..bf7b245 --- /dev/null +++ b/ConsoleApplicationBase/CommandLibrary.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using ConsoleApplicationBase.Properties; + +namespace ConsoleApplicationBase +{ + public static class CommandLibrary + { + public static readonly string CommandNamespace = "ConsoleApplicationBase.Commands"; + public static Dictionary Content { get; } + + + static CommandLibrary() + { + Content = new Dictionary(); + initialize(); + } + + + static void initialize() + { + var assembly = Assembly.GetExecutingAssembly(); + var commandClasses = listMatchingAssemblyTypes(assembly); + + //add commands defined inside DefaultCommands + addCommands(assembly, commandClasses); + + //add commands from external assemblies listed in App.config + foreach (var assemblyFile in Settings.Default.AssemblyFiles) + { + addCommandsFromAssemblyFile(assemblyFile); + } + } + + /// + /// 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 + /// + /// + /// + static void addCommands(Assembly owningAssembly, 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: + 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..ab1bce4 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,24 @@ 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"; + } + } + + public static string Exit() + { + AppState.SetState(State.IDLE); + return "Exiting Application..."; + } } } diff --git a/ConsoleApplicationBase/ConsoleApplicationBase.csproj b/ConsoleApplicationBase/ConsoleApplicationBase.csproj index 8c3be9f..b7854c0 100644 --- a/ConsoleApplicationBase/ConsoleApplicationBase.csproj +++ b/ConsoleApplicationBase/ConsoleApplicationBase.csproj @@ -33,6 +33,7 @@ + @@ -41,6 +42,10 @@ + + + + @@ -48,9 +53,18 @@ + + True + True + Settings.settings + + + SettingsSingleFileGenerator + Settings.Designer.cs +