toddams/RazorLight

How to save template in cache?

alekdavis opened this issue ยท 22 comments

Discussed in #484

Originally posted by alekdavisintel May 31, 2022
The README example shows how to render template from cache, but how do you add it to cache? I.e. what should happen if the cacheResult.Success is false?

var cacheResult = engine.Handler.Cache.RetrieveTemplate("templateKey");
if(cacheResult.Success)
{
	var templatePage = cacheResult.Template.TemplatePageFactory();
	string result = await engine.RenderTemplateAsync(templatePage, model);
}

Here is my code outline (my templates come from strings):

string tempateKey = "XYZ"; 
string templateHtml = "SOME RASOR TEXT"; // Original text has @ placeholders
dynamic model = new ExpandoObject(); // Set some properties below not showing for briefness

var engine = new RazorLightEngineBuilder()
	        .UseMemoryCachingProvider()
	        .Build();

TemplateCacheLookupResult cacheResult = engine.Handler.Cache.RetrieveTemplate(templateKey);

if (cacheResult.Success)
{
    // This code never executes.
    var templatePage = cacheResult.Template.TemplatePageFactory();

    result = await engine.RenderTemplateAsync(templatePage, model);
}
else
{
    // This code always executes.
    result = await engine.CompileRenderStringAsync(templateKey, templateHtml, model);
}

The code never gets into the if (cacheResult.Success) block. What am I doing wrong or not doing at all?

Thanks!

The code never gets into the if (cacheResult.Success) block. What am I doing wrong or not doing at all?

It sounds like you're probably using something like Azure Functions where the process that owns the cache is being torn down on each execution. Do you understand what your process model is? What is it?

@jzabroski We run it from a REST API hosted in Azure. Is there something specific we need to do?

@jzabroski Also, forgot to mention: I get the same behavior when I debug locally (in Visual Studio, ASP.NET CORE web service). So, I'm pretty sure the process is not being restarted. Another thing I noticed when debugging locally: the very first time the template compilation API gets called, it takes a few seconds to return, but all subsequent calls come from the same API a lot faster. Does not matter which template gets compiled: the original or a totally different one. Kind of weird.

There is no precompilation, so the first time it gets compiled, it has to compile the types.

@jzabroski Interesting. How does it know what to compile, since I only compile ad-hoc strings (not types)?

Your problem is you didn't use a RazorLightProject. I thought I configured it to throw an exception if people did things this way. Long story short, lots of people copy-pasted code from some medium.com article that caused a lot of people bugs, so I "fixed" it to continue without the cache.

See the README - it is the first FAQ in the FAQ: https://github.com/toddams/RazorLight#how-to-use-templates-from-memory-without-setting-a-project

@jzabroski Ah, I see. I think I started it like in the recommended example, but since I'm not dealing with partial views and all the templates we use are ad-hoc in-memory strings, at some point I was questioning whether it applied to our use case, so I removed some initialization calls and ended up with the bare minimum. And since it worked (I mean, I got no errors, and it did handle the template task), I assumed this use case would not apply.

So, I changed code to this:

        RazorLightEngine razorEngine = new RazorLightEngineBuilder()
            .UseEmbeddedResourcesProject(GetType())
	    .UseMemoryCachingProvider()
	    .Build();

But it did not really do anything (the code still does not execute the caching logic path). The README says AnyTypeInYourSolution, so I just get the type of class that uses RazorLightEngine (which is a class responsible for reading localized templates from local files and merging them with data). Or is it supposed to be some specific type?

@jzabroski I just googled for RazorLightEngine and UseEmbeddedResourceProject and found this discussion: #250, which sounds like our case: we use RazorLightEngine to generate localized email notifications. We use a somewhat complex logic to choose a specific template for a certain language (we can't just load a resource, we need to see if a specific translation exists and if not, we fall back to a different language, until we find a match). So there is a bit of tango going on before we finally get the string we need and pass it to RazorLightEngine. It also allows us to add (or remove) translations without messing with the project.

Anyway, still not sure why UseEmbeddedResourcesProject with the type that has no embedded templates would be needed and why calling it still did not make any difference. My understanding is that caching occurs when we compile a template which can come from an embedded resource or a string. In our case, it comes from a string, so we compile a string template with a unique key, so it should go into the cache, right?

@jzabroski

Okay, I think I figured it out. I got the source and debugged it in my app. The reason why caching is not working is because the Cache object (LookupCache member of the MemoryCachingProvider class) is not static. Since my RazorLightEngine variable is also not static, every time it gets created (which is on every REST API call), the LookupCache property gets reinitialized, so it loses whatever was saved in cache from the previous call).

I am not sure if this is by design, but after I made the following change in the MemoryCachingProvider.cs file, it started to work as expected.

