RazorLight icon indicating copy to clipboard operation
RazorLight copied to clipboard

How to remove cache compile template when run CompileRenderAsync

Open huanbd opened this issue 8 years ago • 16 comments

I can't remove cache template after run method CompileRenderAsync My case is: Step 1: run resultHtml = await _razorLightEngine.CompileRenderAsync(templateId, htmlTemplate, obj);

When I change htmlTemplate to new value template and run this method again (Step 2), but this still return value that ran in Step 1

htmlTemplate is string template

So, how can I remove cache to get the new value follow htmlTemplate Thank you.

huanbd avatar Apr 20 '18 05:04 huanbd

I have the same problem.

I thought removing it from the cache _razorEngine.TemplateCache.Remove(templateKey);

But is not solving the problem

@toddams any ideas how to approach?

I have looked into this further and it seems that maybe the .Net Compiler is caching?!?!?

benbuckland avatar May 19 '18 08:05 benbuckland

Hi @benbuckland In my case, I use a string template, when using the CompileRenderAsync method, it creates two caches, one is the compile cache and the other one is stored in the memory cache. When using _razorEngine.TemplateCache.Remove (templateKey), it only clears the cache in memory cache, I can not find the way to clear the compile cache

And I figured out that change solution from string template to file template, when the file changes, Razorlight will automatically clear the compile cache

This is my code:

Load template from file

var project = new FileSystemRazorProject(HostingEnvironment.WebRootPath + "/template")
{
      Extension = "txt"
};
services.AddRazorLight(() => new RazorLightEngineBuilder()
                                            .UseMemoryCachingProvider()
                                            .UseProject(project)
                                            .Build());

huanbd avatar May 24 '18 09:05 huanbd

_razorEngine.TemplateCache.Remove (templateKey) It's not work ! it's still return the complied template from cache

263613093 avatar May 25 '18 02:05 263613093

I am facing the same issue. My unit test is the following. As stated in the comment, it fails three lines before end:

using Newtonsoft.Json.Linq;
using Xunit;

namespace GeneralTests
{
    public class DescriptionFormatterTest
    {
        public class Model
        {
            public JObject R;
        }

        [Fact]
        public void DescriptionFormatterRecompileTest()
        {
            string json = @"{x:'Klaus'}";
            Model m = new Model { R = JObject.Parse(json) };
            var engine = new RazorLight.RazorLightEngineBuilder().UseMemoryCachingProvider().Build();
            string template1 = "Hallo @Model.R[\"x\"]";
            //Test with initial template
            Assert.Equal("Hallo Klaus", engine.CompileRenderAsync("templateKey1", template1, m).Result);
            //Fetch the already compiled template from cache
            var cacheResult = engine.TemplateCache.RetrieveTemplate("templateKey1");
            Assert.Equal("Hallo Klaus", engine.RenderTemplateAsync(cacheResult.Template.TemplatePageFactory(), m).Result);
            //change Template
            Assert.True(engine.TemplateCache.Contains("templateKey1"));
            engine.TemplateCache.Remove("templateKey1");
            Assert.False(engine.TemplateCache.Contains("templateKey1"));//this assertion is passed
            template1 = "Hi @Model.R[\"x\"]";
            Assert.Equal("Hi Klaus", engine.CompileRenderAsync("templateKey1", template1, m).Result); //here it fails

            cacheResult = engine.TemplateCache.RetrieveTemplate("templateKey1");
            Assert.Equal("Hi Klaus", engine.RenderTemplateAsync(cacheResult.Template.TemplatePageFactory(), m).Result);
        }
    }
}

klaus-liebler avatar Sep 10 '18 11:09 klaus-liebler

This bug to be fix in plan to release 2.0.0?

ANTPro avatar Oct 25 '18 16:10 ANTPro

Setting ExpirationToken of the project item should do it

jjxtra avatar Feb 24 '19 23:02 jjxtra

Pretty sure I have tried that approach without any luck. Would be keen to know if you are able to get it going, currently restarting containers to purge cache.

