Repository navigation
Memory usage and unloading templates
Every compiled template is a separate .NET assembly. By default it is loaded into the default AssemblyLoadContext and stays in memory until the process exits, even if you no longer reference the template.
That is fine for a fixed set of templates (compile once, cache, reuse — see RazorTemplateCache.cs). But if a long running process compiles many unique templates (user defined templates, templates edited at runtime, etc.) memory will grow forever.
Starting with 2026.10.1 templates can be loaded into their own collectible AssemblyLoadContext and unloaded when no longer needed.
IRazorEngine razorEngine = new RazorEngine();
IRazorEngineCompiledTemplate template = razorEngine.Compile("Hello @Model.Name", builder =>
{
builder.EnableUnloading();
});
string result = template.Run(new { Name = "Alex" });
((IDisposable)template).Dispose(); // template assembly will be unloadedTemplate is unloaded either
- on
Dispose(), or - when template object is garbage collected without
Dispose()
Calling Run() after Dispose() throws ObjectDisposedException.
RazorEngineCompiledTemplate template = RazorEngineCompiledTemplate.LoadFromFile("myTemplate.dll", enableUnloading: true);
// same for strongly typed templates and stream variants
RazorEngineCompiledTemplate<MyTemplateBase> template2 = await RazorEngineCompiledTemplate<MyTemplateBase>.LoadFromFileAsync("myTemplate.dll", enableUnloading: true);
IRazorEngineCompiledTemplate template3 = RazorEngineCompiledTemplate.LoadFromStream(stream, enableUnloading: true);Unloading works well together with cache: dispose template when it is evicted.
MemoryCache cache = new MemoryCache(new MemoryCacheOptions());
IRazorEngineCompiledTemplate GetTemplate(string templateText)
{
return cache.GetOrCreate(templateText, entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
entry.RegisterPostEvictionCallback((key, value, reason, state) => ((IDisposable)value).Dispose());
return razorEngine.Compile(templateText, builder => builder.EnableUnloading());
});
}Runs already in progress complete normally when template is disposed, but a template evicted between GetTemplate() and Run() will throw ObjectDisposedException — retry with a fresh GetTemplate() in that case.
.NET 10, template with @Model.Name and @foreach, anonymous model. Each template is unique, compiled, run and dropped.
Speed — average over 300 fresh templates
Compile() |
first Run()
|
next Run()
|
assemblies left in memory | |
|---|---|---|---|---|
| default | ~67 ms | ~7 ms | ~11 µs | 300 of 300 |
EnableUnloading() |
~52 ms | ~340 ms | ~5 µs | 0 of 300 |
Memory — process private bytes (MB) after N templates compiled and dropped, full GC
| 250 | 500 | 750 | 1000 | |
|---|---|---|---|---|
| default | 37 | 45 | 51 | 53 |
EnableUnloading() |
51 | 48 | 53 | 50 |
Slow first Run() comes from loading a private copy of Microsoft.CSharp for dynamic model (see below). Strongly typed templates that do not use dynamic should not pay this cost (not measured).
Without unloading every unique template leaves ~20 KB in memory forever: 1000 templates ≈ 20 MB, 100 000 templates ≈ 2 GB. With unloading memory stays flat, but every new template costs ~0.4 s instead of ~0.07 s.
Rule of thumb: a few hundreds of unique templates per process lifetime — just cache them. Tens of thousands and more (templates from database, user input etc.) — use EnableUnloading() together with cache.
- .NET 5+ only. Option is ignored on .NET Framework / netstandard2.0 — templates are loaded as usual and never unloaded.
-
First
Run()is ~50x slower (see benchmark). Each unloadable template gets its own copy ofMicrosoft.CSharp(used bydynamic/ anonymous models), otherwise its internal caches keep the template in memory forever (dotnet/runtime#71629). Next runs of the same template are as fast as usual. -
Single-file apps: templates using
dynamicmodels are not unloaded (private copy ofMicrosoft.CSharpcannot be loaded from a bundle). Strongly typed templates are unloaded fine. -
Unloading is cooperative. Assembly is actually unloaded by GC only when nothing references template types anymore. Do not keep template instances, delegates or objects created inside templates after
Dispose(). - Do not use it for a small fixed set of templates — just compile once and cache them, there is nothing to unload.