Handlebars.Net icon indicating copy to clipboard operation
Handlebars.Net copied to clipboard

Is it possible to precompile tempaltes and/or cache them as binary or C# code (for use in an Azure Function)?

Open IKoshelev opened this issue 5 years ago • 8 comments

I'm looking to use Handlebars.NET in a transient runtime (typical scenario is, http request comes in, runtime is brought up to service it and then shuts down again until next request, which may be next day). Specifically, I'm using Azure Functions. I was wondering how can I precompile templates to avoid costly compilation during environment startup? It would be even nicer if I could produce C# code from them.

IKoshelev avatar Dec 08 '19 16:12 IKoshelev

It is possible using AssemblyBuilder and Expression.CompileToMethod(MethodBuilder). Below is a simple example for how to produce a .dll with a class inside of it that looks like this:

public class TemplateContainer
{
    public static TextWriter MyTemplate(object);
}

So you can import it into your project and invoke it as TemplateContainer.MyTemplate(obj).

(Disclaimer: I wrote this from my phone so it's a guide but haven't tested it, may need some tweaks):

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Linq.Expressions;

//set up the assembly and type definition:
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("PrebuiltTemplates"), AssemblyBuilderAccess.Save);
var moduleBuilder = asmBuilder.DefineDynamicModule("PrebuiltTemplates", "PrebuiltTemplates.dll");
var typeBuilder = moduleBuilder.DefineType("TemplateContainer", TypeAttributes.Public);

//do this for each template:
  //put your actual compilation here
  var myTemplate = Handlebars.Compile(""); 
  //create a lambda that invokes the template
  Expression<Action<TextWriter, object>> templateInvoker = (model) => myTemplate(model);
  //define a named method for the template
  var methodBuilder = typeBuilder.DefineMethod("MyTemplate", MethodAttributes.Static, typeof(TextWriter), new[] { typeof(object) });
  //compile the lambda into the method we defined above
  templateInvoker.CompileToMethod(methodBuilder);

//all done, now save it to disk:
typeBuilder.CreateType();
assemblyBuilder.Save("PrebuiltTemplates.dll");

rexm avatar Dec 11 '19 17:12 rexm

@rexm thank you, I've tried running this code, with some changes (did some guesswork)

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Linq.Expressions;
using HandlebarsDotNet;
using System.IO;
using System.Security.Permissions;

namespace Compiler.Net.Framework
{
    class Program
    {
        private static TextReader GetTemplateReader(string source)
        {
            var stream = new MemoryStream();
            var writer = new StreamWriter(stream);
            writer.Write(source);
            writer.Flush();
            stream.Position = 0;
            var reader = new StreamReader(stream);
            return reader;
        }

        [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
        static void Main(string[] args)
        {
            //set up the assembly and type definition:
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("PrebuiltTemplates"), AssemblyBuilderAccess.Save);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule("PrebuiltTemplates", "PrebuiltTemplates.dll");
            var typeBuilder = moduleBuilder.DefineType("TemplateContainer", TypeAttributes.Public);


            var reader = GetTemplateReader(
 @"<div>
    {{Foo}}
</div>");

            //I had to use a modified version of the library, 
            //which exposes resulting Expression without compiling it to Deleagate
            Expression<Action<TextWriter, object>> myTemplate = Handlebars.CompileExpr(reader);

            //this line could be skipped
            Expression<Action<TextWriter, object>> templateInvoker = myTemplate;
            //define a named method for the template
            var methodBuilder = typeBuilder.DefineMethod("MyTemplate", MethodAttributes.Static, typeof(void), new[] { typeof(TextWriter), typeof(object) });

            //compile the lambda into the method we defined above
            //I get an exception here, text below
            templateInvoker.CompileToMethod(methodBuilder);

            //all done, now save it to disk:
            typeBuilder.CreateType();
            assemblyBuilder.Save("PrebuiltTemplates.dll");
        }
    }

