Click here to Skip to main content
Click here to Skip to main content

How to Execute External Uncompiled C# Code

, 12 May 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
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); 

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

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);

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)

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");
}

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);

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);

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)

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)

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)

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

MSBassSinger
Software Developer (Senior)
United States United States
I have been in technology in one form or another since 1973 when I became a part of the U. S. Navy's Nuclear Power Program.
 
I started programming at Florida Atlantic University (FORTRAN IV) in 1979, and worked on computerized control and automation systems for heating and air.
 
I migrated to COBOL, some proprietary machine code, Clipper, QuickBasic, Visual Basic, and finally landing on C# with the occasional VB.NET. Twice in my career I delved into Java. Once back in the late 1990s, only to find it was just not ready for primetime on the Windows platform, compared to VB5 and 6. In 2010, I took two semesters of Java for my BS degree, made A's, but just didn't find the love for it.
 
My experience is in database design, software architecture and engineering, project management, and some experience in sales and marketing.
 
I finished my MBA (Masters in Business Administration)in March of 2013 with a specialization in Technology Management. Experience has taught me that one needs to understand the business side to really make the technology side work better.
Follow on   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberfredatcodeproject12-May-14 12:13 
QuestionVery good article! PinprofessionalVolynsky Alex12-May-14 9:03 
SuggestionGood, but can be better PinmemberPascal-7812-May-14 0:40 
GeneralRe: Good, but can be better PinprofessionalMSBassSinger12-May-14 6:22 
QuestionThanks Pinmembera_pess11-May-14 23:52 
AnswerRe: Thanks PinprofessionalMSBassSinger12-May-14 6:07 
GeneralRe: Thanks PinmemberPascal-7812-May-14 7:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 12 May 2014
Article Copyright 2014 by MSBassSinger
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid