ASP.NET 2.0 - Safely Compile And Execute Source Code Dynamically

By Robbe D. Morris

Printer Friendly Version

Robbe Morris
Robbe & Melisa Morris
  Download C# Source Code
You've probably run across all sorts of code samples for dynamically compiling and running source code in .NET 2.0.  Fortunately, this is fairly easy in .NET.  What I haven't yet seen covered is a safe way to do this in an ASP.NET environment.  The biggest problem you run into is that each time you compile your code (even if you use the exact same attributes and settings) is that a new duplicate assembly is loaded into memory.  So, you could easily wind up with thousands and thousands of duplicate assemblies floating around in memory on your server because there is no facility in .NET 2.0 to unload an assembly from memory.
I am currently working on an application that permits the creation and implementation of business rules at runtime to perform such tasks as data processing, validation, navigation, and calculation rules.  In fact, we support user specific compiled rules in some rare instances.  The requirements are far too complex to attempt to make these rules entirely database driven.  In the end, actual C# compiled code is required.


With no way to unload an assembly from memory via the framework, I put together a fairly simple versioning technique based on establishing a unique key for each rule based method to avoid unnecessary code compilations.  Thus, drastically reducing the number of assemblies loaded into memory.  The SourceCodeManager source code below performs the following tasks:
 
1.Creates a static Generic List of CodeContainer classes.  Each CodeContainer class holds properties about the compiled code as well as a reference to a compiled System.Reflection.Assembly class.  This List<CodeContainer> is defined as static and resides in the Registration class.  Each CodeContainer in the list must have a UniqueKey assigned to it.  The Registration class uses this key to find the desired instance of the Assembly.
2.The Registration.Update method ensures that the current Assembly is loaded to the desired CodeContainer class.  It does this by evaluating the source code passed in with the source code already in our static List.
If the source code has not already been loaded to the List, code is compiled, the CodeContainer is added to the List, and the CodeContainer.CompiledAssembly property is set.
If the source code had been previous loaded, it is compared with the passed in source code.  If they are different, the old CodeContainer is removed from the list and a new one with the newly compiled code is added to the List.
Regardless of the current state of the CodeContainer.CompiledAssembly, the .Update() method ensures that the current assembly reference is always set to the .CompiledAssembly property.
3.Compiles source code in a centralized way to better control what references are included.  Also provides you with a way to add your own code access security or security policy settings for your own implementation.
4.Makes a simple assembly diagnostic report available.  Allows you to monitor how many of your assemblies are loaded into memory.
 
The sample code zip file above contains the SourceCodeManager project along with a sample ASP.NET application.  You'll be able to run/tweak the sample code and watch how it manages versioning as well as be able to see when a new assembly is loaded into memory and when the current one is used.
You'll also want to experiment with this in a true server environment.  You'll need to understand the consequences of how often you recycle your application in IIS 6.0.  Keep in mind, each time your application is recycled, all of the previously loaded assemblies are removed from memory.
This code sample utilizes MethodInfo.Invoke assuming that your rules are so dynamic that you have no better alternative.  That said, this is one of the slowest (if not the slowest methods) in regards to performance.
In my real world application, I'll be using System.Guid values for my namespace and class names (you'll have to strip them of non alpha numeric characters and make sure you prefix the name with an alpha or it won't compile) and then use the same single method name in each class.  I wind up with hundreds of classes that all have the same single method.
This enables me to use an Interface at runtime against all of my dynamic namespaces and classes.  I'll then store a reference to all of my Interface instances on the CodeContainer instead of method names (as this sample does) at compile time.  This will dramatically improve your overall performance when the class methods are executed because you wouldn't be using reflection at that point.  You are restricting your utilization of reflection to compilation only.
Let's take a look at the code:
 
