I'm trying to figure out an issue we've been having lately with RazorEngine 3.7.5 and higher (tried 3.7.7)
Exception:
System.ArgumentException: Please either set a template manager to templates or add the template 'MySolution.Billing.Templates.Layout.cshtml'!
It occurs when trying to cache the template with Engine.Razor.Compile method.
public void AddTemplate(string templateName, string source)
{
Engine.Razor.AddTemplate(templateName, source);
}
public void CacheTemplate(string templateName, Type type)
{
var templateKey = new NameOnlyTemplateKey(templateName, ResolveType.Layout, null);
Engine.Razor.Compile(templateKey, type);
}
The PreloadTemplates method is called when the service which contains it is created using StructureMap for instanciation. Each templates is stored as an Embedded Resource and loaded into RazorEngine cache and immediatly after that compiled using RazorEngine to make sure that all templates load as quick as possible.
private void PreloadTemplates()
{
var embeddedResources = Assembly.GetExecutingAssembly().GetManifestResourceNames().Where(x => x.StartsWith("MySolution.Billing.Templates")).ToList();
foreach (var invoiceResource in embeddedResources)
{
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(invoiceResource))
{
using (var reader = new StreamReader(stream))
{
var template = reader.ReadToEnd();
this._templatingService.AddTemplate(invoiceResource, template);
}
}
}
this._templatingService.CacheTemplate("MySolution.Billing.Templates.Header.cshtml", typeof(HeaderModel));
this._templatingService.CacheTemplate("MySolution.Billing.Templates.Layout.cshtml", typeof(LayoutModel));
this._templatingService.CacheTemplate("MySolution.Billing.Templates.Footer.cshtml", null);
}
RazorEngine is configured as follow
var config = new TemplateServiceConfiguration();
config.CachingProvider = new DefaultCachingProvider(t => { });
config.DisableTempFileLocking = true;
How we are using RazorEngine, flow of the application
Steps to reproduce
We can reproduce the error almost everytimes by stopping IIS and starting it back again and doing a a call to the WCF method. It seems to be a problem with recycling the app pool or stopping IIS because the error does not come back after WCF has "warmed up".
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.
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.
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 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.
I was able to find an answer by myself afterall.
I modified my TemplatingService class as following
public class TemplatingService : ITemplatingService
{
private readonly IRazorEngineService _razorEngineService;
public TemplatingService(Assembly assembly, string templatesNamespace)
{
var config = new TemplateServiceConfiguration();
config.TemplateManager = new EmbeddedResourceTemplateService(assembly, templatesNamespace);
#if DEBUG
config.Debug = true;
#endif
this._razorEngineService = RazorEngineService.Create(config);
}
public void CacheTemplate(string templateName, Type type)
{
var templateKey = new NameOnlyTemplateKey(templateName, ResolveType.Layout, null);
this._razorEngineService.Compile(templateKey, type);
}
public string RunTemplate(string templateName, Type type, object model, IDictionary<string, object> dynamicViewBag = null)
{
var templateKey = new NameOnlyTemplateKey(templateName, ResolveType.Layout, null);
return this._razorEngineService.RunCompile(templateKey, type, model, dynamicViewBag != null ? new DynamicViewBag(dynamicViewBag) : null);
}
}
I started using the TemplatingManager from the official website: RazorEngine string layouts and sections? and it seemed to have done the trick.
this.For<ITemplatingService>()
.Singleton()
.Add<TemplatingService>()
.Named("invoiceTemplates")
.Ctor<Assembly>("assembly").Is(billingDocumentGeneratorAssembly)
.Ctor<string>("templatesNamespace").Is("MyBillingNamespace.DocumentGenerator.Invoices.Templates");
And I can use the TemplatingService as follows
var footerHtml = this._templatingService.RunTemplate("Footer.cshtml", null, null);
var headerHtml = this._templatingService.RunTemplate("Header.cshtml", typeof(AccountStatementHeaderModel), accountStatementModel.Header);
I hope this help someone else.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With