RazorEngine un-cache compiled templates
Asked Answered
I

3

14

Currently, I am using RazorEngine v2.1 as part of a background process that sends templated emails (thousands of them). To speed things up, the templates are compiled with their md5 sum as a name. This makes it so that when a template is changed, it is re-compiled and all emails using the template are able to use the same compiled template. I keep track of the names of the compiled templates in a list so that I know when to call compile again (and do a few other things).

The problem: It has occurred to me that after a really long time and after lots of template modifications that all these cached compiled templates will probably still be in memory since it looks like they are being stored in a dynamic. For this particular process, which might run for months at a time without a restart, this could constitute a serious memory leak if all the previous versions of templates are still hanging around.

The question: Is there a way to un-cache old templates so that they are no longer hanging around in the dynamic?

If, for example, I was able to keep the compiled template objects myself and just pass them into RazorEngine when I wanted to use them I could decide when to toss them out and that would eliminate the memory leak. However, if RazorEngine already has a way around this then knowing about that would be handy as well since I can't find very many references to this particular problem on the internet. There are plenty of things about why compiled templates should be used to reduce memory usage, but I had a hard time finding anything about tons and tons of unused compiled templates accumulating in a long-lived application.

EDIT: I have just read a bit about how the caching works and that if the same name is passed in with a different template it will re-cache it and discard the old one. The problem here still remains, however, since over the course of time emails will be added and removed and with time all the old removed emails will still be around (even though it won't be storing copies of each version of the template).

Impeller answered 16/1, 2013 at 23:14 Comment(0)
P
7

Answering this because it still seems to be relevant for some people. (https://github.com/Antaris/RazorEngine/issues/232#issuecomment-128802285)

For this particular process, which might run for months at a time without a restart, this could constitute a serious memory leak if all the previous versions of templates are still hanging around.

When you change and recompile templates you have a memory leak, because you cannot unload loaded assemblies (which RazorEngine compiles and loads for you in the background).

The only way to really free the memory is to reload the AppDomain or restart the process.

The other answers seem to talk about newer versions which prevent memory leaks in the default configuration (to make you aware of the problem) and require some custom configuration to be able to recompile a key with another template code. Note that all the other answers will actually increase memory consumption!

matthid, a RazorEngine contributor

Photophore answered 7/8, 2015 at 19:36 Comment(0)
I
6

I recently upgraded to the latest stable version of RazorEngine (3.6.1) and my strategy for purging the cache no longer worked due to all the changes. A lot has changed and the documentation for this project is not only out of date but written from an authors perspective making for a poor user experience.

This is my current code for purging all cached templates using the 3.6.1.

public static class TemplateManager
{
    static IRazorEngineService Service { get; set; }
    static TemplateServiceConfiguration Configuration { get; set; }

    static TemplateManager()
    {
        Configuration = new TemplateServiceConfiguration()
        {
            // setting up our custom template manager so we map files on demand
            TemplateManager = new MyTemplateManager()
        };
        Service = RazorEngineService.Create(Configuration);
        Engine.Razor = Service;
    }

    /// <summary>
    /// Resets the cache.
    /// </summary>
    public static void ResetCache()
    {
        Configuration.CachingProvider = new RazorEngine.Templating.DefaultCachingProvider();
    }

    /// <summary>
    /// Compiles, caches and parses a template using RazorEngine.
    /// </summary>
    /// <param name="templateType">Type of the template.</param>
    /// <param name="anonymousType">Type of the anonymous object.</param>
    /// <param name="cachedEnabled">true to enabled caching; false otherwise</param>
    /// <returns></returns>
    public static string GetTemplate<T>(EmailTemplateType templateType, T anonymousType, bool cachedEnabled = true)
    {
        string templateName = templateType.ToString();

        if (cachedEnabled == false)
            ResetCache();

        // pre-compile, cache & parse the template
        return Engine.Razor.RunCompile(templateName, null, anonymousType);
    }
}

public enum EmailTemplateType
{
    ForgotPassword,
    EmailVerification
}

public class MyTemplateManager : ITemplateManager
{
    public ITemplateSource Resolve(ITemplateKey key)
    {
        string file = HttpContext.Current.Server.MapPath(string.Format("~/EmailTemplates/{0}.cshtml", key.Name));
        return new LoadedTemplateSource(System.IO.File.ReadAllText(file), file);
    }

    public ITemplateKey GetKey(string name, ResolveType resolveType, ITemplateKey context)
    {
        return new NameOnlyTemplateKey(name, resolveType, context);
    }

    public void AddDynamic(ITemplateKey key, ITemplateSource source)
    {
        throw new NotImplementedException("dynamic templates are not supported!");
    }
}

This is an example usage of the code in Asp.Net MVC:

var emailBody = TemplateManager.GetTemplate(EmailTemplateType.ForgotPassword, new
{
    SiteUrl = Url.Action(MVC.Home.Index(), protocol: Request.Url.Scheme),
    SiteFriendlyName = SiteSettings.Instance.DomainName.FriendlyName,
    PasswordResetLink = Url.Action(MVC.Account.ActionNames.ResetPassword, MVC.Account.Name, new { userId = user.Id, code = code }, protocol: Request.Url.Scheme),
    NotRequestedUrl = Url.Action(MVC.Account.ActionNames.PasswordResetNotReqeuested, MVC.Account.Name, new { userId = user.Id, requesterIpAddress = WebUtils.GetClientIPAddress(), code = code }, protocol: Request.Url.Scheme)
},
/* this setting allows me to disable caching during development */
!SiteSettings.Instance.EmailSettings.DebugEmailTemplates );

// I could also have a button on an admin page that executed this code to manually reset the cache in production.
TemplateManager.ResetCache();
Ireful answered 3/3, 2015 at 1:57 Comment(3)
My scenario is a bit different, I want to return the template (emailBody) to the user from controller so it will be visible on the page. How can I return that as a view?Osteitis
@tugboatcaptain I tried your solution and getting error An exception of type 'System.ArgumentException' occurred in RazorEngine.dll but was not handled in user code. Additional information: Invalid token for impersonation - it cannot be duplicated.. All i am doing is getting template from database.Eared
Great answer. Thx for the help!!Goodrow
J
2

It seems that RazorEngine stores cache for compiled templates inside TemplateService instance. So you can recreate new instances of TemplateService from time to time to drop all cached templates.

You can also consider using my own library which is based on RazorEngine and implements custom caching mechanism with expiration: http://www.nuget.org/packages/Essential.Templating.Razor

Jolo answered 12/3, 2014 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.