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.