CodeContainer.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace SourceCodeManager
{
    public class CodeContainer
    {
 
        private string sourceCode = "";
        private string uniqueKey = "";
        private string nameSpace = "";
        private string className = "";
        private System.Reflection.Assembly compiledAssembly = null;

        public string UniqueKey
        {
            get
            {
                return uniqueKey;
            }
            set
            {
                if (value != uniqueKey)
                {
                    uniqueKey = value.Trim();
                }
            }
        }
 
        public string SourceCode
        {
            get
            {
                return sourceCode;
            }
            set
            {
                if (value != sourceCode)
                {
                    sourceCode = value.Trim();
                }
            }
        }
 
        public string NameSpace
        {
            get
            {
                return nameSpace;
            }
            set
            {
                if (value != nameSpace)
                {
                    nameSpace = value.Trim();
                }
            }
        }
 
        public string ClassName
        {
            get
            {
                return className;
            }
            set
            {
                if (value != className)
                {
                    className = value.Trim();
                }
            }
        }
 
        public System.Reflection.Assembly CompiledAssembly
        {
            get
            {
                return compiledAssembly;
            }
            set
            {
 
                compiledAssembly = value;
            }
        }
        #endregion

    }
} 
 
Compiler.cs
using System;
using System.Data;
using System.Configuration;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Text;
using System.Reflection;
using System.Diagnostics;
using System.Security.Policy;
using System.IO;
using System.Web;

namespace SourceCodeManager
{
 /// <summary>
 /// Summary description for Compiler
 /// </summary>
 public class Compiler
 {
    private CompilerErrorCollection compilerErrors = null;
 
    public Compiler()
    {
        compilerErrors = new CompilerErrorCollection();
    }
 
    public CompilerErrorCollection Errors
    {
        get { return compilerErrors; }
    }
 
     public System.Reflection.Assembly Compile(string sourceCode)
     {
         CSharpCodeProvider provider = new CSharpCodeProvider();
         CompilerParameters parameters = new CompilerParameters();
         CompilerResults results = null;
         StringBuilder sb = new StringBuilder();

         try
         {

             parameters.OutputAssembly = "SourceCodeManager";    
             parameters.ReferencedAssemblies.Add("system.dll");
             parameters.ReferencedAssemblies.Add("system.xml.dll");
             parameters.ReferencedAssemblies.Add("system.data.dll");
             parameters.ReferencedAssemblies.Add("system.web.dll");
             parameters.CompilerOptions = "/t:library";
             parameters.GenerateInMemory = true;
             parameters.GenerateExecutable = false;
             parameters.IncludeDebugInformation = false;

             sb.Append("using System;" + "\n");
             sb.Append("using System.Collections.Generic;" + "\n\n");
             sb.Append("using System.Xml;" + "\n");
             sb.Append("using System.Data;" + "\n\n");
             sb.Append("using System.Web;" + "\n\n");

             sb.Append(sourceCode);
           
             results = provider.CompileAssemblyFromSource(parameters,
                                                          sb.ToString());

             if (results.Errors.Count != 0)
             {
                 compilerErrors = results.Errors;
                 throw new Exception("Code compilation errors occurred.");
             }
             else
             {
                 return results.CompiledAssembly; 
             }

         }
         catch (Exception) { throw; }
        
     }
    
  }

}
  
 
Runtime.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Reflection;
using System.Diagnostics;

namespace SourceCodeManager
{
    public class Runtime
    {
 
        /// <summary>
        /// Assumes that codeContainer.CompiledAssembly is populated
        /// via the Registration.Update method.
        /// This method will execute the desired classname.method with
        /// the parameters passed in and return an object.
        /// </summary>
        public object Execute(CodeContainer codeContainer,
                              string methodName,
                              object[] parameters)
        {

            object assemblyInstance = null;
            MethodInfo methodInformation = null;
            Type type = null;
            string qualifiedClassName = "";

            try
            {

                qualifiedClassName += codeContainer.NameSpace + ".";
                qualifiedClassName += codeContainer.ClassName;
                assemblyInstance = codeContainer.CompiledAssembly.CreateInstance(
                                                                  qualifiedClassName,
                                                                  false
                                                                  );
                type = assemblyInstance.GetType();
                methodInformation = type.GetMethod(methodName);
                return methodInformation.Invoke(assemblyInstance, parameters);

            }
            catch (Exception) { throw; }
            return null;

        }
       

    }
} 
 
