Skip to content

Memory usage and unloading templates

Alexander Selishchev edited this page Oct 22, 2026 · 3 revisions

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.

Enable unloading

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 unloaded

Template is unloaded either

  • on Dispose(), or
  • when template object is garbage collected without Dispose()

Calling Run() after Dispose() throws ObjectDisposedException.

Load saved template with unloading

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);

Cache with eviction

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.

Benchmark

.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.

Things to know

  • .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 of Microsoft.CSharp (used by dynamic / 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 dynamic models are not unloaded (private copy of Microsoft.CSharp cannot 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.

Clone this wiki locally