How to remove cache compile template when run CompileRenderAsync
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.
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?!?!?
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());
_razorEngine.TemplateCache.Remove (templateKey) It's not work ! it's still return the complied template from cache
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);
}
}
}
This bug to be fix in plan to release 2.0.0?
Setting ExpirationToken of the project item should do it
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.
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);
}
}
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.
Hi @kieranbenton. Thanks for sharing your workaround. It works. One question: What do you mean by "turning on compiled template caching"?
RazorLight has a way to configure the engine to use caching.
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.
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();
Is someone knows good decision how to solve this issue? I turned off UseMemoryCache() but still sometimes works sometimes not!
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.
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);
}