Registration
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics; 

namespace SourceCodeManager
{
    public class Registration
    {
        public static List<CodeContainer> CodeContainers = new List<CodeContainer>();
 
        /// <summary>
        /// Manages the static List CodeContainers.   
        /// Compare this code with that which is set in
        /// our static List by the UniqueKey.
        /// If not found, compile and add.
        /// If found but source code is different, delete, compile, and add.
        /// If found and source code is the same, grab reference to
        /// previously compiled assembly.
        /// The codeContainer passed in will always have its .CompiledAssembly
        /// property populated after this method (Update) is called.
        /// </summary>
        public static void Update(CodeContainer codeContainer)
        {
            CodeContainer existingCode = null;
            SourceCodeManager.Compiler compiler = null;
            System.Reflection.Assembly assembly = null;

            try
            {

                existingCode = GetCodeContainer(codeContainer.UniqueKey);

                if (existingCode != null)
                {
                    if (existingCode.SourceCode != codeContainer.SourceCode)
                    {
                        Delete(existingCode);
                        existingCode = null;
                    }
                }

                if (existingCode == null)
                {
                    compiler = new SourceCodeManager.Compiler();
                    assembly = compiler.Compile(codeContainer.SourceCode);
                    codeContainer.CompiledAssembly = assembly;
                    CodeContainers.Add(codeContainer);
                    return;
                }

                codeContainer.ClassName = existingCode.ClassName;
                codeContainer.NameSpace = existingCode.NameSpace;
                codeContainer.CompiledAssembly = existingCode.CompiledAssembly;
              
            }
            catch (Exception) { throw; }
        }
     
        /// <summary>
        /// Remove this code container object from our static List.
        /// </summary>
        public static void Delete(CodeContainer code)
        {
            try
            {
                code.CompiledAssembly = null;
                CodeContainers.Remove(code);
            }
            catch (Exception) { throw; }
        }
 
        /// <summary>
        /// Get a reference to the static List CodeContainer object
        /// via its UniqueKey property.
        /// </summary>
        public static CodeContainer GetCodeContainer(string uniqueKey)
        {
            try
            {

                return CodeContainers.Find(delegate(CodeContainer record)
                             {
                                 return int.Equals(record.UniqueKey,
                                                   uniqueKey);
         
                             });

            }
            catch (Exception) { throw; }
        }
    
    }
} 
 
Diagnostics.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.IO;

