Try fast search NHibernate

13 August 2011

Parse string as Razor template

For a special requirement at work (actually a mix with self requirement) I need to get a chunk of HTML content from a persistence-system. Taken as is, it does not appear a big challenge but analyzing the problem a little bit more deeper I saw that that the HTML chunk may contain variables, it may need a model… to be short it may be so complex as a MVC-PartialView.

To achieve the target quickly I tried to find something in the NET but without so much lucky. I have found a framework that seems to do the work @razorengine but, after a quick look at the code, I didn’t feel so enthusiast especially for the use we have to give to this new feature of our application (web application in Azure with something high traffic). hmm…. so ? what I can do ?

Personally I think that the MVC team in Microsoft is one of the best teams Microsoft ever had and I’m pretty sure there should be a way to parse a string using Razor without so much static classes… here we go with the Saturday proof of concept.

The code will talk by itself…

The VisualStudio solution

StringAsRazorSolution

The base class

The beating heart of Razor is inside System.Web.Razor but to use it you need a class without any special reference, just some “special” methods.

namespace YourCompany.DynamicContents
{
    public abstract class DynamicContentGeneratorBase
    {
        private StringBuilder buffer;
        protected DynamicContentGeneratorBase()
        {
            DynModel = new ExpandoObject();
        }

        /// <summary>
        /// This is just a custom property
        /// </summary>
        public dynamic DynModel { get; set; }

        /// <summary>
        /// This method is required and have to be exactly as declared here.
        /// </summary>
        public abstract void Execute();

        /// <summary>
        /// This method is required and can be public but have to have exactly the same signature
        /// </summary>
        protected void Write(object value)
        {
            WriteLiteral(value);
        }

        /// <summary>
        /// This method is required and can be public but have to have exactly the same signature
        /// </summary>
        protected void WriteLiteral(object value)
        {
            buffer.Append(value);
        }

        /// <summary>
        /// This method is just to have the rendered content without call Execute.
        /// </summary>
        /// <returns>The rendered content.</returns>
        public string GetContent()
        {
            buffer = new StringBuilder(1024);
            Execute();
            return buffer.ToString();
        }
    }
}

Surprised ? yes!!… that is all you need.

The concept

With the System.Web.Razor.RazorTemplateEngine you can generate the source-code of concrete classes inherited from your abstract DynamicContentGeneratorBase. The generated code will have the concrete implementation of the abstract method Execute with the calls to the methods Write and WriteLiteral as required by your string. In practice giving a simple string as "<b>Well done @DynModel.Who !!</b>" the implementation of Execute, generated by Razor, will look like:

public override void Execute()
{
    WriteLiteral("<b>Well done ");
    Write(DynModel.Who);
    WriteLiteral(" !!</b>");
}

To use the generated source you have to compile it in an assembly and create an instance of the concrete class.

Usage

  1. static void Main(string[] args)
  2. {
  3.     const string dynamicallyGeneratedClassName = "DynamicContentTemplate";
  4.     const string namespaceForDynamicClasses = "YourCompany";
  5.     const string dynamicClassFullName = namespaceForDynamicClasses + "." + dynamicallyGeneratedClassName;
  6.  
  7.     var language = new CSharpRazorCodeLanguage();
  8.     var host = new RazorEngineHost(language)
  9.                   {
  10.                                 DefaultBaseClass = typeof(DynamicContentGeneratorBase).FullName,
  11.                                 DefaultClassName = dynamicallyGeneratedClassName,
  12.                                 DefaultNamespace = namespaceForDynamicClasses,
  13.                   };
  14.     host.NamespaceImports.Add("System");
  15.     host.NamespaceImports.Add("System.Dynamic");
  16.     host.NamespaceImports.Add("System.Text");
  17.     var engine = new RazorTemplateEngine(host);
  18.         
  19.     var tr = new StringReader(GetStringFromSomewhere()); // here is where the string come in place
  20.     GeneratorResults razorTemplate = engine.GenerateCode(tr);
  21.  
  22.     var compiledAssembly = CreateCompiledAssemblyFor(razorTemplate.GeneratedCode);
  23.  
  24.     var templateInstance = (DynamicContentGeneratorBase)compiledAssembly.CreateInstance(dynamicClassFullName);
  25.  
  26.     templateInstance.DynModel.Who = "Fabio";
  27.         
  28.     Console.WriteLine(templateInstance.GetContent());
  29.     Console.ReadLine();
  30. }
  31.  
  32. private static Assembly CreateCompiledAssemblyFor(CodeCompileUnit unitToCompile)
  33. {
  34.     var compilerParameters = new CompilerParameters();
  35.     compilerParameters.ReferencedAssemblies.Add("System.dll");
  36.     compilerParameters.ReferencedAssemblies.Add("Microsoft.CSharp.dll");
  37.     compilerParameters.ReferencedAssemblies.Add("System.Core.dll");
  38.     compilerParameters.ReferencedAssemblies.Add(typeof(DynamicContentGeneratorBase).Assembly.Location);
  39.     compilerParameters.GenerateInMemory = true;
  40.  
  41.     CompilerResults compilerResults = new CSharpCodeProvider().CompileAssemblyFromDom(compilerParameters, unitToCompile);
  42.     Assembly compiledAssembly = compilerResults.CompiledAssembly;
  43.     return compiledAssembly;
  44. }
  45.  
  46. private static string GetStringFromSomewhere()
  47. {
  48.     return "<b>Well done @DynModel.Who !!</b>";
  49. }

Form line 7 to 20 I’m generating the source of a concrete class called YourCompany.DynamicContentTemplate using the Razor template engine, then I’m compiling the source in an in-memory assembly, at line 24 I’m creating an instance of the YourCompany.DynamicContentTemplate, at line 25 I’m assigning a value to my DynamicModel and the result is:

StringAsRazorResult

Winking smile

9 comments:

  1. You better go check the Spark View Engine, has a powerful parser and a compiler, you can create your own tags (spark bindings) and so on.

    ReplyDelete
  2. Imho and without running a test I think your code has a threading issue when calling templateInstance.GetContent() on multiple threads..

    ReplyDelete
  3. 708 milliseconds to execute this:
    DynamicContentGeneratorBase templateInstance2;
    for (int i = 0; i < 1000000; i++)
    {
    templateInstance2 = ctors[1]();
    templateInstance2.DynModel.Guys = new[] { "Fabio", "El tano" };
    var p = templateInstance2.GetContent();
    }
    I don't need a single instance, ergo each thread will have its instance, ergo no threading issue on multiple threads ;)

    ReplyDelete
  4. FYI this is the code executed 1000000 times in 708 milliseconds:

    ul
    @foreach(var item in DynModel.Guys)
    {
    li@itemli
    }
    ul

    ReplyDelete
  5. Hola Fabio, estoy usando el unhaddins + nhibernate y tengo una pregunta acerca de mi implementaciĆ³n del repositorio.

    ¿quĆ© mejor canal para hacer una pregunta sobre este tema?

    gracias

    ReplyDelete
  6. check out razorgenerator

    we use it for unit testing our views http://razorgenerator.codeplex.com/

    ReplyDelete
  7. I like your writeup, thanks for the explanations!

    ReplyDelete
  8. Estamos en el 2020 y pasaron varias versiones de Razor y en varias apps y este codigo sigue siendo el mismo y sigue enviando miles de mails a diario. Hay veces que me impresionan estas cosas.

    ReplyDelete