From: Jeff Johnson [email protected] Sent: Monday, 25 February 2019 12:02 PM To: toddams/RazorLight [email protected] Cc: Ben Buckland [email protected]; Mention [email protected] Subject: Re: [toddams/RazorLight] How to remove cache compile template when run CompileRenderAsync (#177)

Setting ExpirationToken of the project item should do it

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/toddams/RazorLight/issues/177#issuecomment-466827394, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AcXFHwwkb6FWTcqBFVAwjhcoqSPQePk7ks5vQxnzgaJpZM4Tc3zn.

benbuckland avatar Feb 25 '19 03:02 benbuckland

The following test works for me on the beta 2.0 version. This is for an open source project, https://github.com/DigitalRuby/MailDemon. I can see it going into my custom db provider (using LightDB) and re-retrieving the template and recompiling it when I change the template.

You probably need to implement IChangeToken and set this as the ExpirationToken property of the class you are using that inherits RazorLightProjectItem.

[Test]
public void TestTemplateCache()
{
RazorLightEngineBuilder builder = new RazorLightEngineBuilder();
builder.AddDefaultNamespaces("System", "System.IO", "System.Text", "MailDemon");
builder.UseCachingProvider(new MemoryCachingProvider());
builder.UseProject(new RazorLight.Razor.FileSystemRazorProject(Directory.GetCurrentDirectory()));
builder.UseProject(new MailDemonRazorLightDatabaseProject());
var engine = builder.Build();

using (var db = new MailDemonDatabase())
{
    db.Insert<MailList>(new MailList { Name = "test" });
    MailListRegistration model = new MailListRegistration { Fields = new Dictionary<string, object> { { "firstName", "Bob" } } };
    MailTemplate template = new MailTemplate { Name = "test", Template = "<b>Hello World</b> @Model.Fields[\"firstName\"].ToString()".ToUtf8Bytes() };
    db.Insert<MailTemplate>(template);
    var found = engine.TemplateCache.RetrieveTemplate("test");
    string html;
    if (found.Success)
    {
        Assert.Fail("Template should not be cached initially");
    }
    else
    {
        html = engine.CompileRenderAsync("test", model).Sync();
        Assert.AreEqual("<b>Hello World</b> Bob", html);
    }

    found = engine.TemplateCache.RetrieveTemplate("test");
    if (!found.Success)
    {
        Assert.Fail("Template should be cached after compile");
    }
    html = engine.RenderTemplateAsync(found.Template.TemplatePageFactory(), model).Sync();
    Assert.AreEqual("<b>Hello World</b> Bob", html);

    template.Template = ((template.Template.ToUtf8String()) + " <br/>New Line<br/>").ToUtf8Bytes();
    template.Dirty = true;
    db.Update(template);
    var found2 = engine.TemplateCache.RetrieveTemplate("test");

    // should not be cached after modify
    if (found2.Success)
    {
        Assert.Fail("Template should not be cached after modification");
    }
    else
    {
        html = engine.CompileRenderAsync("test", model).Sync();
        Assert.AreEqual("<b>Hello World</b> Bob <br/>New Line<br/>", html);
    }
}

jjxtra avatar Feb 25 '19 17:02 jjxtra

Researched this a little bit today as I've hit exactly the same problem, providing my own custom cache implementation yet no amount of flushing that results in RL regenerating a template even if the source text has changed.

So, I have found a way to defeat the 'internal' cache in RL (not actually sure if its RL itself or Razor that is doing it). @jjxtra put me on the right track and they were right, providing a custom IChangeToken is the way to go - in my case I went with (very simply):

public class AlwaysChangedToken : IChangeToken
	{
		public bool ActiveChangeCallbacks => false; // means that registered parties need to poll

		public IDisposable RegisterChangeCallback(
			Action<object> callback,
			object state)
		{
			// NOTE: we do not allow callers to register for callbacks
			throw new NotImplementedException();
		}

		public bool HasChanged
		{
			get
			{
				// we are *always* changed
				return true;
			}
		}
	}

When you are building your TextSourceRazorProjectItem inside of a custom RazorLightProject you just need to specify an instance of the ChangeToken like:

return new TextSourceRazorProjectItem(compositeTemplateKey, templateString)
{
    ExpirationToken = new AlwaysChangedToken()
};

With that done, beware a new template will be compiled for every RL invocation unless you turn on compiled template caching (which of course you should do).

Annoying and I still don't believe this should be the behaviour of RL out of the box or at the very least some kind of explanation in the documentation would be useful.

kieranbenton avatar Sep 14 '21 16:09 kieranbenton

Hi @kieranbenton. Thanks for sharing your workaround. It works. One question: What do you mean by "turning on compiled template caching"?

titobf avatar Nov 21 '22 16:11 titobf

RazorLight has a way to configure the engine to use caching.

jzabroski avatar Nov 21 '22 16:11 jzabroski

What do you mean by "turning on compiled template caching"? I would also be interested in that. I think UseMemoryCachingProvider() or UseCachingProvider() extensions only affect page template cache, not RL's private compiled metadata template cache which is the crux of the problem.

reponemec avatar Feb 16 '23 14:02 reponemec

Yes, by "compiled template caching" I mean adding a caching provider to Razorlight eg:

		// create the engine (this will use a cache provider)
		this.engine = new RazorLightEngineBuilder()
			.UseProject((RazorLightProject)templateProject)
			.UseCachingProvider(compiledTemplateCache) // all templates get cached in one place
			.Build();

kieranbenton avatar Mar 09 '23 10:03 kieranbenton

Is someone knows good decision how to solve this issue? I turned off UseMemoryCache() but still sometimes works sometimes not!

romapavliuk avatar Mar 23 '23 17:03 romapavliuk

public interface ITemplateCacheCleaner
{
    void Clear();
}

public class DatabaseTemplateItemProvider : RazorLightProject, ITemplateCacheCleaner
{
    CancellationTokenSource _resetTemplateItemCacheToken = new();

    public override async Task<RazorLightProjectItem> GetItemAsync(string templateKey)
    {
        var templateEntity = await dbContext
            .Set<Template>()
            .SingleOrDefaultAsync(tmp => tmp.Key == templateKey);
        
        var templateItem = new TemplateItem(templateKey, templateEntity?.Content);
        templateItem.ExpirationToken = new CancellationChangeToken(_resetTemplateItemCacheToken.Token);
		
        return templateItem;
    }

    void ITemplateCacheCleaner.Clear()
    {
        _resetTemplateItemCacheToken.Cancel();
        _resetTemplateItemCacheToken.Dispose();
        _resetTemplateItemCacheToken = new CancellationTokenSource();
    }
}

ITemplateCacheCleaner, like DatabaseTemplateItemProvider, is a regular DI singleton service.

reponemec avatar May 02 '23 12:05 reponemec

Is there any update related to invalidating or overwriting a cached template? This doesn't work.

        var key = $"report_{report.Id}_subject";
        var cache = this.RazorEngine.Handler.Cache.RetrieveTemplate(key);
        var model = new TemplateModel(content, this.Options);

        if (!updateCache && cache.Success)
            return await this.RazorEngine.RenderTemplateAsync(cache.Template.TemplatePageFactory(), model);
        else
        {
            return await this.RazorEngine.CompileRenderStringAsync(key, report.Settings.GetDictionaryJsonValue<string>("subject") ?? "", model);
        }

Fosol avatar May 15 '23 20:05 Fosol