namespace SourceCodeManager
{
    public class Diagnostics
    {

 
        /// <summary>
        /// Simple Html output of assemblies currently loaded into
        /// the application domain.
        /// </summary>
        public static string GetReportInHtmlTableFormat(string serverIPAddress,
                                                        bool showSystemNameSpaces)
        {
            StringBuilder sb = new StringBuilder();
            System.Reflection.Assembly[] assemblies = null;

            try
            {
                 
                sb.Append("<table border=\"0\" cellpadding=\"1\" cellspacing=\"1\">");
                sb.Append("<tr>");
                sb.Append("<td align=\"left\" width=\"50\">");
                sb.Append("<b>Server</b>");
                sb.Append("</td>");
                sb.Append("<td align=\"left\"><b>");
                sb.Append(serverIPAddress);
                sb.Append("</b></td>");
                sb.Append("</tr>");

                assemblies = System.AppDomain.CurrentDomain.GetAssemblies();

                for (int i = 0; i < assemblies.Length; i++)
                {
                    if (!showSystemNameSpaces)
                    {
                        if (assemblies[i].FullName.ToLower().StartsWith("system."))
                        {
                            continue;
                        }
                    }
                    sb.Append("<tr>");
                    sb.Append("<td align=\"right\" width=\"30\" nowrap>");
                    sb.Append(i.ToString() + ".");
                    sb.Append("</td>");
                    sb.Append("<td align=\"left\">");
                    sb.Append(assemblies[i].FullName);
                    sb.Append("</td>");
                    sb.Append("</tr>");
                }

                sb.Append("</table>");
                

            }
            catch (Exception) { throw; }
            return sb.ToString();
        }
      
    }
}
 
 
default.aspx.cs
using System;
using System.Data;
using System.Collections.Generic; 
using System.Configuration;
using System.Diagnostics; 
using System.Web;
using System.Web.Security;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Threading;

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
       
        try
        {

            if (!Page.IsPostBack)
            {

               // Let's write some sample code for use in this demo.
               // As an fyi, it really isn't optimal to perform string
               // concatenation inside the .Append() method.
               // I did it here and elsewhere for readability.  In
               // your code, you'll want to simply add another
               // sb.Append(@"\n"); wherever you want a newline.

               StringBuilder sb = new StringBuilder();

               sb.Append("namespace MyNameSpace" + "\n");
               sb.Append("{ " + "\n");
               sb.Append("   public class MyClassName" + "\n");
               sb.Append("   { " + "\n");
               sb.Append("      public bool Validate(List<int> parameter1," + "\n");
               sb.Append("                           List<int> parameter2) " + "\n");
               sb.Append("      { " + "\n");
               sb.Append("        int i = 5;" + "\n");
               sb.Append("                          " + "\n");
               sb.Append("         try " + "\n");
               sb.Append("         { " + "\n");
               sb.Append("            if (i==1)" + "\n");
               sb.Append("            {" + "\n");
               sb.Append("              return true; " + "\n");
               sb.Append("            }" + "\n");
               sb.Append("            if (parameter1.Count==1)" + "\n");
               sb.Append("            {" + "\n");
               sb.Append("              return true; " + "\n");
               sb.Append("            }" + "\n");
               sb.Append("            return false; " + "\n");
               sb.Append("         } " + "\n");
               sb.Append("         catch (Exception) { throw; }" + "\n");
               sb.Append("         return false; " + "\n");
               sb.Append("      } " + "\n");
               sb.Append("  } " + "\n");
               sb.Append("}" + "\n\n");
  
               this.Label1.Text = ""; 
               this.TextBox1.Text = sb.ToString();

            }
          
        }
        catch (Exception err)
        {
           this.Label1.Text = err.Message;
        }
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
        SourceCodeManager.Runtime runtime = new SourceCodeManager.Runtime();
        SourceCodeManager.CodeContainer codeContainer = null;
        string methodName = "Validate";
        object[] parameters = null;

        try
        {

            // Simulate pulling code from disk or from database.

            codeContainer = new SourceCodeManager.CodeContainer();
            codeContainer.ClassName = "MyClassName";
            codeContainer.NameSpace = "MyNameSpace";
            codeContainer.UniqueKey = "robbe";
            codeContainer.SourceCode = this.TextBox1.Text;

            // Compare this code with that which is set in
            // our static List<CodeContainer> by the UniqueKey.
            // If not found, compile and add.
            // If found but source code is different, delete, compile, and add.
            // If found and source code is the same, grab reference to
            // previously compiled assembly.

            SourceCodeManager.Registration.Update(codeContainer);

            // Let's create some sample parameters to pass into
            // our dynamically compiled class/method.

            parameters = new object[2];

            List<int> parameter1 = new List<int>();

            parameter1.Add(5);
            parameter1.Add(10);

            List<int> parameter2 = new List<int>();

            parameter2.Add(50);
            parameter2.Add(100);

            parameters[0] = parameter1;
            parameters[1] = parameter2;

            // Execute the desired method and pass in our parameters.
            // Our Execute method will always return an object.  So,
            // we need to know its actual desired return type ahead
            // of time if we want to convert it to the property Type.

            bool returnValue = (bool)runtime.Execute(codeContainer,
                                                     methodName,
                                                     parameters);

            this.Label1.Text =  "Method result: " + returnValue.ToString();
                                             

        }
        catch (Exception err)
        {
            this.Label1.Text = err.Message;
        }
        finally { runtime = null; }
    }
    
}
 
 

Robbe has been a Microsoft MVP in C# since 2004.  He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.