ORIGINAL:

		public MemoryCachingProvider()
		{
			var cacheOptions = Options.Create(new MemoryCacheOptions());

			LookupCache = new MemoryCache(cacheOptions);
		}

		protected IMemoryCache LookupCache { get; set; }

NEW:

		public MemoryCachingProvider()
		{
			var cacheOptions = Options.Create(new MemoryCacheOptions());

			if (LookupCache == null)
				LookupCache = new MemoryCache(cacheOptions);
		}

		protected static IMemoryCache LookupCache { get; set; }

If the original implementation is by design, I think I can address it by making my RazorLightEngine variable a static member (I'm pretty sure it will work). I assume you use the default implementation of MemoryCache, which is supposed to be thread safe, so it should be okay. It would be nicer if the memory cache implementation of RasorLightEngine were static, but I do not know if the intention was to provide caching for instance invocations only (it would be weird, though, but who knows, maybe you intend to enable cache in a loop within a function only, I would not try to guess).

Anyway, please let me know if this is by design.

Also, after looking at the source code and reading comments, it looks like calling UseEmbeddedResourcesProject is not required and for ad-hoc strings a better option would be to call UseNoProject. You may want to include this bit in the README section.

Thanks for the updates.

Correct, I put UseNoProject because of some bad code on the Internet where people were bypassing the RazorLightEngineBuilder completely along with the RazorLightEngine and just using the IEngineHandler. That made it really hard to diagnose bug reports since people who copy-paste code from the Internet often also submit hard to comprehend bug reports asking for help.

SOrry for terse replies - only have so much time to answer questions.

Anyway, please let me know if this is by design.

I would not use the static keyword but instead configure Microsoft DI to register the Engine as a singleton. The static keyword is once per class, whereas the singleton is per "type universe" ("built" from service provider's service collection). It's just a lot easier to reason about behavior in case people create another class that does engine-foo.

@jzabroski No, thanks, I appreciate it. Just need to know if the caching behavior is by design or a bug. Is it supposed to work only within the scope of a single RazorLightEngine instance? Yes? No?

@jzabroski Seems like we submitted comments at the same time. Thanks for the reply. In my case, I think I need it to be "once per class", so should be okay. Thanks. I would add to the documentation this bit of info, because it may not be intuitive.

@jzabroski On the second thought, you may be right. Thanks again for the tip.

I'm tripping on this as well. I'm running this in a docker container and have a memory leak. I've narrowed this down to UseMemoryCache(). I think I have an idea how to solve this, I'll respond tomorrow.

@dallasbeek Oh, man, this doesn't sound good. We'll be deploying our app in Azure (in a container), as well. Please let us posted what you find.

For the caching issue, I ended up defining a static property holding the RazorLightEnfine object. Other approaches did not work in my scenario.

When you say you have a memory leak, you should be able to quickly prove it with WinDbg and dumping the heap stats for the object or namespace you think is the offender.

Alternatively, you can use a fancy paid (trialware) tool like Redgate ANTS memory profiler.

I doubt you have a memory leak, but let's see the stats. What you might have done is use string rendering and glued together different strings ad-hoc, which is by definition a memory leak - that's why it doesn't make sense to cache the string results.

@jzabroski When you say that "it doesn't make sense to cache the string results", I assume you mean random string, right? If we have enumerated number of fixed strings, cache would make sense.

Yes. But you would be surprised at what nonsense people cobble together. Just no design whatsoever.

So what I was seeing was that every generated html document added to the memory. Our simple fix was to register the memorycache as singleton, inject it into our service class and change .UseMemoryCachingProvider() to .UseCachingProvider(_provider). We are using Autofac

register it
builder.RegisterType<MemoryCachingProvider>().As<ICachingProvider>().SingleInstance(); 

inject it
private readonly ICachingProvider _provider;

public DocumentService(ICachingProvider provider)
{
    _provider = provider;
}

use it
var engine = new RazorLightEngineBuilder()
        .UseFileSystemProject($"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}/Views", string.Empty)
        .UseCachingProvider(_provider)
        .Build();

It's a bit hard to see exactly where the issue is from this snippet, but it sounds like your DocumentService should have a RazorLightEngine reference. In that case, were you registering the IRazorLightEngine interface via delegate?

The main drawback to registering it via delegate is that, as I recall, Microsoft DI doesnt allow a way to "remove" delegate registrations last I checked (from memory - not looking up docs). So, I dont register delegates in a base / reusable library that then gets exported elsewhere, if that makes sense. I only register interfaces to resolve via delegate if its an endpoint/entrypoint into a process

Closing as resolved based on @alekdavis upvoting @dallasbeek code example. Please re-open if I am misguided.