Introduction
Actually I am mostly a C++ developer, but I found that C# with .NET framework is a perfect tool for my frequent auxiliary jobs like, log files and data dump files analyses and other similar tasks. Thus, I have written a number of console utilities in C#. It is quite normal that each Main()
function of these utilities does command line parsing, arguments validation and conversion into proper types, decides which method has to be called and then calls this method with prepared parameter values.
But I am tired of composing each command line rules and of writing a boring code that follows these rules. And I am tired of updating these rules and corresponding code, every time after adding or changing utility abilities. I have found a solution. From this time onwards, I prefer to write a direct call to a method with necessary parameter values just in a command line. For example: test.exe "Job1(1, Math.Sqrt(3.14), new MyClass())" or "Job2(@\"C:\Temp\", true)"
where Job1()
and Job2()
are existing public methods. I do not write a line of code to parse command lines in my utilities any more. Thanks C# and .NET!
What can you do with the Accessor class?
- You can evaluate C# string expression or execute a method text at runtime; note that these expressions and methods can freely access all existing public members in the main assembly (or in any other additional, if specified);
- My proposal is to apply this ability to evaluate or execute a command line in the
Main()
! As you have access to public members of the main assembly, you can invoke any method that was designed for that. A command line becomes a real part of your C# program and it is written also in C#.
- Although I have started the article with words about console applications, you can apply this technique for Win Forms as well.
Your benefits if the Main() evaluates a command line
- If you change or extend the program functionality, then you do not change
Main()
any more. You can freely call new or changed methods just from a command line.
- You can use this effectively for debugging. Do you have a just created method not yet debugged? Do you want to invoke it immediately? Just write the call to this method in the command line with some parameter values and run debugging.
- You can use this effectively for testing. You can call for testing, all public methods of your program just from a command line. The simplest way is a batch file with multiple runs of your program with different command lines, read: different calls to different methods.
A word about safety
This approach is not a toy for a final user! It is not a good idea to give a customer the ability to call any public method in your program from a command line. This approach is mostly for software developers themselves, just to eliminate a lot of simple but very boring and non-creative work.
Nevertheless, there are different quite safe ways to keep and use this ability in a released application. You can design your application so that your main assembly has only the methods intended for calling from a command line. These methods themselves have access to the other application assemblies, but a source written in command line has to be referenced to the main assembly, only in order to access the methods designed for that. A command line source compilation will fail on attempting to access something else which is not allowed. Of course it is up to a developer how to protect his application from dummies or hackers.
The code
The code below has just to be added to your code to make it all possible. It defines the class Accessor
and also contains examples of how it works (namespace Test
, see 3 examples in Test.Example.Main()
). You have to define the EXAMPLE
constant or you may just replace it with true
to enable the example code. It seems I have nothing to say here any more, the code and comments say the rest. Note that Accessor
can also be used as expressions evaluator and methods executor.
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System;
#if EXAMPLE
namespace Test
{
public class MyClass
{
public MyClass()
{
Console.WriteLine("Hello from Test.MyClass()");
}
protected int MyMethod()
{
Console.WriteLine("Hello from Test.MyClass.MyMethod()");
return 12345;
}
}
public class Example
{
public int Job1(int arg1, double arg2, MyClass arg3)
{
Console.WriteLine
("Hello from Job1() with {0}, {1}, {2}", arg1, arg2, arg3);
return 0;
}
public int Job2(string arg1, bool arg2)
{
Console.WriteLine
("Hello from Job2() with {0}, {1}", arg1, arg2);
return 0;
}
static int Main(string[] args)
{
string myExpression = "M.Sin(0.5) + M.Sqrt(2.0)";
Console.WriteLine("\nExample 1. Evaluating:\n{0}...",
myExpression);
double result1 = (double)Absolute.Accessor.Evaluate(
myExpression,
false,
"using M = System.Math;",
null);
Console.WriteLine("Evaluation result: {0}", result1);
string myMethod = @"
int Executable()
{
Console.WriteLine(""Hello from Executable()"");
return MyMethod();
}";
Console.WriteLine("\nExample 2. Executing method:{0}...",
myMethod);
int result2 = (int)Absolute.Accessor.Execute(
myMethod,
true,
"using Test;",
"MyClass");
Console.WriteLine("Execution result: {0}", result2);
int jobResult;
try
{
string args0 =
args.Length > 0 ?
args[0] : "Job1(1, Math.Sqrt(3.14), new MyClass())";
Console.WriteLine
("\nExample 3. Running command line:\n\"{0}\"", args0);
jobResult = (int)Absolute.Accessor.Evaluate(
args0,
true,
"using Test;",
"Example");
Console.WriteLine("Command line result: {0}", jobResult);
}
catch(Exception e)
{
Console.WriteLine(e.Message);
jobResult = -1;
}
return jobResult;
}
}
}
#endif
namespace Absolute
{
public sealed class Accessor
{
public static object Evaluate(
string expression,
bool mainAssembly,
string headCode,
string classBase,
params string[] assemblies)
{
string methodCode = String.Format(
"object Expression()\n{{return {0};}}", expression);
return Execute(
methodCode, mainAssembly, headCode, classBase, assemblies);
}
public static object Execute(
string methodCode,
bool mainAssembly,
string headCode,
string classBase,
params string[] assemblies)
{
int i1, i2 = methodCode.IndexOf('(');
if(i2 < 1)
throw new Exception("Accessor: syntax error: ( expected");
for(--i2; i2 >= 0 && methodCode[i2] <= ' '; --i2);
for(i1 = i2; i1 >= 0 && methodCode[i1] > ' '; --i1);
string methodName = methodCode.Substring(i1 + 1, i2 - i1);
StringBuilder code = new StringBuilder();
code.Append("using System;\n");
if(headCode != null)
code.AppendFormat("{0}\n", headCode);
code.Append("public class AbsoluteClass");
if(classBase != null)
code.AppendFormat(" : {0}", classBase);
code.AppendFormat("\n{{\npublic {0}\n}}\n", methodCode);
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = false;
parameters.GenerateInMemory = true;
parameters.ReferencedAssemblies.Add("system.dll");
if(mainAssembly)
parameters.ReferencedAssemblies.Add(
Process.GetCurrentProcess().MainModule.FileName);
if(assemblies != null)
parameters.ReferencedAssemblies.AddRange(assemblies);
ICodeCompiler compiler =
new CSharpCodeProvider().CreateCompiler();
CompilerResults results =
compiler.CompileAssemblyFromSource(
parameters, code.ToString());
if(results.Errors.HasErrors)
{
StringBuilder error = new StringBuilder();
error.Append("Accessor: compilation errors:\n");
foreach(CompilerError err in results.Errors)
error.AppendFormat(
"#{0} {1}. Line {2}.\n",
err.ErrorNumber, err.ErrorText, err.Line);
error.AppendFormat("\n{0}", code);
throw new Exception(error.ToString());
}
object classInstance =
results.CompiledAssembly.CreateInstance("AbsoluteClass");
MethodInfo info = classInstance.GetType().GetMethod(methodName);
return info.Invoke(classInstance, null);
}
private Accessor()
{
}
}
}
History
- 07/20/2003 - Original article.
- 07/21/2003 - More detailed explanation and code comments.