Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to Execute External Uncompiled C# Code

0.00/5 (No votes)
12 May 2014 1  
Compiling and running C# code from your application.

Introduction

Ever had a need to run an external script from your application, and wish your script had the power and speed of a compiled language like C#? This "how to" shows you how to read in a C# code file, compile it, check for errors, and execute the code. You can even pass in objects from within your compiled application for the external code to use.

Background

From time to time, I have had need to extend a program I wrote by having it execute a script, but wanted the script to use data from within the application, logging, etc. This approach allows me to use the power of C#. In production code, I would use checksums on the C# code files to make sure only authorized scripts could be compiled and run.

Using the code

The core of how this works is found in the class CodeDOMProcessor.cs in the attached ZIP file. The project shows how to use this class to read in a code file designed for this example.

It should be noted that the using statements in an external code file use the file name (e.g. "System.dll" instead of "System"). If the reference is not in the GAC, the reference must be the fully qualified file name of the assembly file. One user has reported he did not need the ".dll", but in the initial development for this article (using VS2010), failure to include it caused runtime errors as the referenced assemblies could not be resolved. The errors received on compile are:

CompilerResults CompileResults = DOMProvider.CompileAssemblyFromSource(DOMCompilerParams, pCodeToCompile); 
Colourised in 0ms

Error# [CS0006] - [Metadata file 'System' could not be found] Line# [0] Column# [0].
Error# [CS0006] - [Metadata file 'System.Windows.Forms' could not be found] Line# [0] Column# [0].
Error# [CS0006] - [Metadata file 'System.Runtime.Serialization' could not be found] Line# [0] Column# [0].

It does not hurt to add it, and if you do not add it, an d get such exceptions, then add the ".dll".

Writing an External C# Code File

Obviously, the code you want to execute hast be as compilable as the code you would have in your project. The code shown below exists in the example project ZIP file, RunExternal.zip, as Test.cs. It includes a set of "using" declarations (which have no meaning to the compiler), and the expected namespace, class, and other statements, declarations, and code - even comments. I have found that the compiler expects type names to be the full name, leading me to believe the IDE expands that "under the covers".

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">using System.dll;
using System.Windows.Forms.dll;
using System.Runtime.Serialization.dll;
 
namespace JeffJones.ExternalCode
{
 
    public class JJScriptTest
    {
 
        public System.Int32 TestMemberInt = 0;
 
        private System.String m_Message = "";
 
        private System.String m_Caption = "";
 
        /// <summary>
        /// 
        /// </summary>
        /// <param name="pTestObject"></param>
        /// <param name="pCaption"></param>
        /// <param name="pTestObject"></param>
        public void ShowMessage(System.String pMessage, System.String pCaption, RunExternal.TestObject pTestObject)
        {
 
            m_Message = pMessage;
 
            m_Caption = pCaption;
 
            System.String String2Show = pMessage + System.Environment.NewLine + pTestObject.ComputerName;
 
            System.Windows.Forms.MessageBox.Show(String2Show, pCaption, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Exclamation);
 
        }  // END public void ShowMessage(System.String pMessage, System.String pCaption, RunExternal.TestObject pTestObject)
 
        /// <summary>
        /// 
        /// </summary>
        /// <param name="pTestObject"></param>
        public void ShowMessage2(RunExternal.TestObject pTestObject)
        {
            if ((m_Message.Length > 0) & (m_Caption.Length > 0))
            {
                System.String String2Show = m_Message + System.Environment.NewLine + pTestObject.ComputerName;
 
                System.Windows.Forms.MessageBox.Show(String2Show, m_Caption, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Exclamation);
            }
            else
            {
                throw new System.ArgumentException("Message and/or caption missing.");
            }
        }  // END public void ShowMessage2(RunExternal.TestObject pTestObject)
 
        /// <summary>
        /// 
        /// </summary>
        public System.String Message2Show
        {
            get
            {
                return m_Message;
            }
        }
 
