Most enterprise projects have one or more console applications for utility tasks such as cleaning up or importing data into the database. These utilities tend to be project-specific and small in terms of code size, and instead of several smaller assemblies, it makes sense to combine these into a single assembly. The generic runner would read the utility, called the command, and arguments from the command-line and use the command pattern to create and execute it.
For the generic runner to work, each command has to fulfill the contract.
public enum ExitCode { Success = 0, Failure }; public interface ICommand { string Usage { get; } string Description { get; } ExitCode Execute(string[] args); }
I want the runner to adhere to the open/closed principle. For its behavior to be modified without altering its core delegation logic. This requires the use of reflection to retrieve and instantiate a command based on command-line arguments.
class Program { static IEnumerable<ICommand> GetCommands() { var iCommand = typeof (ICommand); return System.Reflection.Assembly.GetExecutingAssembly().GetTypes().ToList() .Where(t => iCommand.IsAssignableFrom(t) && t != iCommand) .Select(t => Activator.CreateInstance(t) as ICommand); } static void DisplayHelp() { Console.WriteLine("Console [Command] [Arg1] [Arg2] [ArgN]\n\n"); GetCommands().ToList().ForEach(command => Console.WriteLine(command.Usage + "\n" + command.Description + "\n\n")); } static int Main(string[] args) { if (args.Length == 0) { DisplayHelp(); return (int)ExitCode.Failure; } var commandName = args[0]; var command = GetCommands().SingleOrDefault(t => t.GetType().Name == commandName); if (command == null) throw new ArgumentException(string.Format("Command '{0}' not found", commandName)); var executeArguments = new List<string>(args); executeArguments.RemoveAt(0); var exitCode = command.Execute(executeArguments.ToArray()); return (int)exitCode; } }
A trivial example of a command that adds two numbers would be the following:
// $> GenericRunner.exe Calculator 2 3 => 2 + 3 = 5 public class Calculator : ICommand { public string Usage { get { return "Calculator [Op1] [Op2]"; } } public string Description { get { return "World's simplest calculator"; } } public ExitCode Execute(string[] args) { try { Console.WriteLine( "{0} + {1} = {2}", args[0], args[1], int.Parse(args[0]) + int.Parse(args[1])); return ExitCode.Success; } catch (Exception e) { Console.WriteLine(e.ToString()); return ExitCode.Failure; } } }
Now multiple smaller assemblies can be grouped into one, with a description of all commands automatically being assembled, and without commands interfering (too much) with each other.