    public class ViewModel
    {
        public string Foo { get; set; }
    }
}

Exception I get:

System.InvalidOperationException: 'CompileToMethod cannot compile constant 'HandlebarsDotNet.HtmlEncoder' because it is a non-trivial value, such as a live object. Instead, create an expression tree that can construct this value.'

Debug view of compiled myTemplate

.Lambda #Lambda1<System.Action`2[System.IO.TextWriter,System.Object]>(
    System.IO.TextWriter $buffer,
    System.Object $data) {
    .Block(HandlebarsDotNet.Compiler.BindingContext $context) {
        .If (
            $data .Is HandlebarsDotNet.Compiler.BindingContext
        ) {
            $context = $data .As HandlebarsDotNet.Compiler.BindingContext
        } .Else {
            $context = .New HandlebarsDotNet.Compiler.BindingContext(
                $data,
                .Call HandlebarsDotNet.EncodedTextWriter.From(
                    $buffer,
                    .Constant<HandlebarsDotNet.ITextEncoder>(HandlebarsDotNet.HtmlEncoder)),
                null,
                null,
                null)
        };
        .Call ($context.TextWriter).Write(
            "<div>
    ",
            False);
        .Call ($context.TextWriter).Write(.Call .Constant<HandlebarsDotNet.Compiler.PathBinder>(HandlebarsDotNet.Compiler.PathBinder).ResolvePath(
                $context,
                "Foo"));
        .Call ($context.TextWriter).Write(
            "
</div>",
            False)
    }
}

IKoshelev avatar Dec 12 '19 20:12 IKoshelev

I see. It looks like we would need to do some more work on the internals to make the expression tree serializable. So this request is more involved than I initially thought.

rexm avatar Dec 12 '19 20:12 rexm

Has there been any progress on this front? I came back to the question of View engine for Azure Functions, and it looks like 2 years later there is still no good options for .NET :-(

IKoshelev avatar Sep 21 '21 11:09 IKoshelev

Hello @IKoshelev As part of v2 a lot of effort was done to make it possible. Now it's possible to fairly easily replace "compiler" as well as do additional expression trees transformations that might be required. However, there's still some work to do. I'd be happy to help if you're interested in investing into this functionality.

oformaniuk avatar Oct 06 '21 05:10 oformaniuk

@zjklee I kind of gave-up on the prospect of using a view-engine in Azure FN, and just stuck to Interpolated strings for now, making a reminder to adjust for all the awesome improvements coming in C# 10 .

In 2019 I sort-of managed to use Razor: I located the C# files that CSHTML compiles into and made a script that surgically copies parts of that C# code to my own C# class that uses stream writer. Had to abstain from using more advanced things like helpers, which weren't much use to me anyway. When I needed a view-engine again, I re-evaluated what I'm actually getting and realized that interpolated strings will work just fine. For a blog project ended-up with something like this:

        public string RenderArticleBody(ArticleForRendering article, bool isPreview = false)
        {
            var continueReadingLink = Routes.GetArticleRelativeUrl(
                article.ArticleMetadata.Id,
                Routes.GetArticleNameEscapedForUrl(article.ArticleMetadata));

            var timestamp = article.ArticleMetadata.Date.ToString("yyyy MMMM dd", Const.UsCulture);

            return
$@"<blog-post>
    <title1>{article.ArticleMetadata.Title1}</title1>
    <title2>{article.ArticleMetadata.Title2}</title2>
    <tags>
        <timestamp>[{timestamp}]</timestamp>
        {article.ArticleMetadata.Tags.Select(tag =>
            @$"<a href=""{Routes.GetTagSearchRelativeUrl(tag)}"">{tag}</a>"
        ).JoinString(", ")}
    </tags>
    <article-content>{(isPreview ? article.Preview : article.Full)}</article-content>
    { isPreview.ThenRender(() => $@" <a href=""{continueReadingLink}"">continue reading</a>") }
    { (!isPreview).ThenRender(() => RenderDisquisSection(article.ArticleMetadata)) }
</blog-post>";
        }

IKoshelev avatar Oct 09 '21 13:10 IKoshelev

Hello @zjklee, I saw your note about replacing 'compiler', with that in mind - I was wondering in .Net core, if with these changes, you had a good solution for precompiling and utilizing the templates in a different way or some other method to improve the performance for us. We have many templates that need to be compiled for a given client.

We have run into issues because .Net core has removed the above AssemblyBuilder.Save method for creating a dll.

Thank you very much!

@markroberts-csi, @supjohn1

mattbruc avatar Feb 24 '22 17:02 mattbruc

@mattbruc , as far as I know there's no build-in alternative at the moment. You may have a look at ILPack as an alternative.

However, you'd still need to implement custom Handlebars compiler. Few pitfalls you may encounter:

  • Generated Expression Tree is not 100% stateless. You'd need to serialize and save the state and inject it afterwards.
  • Expression Tree is not compiled in one go. Some parts of the template are compiled separately (this is done for runtime performance). You'd need to handle this behavior somehow (e.g., create a method in a class and inject method call instead of current delegate call).
    • I had a draft to enable compilation in one go. I may get back to it in case there'd be no other solution.

oformaniuk avatar Mar 04 '22 08:03 oformaniuk