        /// <summary>
        /// 
        /// </summary>
        public System.String Caption
        {
            get
            {
                return m_Caption;
            }
            set
            {
                m_Caption = value;
            }
        }
 
    }  // END public class JJScriptTest
 
}  // namespace JeffJones.ExternalCode
Colourised in 42ms

I read the code in and go line by line using the CodeFile.cs class instance method, "SetAndCompileCSCode()". In that method, I pull out the "using" declarations to get the assembly references, and do not include those lines in the code to compile. For this exercise, I look for the first class declaration to get a "main class" name (a value required by the compiler) and use it. A refinement of the code might find all class names to allow the user to choose the main class for compiling.

The CodeProvider Object

In the CodeDOMProcessor.cs class, the Microsoft.CSharp.CSharpCodeProvider object (DOMProvider) is used to compile the code. The System.CodeDom.Compiler.CompilerParameters object (DOMCompilerParams) is used with the CSharpCodeProvider object to provide the referenced assemblies (the "using" declarations at the top of the external code). The constructor for the CSharpCodeProvider takes a dictionary that allows the programmer to specify the version of the .NET framework to use. For this example project, .NET 4.0 is used.

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">DOMProviderOptions = new Dictionary<String, String>();
 
DOMProviderOptions.Add(COMPILER_VERSION_KEY, COMPILER_VERSION_SUPPORTED);
 
// Could use Microsoft.VisualBasic.VBCodeProvider for VB.NET code
// The Dictionary specifies the compiler version. 
DOMProvider = new CSharpCodeProvider(DOMProviderOptions);
Colourised in 1ms

Getting Referenced Assemblies

Having already parsed out the assemblies from the code, it is time to add those assemblies to the compiler. Remember, the references that are not in the GAC must be a fully qualified file name to the assembly file. The code below shoes how the assemblies are added.

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// Could use Microsoft.VisualBasic.VBCodeProvider for VB.NET code
// The Dictionary specifies the compiler version. 
DOMProvider = new CSharpCodeProvider(DOMProviderOptions);
 
// Add referenced assemblies to the provider parameters
DOMCompilerParams = new CompilerParameters();
 
if (pReferencedAssemblies != null)
{
    if (pReferencedAssemblies.Count > 0)
    {
        foreach (String RefAssembly in pReferencedAssemblies)
        {
            if (RefAssembly != null)
            {
                if (RefAssembly.Length > 0)
                {
                    DOMCompilerParams.ReferencedAssemblies.Add(RefAssembly);
                } // END if (File.Exists(pExecutableFullPath))
                else
                {
                    ReturnVal.Add(String.Format("A reference file was empty.{0}", Environment.NewLine));
                }
            }  // END if (pExecutableFullPath.Length > 0)
            else
            {
                ReturnVal.Add(String.Format("A reference file was null.{0}", Environment.NewLine));
            }
 
        }  // END foreach (String RefAssembly in pReferencedAssemblies)
 
    }  // END if (pReferencedAssemblies.Count > 0)
 
} // END if (pReferencedAssemblies != null)
Colourised in 14ms

What if the code misses a needed reference? You can now check to see references you know are needed are present, and if not, add them.

 <pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// These references will always be there to support the code compiling
// If these are not found, be sure to add them.
// Note references are in the form of the file name.
// If the reference is not in the GAC, you must supply the fully
// qualified file name of the assembly, such as C:\SomeFiles\MyDLL.dll.
if (!DOMCompilerParams.ReferencedAssemblies.Contains("System.dll"))
{
    DOMCompilerParams.ReferencedAssemblies.Add("System.dll");
}
 
if (!DOMCompilerParams.ReferencedAssemblies.Contains("System.Windows.Forms.dll"))
{
    DOMCompilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");
}
 
if (!DOMCompilerParams.ReferencedAssemblies.Contains("System.Runtime.Serialization.dll"))
{
    DOMCompilerParams.ReferencedAssemblies.Add("System.Runtime.Serialization.dll");
}
Colourised in 5ms

And finally, add the reference of the external code itself.

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// Adds this executable so it self-references.
DOMCompilerParams.ReferencedAssemblies.Add(System.Reflection.Assembly.GetEntryAssembly().Location);
Colourised in 1ms

Now that the compiler is selected, the framework is chosen, and the referenced assemblies added, the destination for the compiled code should be selected.

In Memory or a DLL?

The compiler can generate the results in memory or create a file. You can set compiler options, choose to include debug information, and specify the main class. The code shown below uses an in-memory choice (which oddly enough has a file name).

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// For this example, I am generating the DLL in memory, but you could
// also create a DLL that gets reused.
DOMCompilerParams.GenerateInMemory = true;
DOMCompilerParams.GenerateExecutable = false;
DOMCompilerParams.CompilerOptions = "/optimize";
DOMCompilerParams.IncludeDebugInformation = true;
DOMCompilerParams.MainClass = pMainClassName;
 
// Compile the code.
CompilerResults CompileResults = DOMProvider.CompileAssemblyFromSource(DOMCompilerParams, pCodeToCompile);
Colourised in 3ms

The System.CodeDom.Compiler.CompilerResults instance contains the results of compiling, which we examine below.

Compiling and the Results

The CompilerResults instance that is returned has an Errors collection. You can iterate through the collection to find the errors that prevented successful compilation. The CompileError child object in the collection gives the line and column number for each error. Note that when counting line numbers, comment lines do not get counted in the compiler's line count.

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">if (CompileResults.Errors.Count != 0)
{
 
    foreach (CompilerError oErr in CompileResults.Errors)
    {
        ReturnVal.Add(String.Format("Error# [{0}] - [{1}] Line# [{2}] Column# [{3}].{4}",
                        oErr.ErrorNumber.ToString(), oErr.ErrorText, oErr.Line.ToString(),
                        oErr.Column.ToString(), Environment.NewLine));
 
    }  // END foreach (CompilerError oErr in CompileResults.Errors)
 
}  // END if (CompileResults.Errors.Count != 0)
Colourised in 6ms

You could keep a separate List<String> of code lines the way the compiler does if you wanted and take the line number and column number to show the user the exact location of the error.

Hi Good Looking Code -Tell Me About Yourself

The compiler has much to tell you about what the code represents. This information is useful for making decisions about using the external code, or giving the user choices about how to use it. You can get information on:

  • Assemblies
  • Constructors and Parameters
  • Members (including Default Members)
  • Fields
  • Methods, Return Values, and Parameters

The code below shows ho this example uses the objects to glean information on the object represented by the code. You could create and populate class instances that contain information gleaned from constructors, methods, members, properties, etc. and their parameters instead of passing back these generalized descriptions.

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">Type[] ObjectTypes = CompileResults.CompiledAssembly.GetTypes();
 
// List<String> pMembers, List<String> pFields, List<String> pMethods, List<String> pProperties
if (ObjectTypes.Length > 0)
{
    pFullTypeName = ObjectTypes[0].FullName;
 
    Object CompiledObject = CompileResults.CompiledAssembly.CreateInstance(pFullTypeName);
 
    Type CompiledType = CompiledObject.GetType();
 
    pModuleName = CompiledType.Module.ScopeName;
 
    // Beginning here, you could create and populate class instances that
    // contain information gleaned from constructors, methods, members, 
    // properties, etc. and their parameters instead of passing back these 
    // generalized descriptions.
    ConstructorInfo[] TempConstructors = CompiledType.GetConstructors();
 
    foreach (ConstructorInfo TempConstructor in TempConstructors)
    {
        String StringToAdd = "";
 
        if (TempConstructor.Name == ".ctor")
        {
            StringToAdd = "void " + ObjectTypes[0].Name;
        }
        else
        {
            StringToAdd = "void " + TempConstructor.Name;
        }
 
        String ParmString = "";
 
        if (TempConstructor.Module.ScopeName.Equals(pModuleName))
        {
 
            ParameterInfo[] TempConstructorParam = TempConstructor.GetParameters();
 
            foreach (ParameterInfo TempParam in TempConstructorParam)
            {
                ParmString += String.Format("{0} {1}, ", TempParam.ParameterType.FullName, TempParam.Name);
            }
        }
 
        StringToAdd += "(" + ParmString + ")";
 
        pConstructors.Add(StringToAdd);
    }
 
    MemberInfo[] TempDefaultMembers = CompiledType.GetDefaultMembers();
 
    // List<String> pFields, List<String> pMethods, List<String> pProperties
    if (TempDefaultMembers.Length > 0)
    {
 
        foreach (MemberInfo TempMember in TempDefaultMembers)
        {
            if (TempMember.Module.ScopeName.Equals(pModuleName))
            {
                String StringToAdd = "";
 
                StringToAdd = String.Format("{0} {1}, ", TempMember.ReflectedType.FullName, TempMember.Name);
 
                pMembers.Add(StringToAdd);
            }  // END if (TempMember.Module.ScopeName.Equals(pModuleName))
 
        }  // END if (TempDefaultMembers.Length > 0)
 
    }  // END if (TempDefaultMembers.Length > 0)
 
    FieldInfo[] TempFields = CompiledType.GetFields();
 
    // List<String> pFields, List<String> pMethods, List<String> pProperties
    if (TempFields.Length > 0)
    {
        foreach (FieldInfo TempField in TempFields)
        {
            if (TempField.Module.ScopeName.Equals(pModuleName))
            {
 
                String StringToAdd = "";
 
                StringToAdd = String.Format("{0} {1}, ", TempField.ReflectedType.FullName, TempField.Name);
 
                pFields.Add(StringToAdd);
            }  // END if (TempField.Module.ScopeName.Equals(pModuleName))
 
        }  // END foreach (FieldInfo TempField in TempFields)
 
    }  // END if (TempFields.Length > 0)
 
 
    MemberInfo[] TempMembers = CompiledType.GetMembers();
 
    // List<String> pProperties
    if (TempMembers.Length > 0)
    {
 
        foreach (MemberInfo TempMember in TempMembers)
        {
 
            if (TempMember.Module.ScopeName.Equals(pModuleName))
            {
                String StringToAdd = "";
 
                StringToAdd = TempMember.ToString();  // String.Format("{0} {1}, ", TempMember.GetType().FullName, TempMember.Name);
 
                pMembers.Add(StringToAdd);
            }
        }  // END if (TempDefaultMembers.Length > 0)
 
    }  // END if (TempDefaultMembers.Length > 0)
 
 
    MethodInfo[] TempMethods = CompiledType.GetMethods();
 
    foreach (MethodInfo TempMethod in TempMethods)
    {
 
        if ((TempMethod.Module.ScopeName.Equals(pModuleName)) && (!TempMethod.IsSpecialName))
        {
            String StringToAdd = "";
 
            StringToAdd = String.Format("{0} {1}, ", TempMethod.ReturnType.FullName, TempMethod.Name);
 
            ParameterInfo[] TempParams = TempMethod.GetParameters();
 
            String ParmString = "";
 
            foreach (ParameterInfo TempParam in TempParams)
            {
                String ParamName = TempParam.Name;
                String ParamTypeName = TempParam.ParameterType.FullName;
                Object DefaultValue = TempParam.DefaultValue;
 
                if (DefaultValue.ToString().Length == 0)
                {
                    ParmString += String.Format("{0} {1}, ", ParamTypeName, ParamName);
                }
                else
                {
                    ParmString += String.Format("{0} {1}={2}, ", ParamTypeName, ParamName, DefaultValue.ToString());
 
                }
            }  // END foreach (ParameterInfo TempParam in TempParams)
 
            if (ParmString.EndsWith(", "))
            {
                ParmString = ParmString.Substring(0, ParmString.Length - 2);
            }
 
            StringToAdd += "(" + ParmString + ")";
 
            pMethods.Add(StringToAdd);
 
        }  // END if (TempMethod.Module.ScopeName.Equals(pModuleName))
 
    }  // END foreach (MethodInfo TempMethod in TempMethods)
 
 
    PropertyInfo[] TempProperties = CompiledType.GetProperties();
 
    // List<String> pProperties
    if (TempProperties.Length > 0)
    {
 
        foreach (PropertyInfo TempProperty in TempProperties)
        {
            if (TempProperty.Module.ScopeName.Equals(pModuleName))
            {
                String StringToAdd = "";
 
                StringToAdd = String.Format("{0} {1}, ", TempProperty.PropertyType.FullName, TempProperty.Name);
 
                if (TempProperty.CanRead && TempProperty.CanWrite)
                {
                    StringToAdd += " (get/set)";
                }
                else if (!TempProperty.CanRead && TempProperty.CanWrite)
                {
                    StringToAdd += " (set ONLY)";
                }
                else if (TempProperty.CanRead && !TempProperty.CanWrite)
                {
                    StringToAdd += " (get ONLY)";
                }
                else
                {
                // No action
                }
 
                pProperties.Add(StringToAdd);
            }  // END if (TempProperty.Module.ScopeName.Equals(pModuleName))
 
        }  // END if (TempDefaultMembers.Length > 0)
 
    }  // END if (TempDefaultMembers.Length > 0)
 
} // END if (ObjectTypes.Length > 0)
Colourised in 113ms

Here are what the results look like displayed in the sample app:

Executing the Code

Once you have successfully compiled the code, all that is left is to execute it. That last part is quite easy. You create the instance of the compiled assembly, get a reference to the method to execute, then invoke the method with an Object array of parameters, and capture the return value. The code below shows how that is done for this example. For the example, there is only one assembly. An external file could have more.

<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">Type[] ObjectTypes = CompileResults.CompiledAssembly.GetTypes();
 
if (ObjectTypes.Length > 0)
{
    String FullTypeName = ObjectTypes[0].FullName;
 
    Object CompiledObject = CompileResults.CompiledAssembly.CreateInstance(FullTypeName);
 
    MethodInfo CompiledMethod = CompiledObject.GetType().GetMethod(pExecutionMethodName);
 
    Object ReturnValue = CompiledMethod.Invoke(CompiledObject, pMethodParameters);
 
} // END if (ObjectTypes.Length > 0)
Colourised in 6ms

Now you have external, uncompiled C# code that was loaded, compiled, and run from a C# program.

What About Security?

If the developer is the only one providing and maintaining the script, clearly using a DLL that is "late bound" or read by reflection is the better route as compared to an editable script. However, if the scripts are created by, or maintained by, someone other than developers (such as support teams or others who may know C# or VB.NET well enough to maintain a script), then ensuring a malicious script is prevented becomes important. That type of scenario is the context of this section on security.

A reasonable and prudent question might be, "How do I keep someone from supplying a malicious script and my program getting blamed for the damage it does?"

There are several approaches, one of which I will cover here.

The Checksum Method - When you are supplying the script, you can checksum each line, plus a secret string only your program knows. The first line of your code is a comment with the total checksum value you calculated when the script was made. If the checksum the program calculates when reading in the code (less that first line) matches the value in the file, the file is valid. If not, the code is not compiled nor executed.

Conclusion

When you think of the versatility offered by being able to create or provide scripts for a program you have written, this capability offers a new way to extend your application.

Points of Interest

This also works with VB.NET code by using the Microsoft.VisualBasic.VBCodeProvider to process the VB.NET code.

History

When Who What
======= == ======================================================
05/08/2014 JDJ Genesis.
05/09/2014 JDJ Update an expansion of text.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here