Apricot Framework

ASP.NET Core

What AddRazorEngine registers, where templates may live, and what a non-web host has to supply.

One call, in any order relative to the rest of your MVC setup:

using ApricotFramework.RazorEngine.AspNetCore.Extensions;

builder.Services.AddRazorEngine();

What the call registers

RegistrationWhy
AddMvcCore().AddRazorViewEngine()Supplies IRazorViewEngine, which locates and compiles views, and the ITempDataProvider a ViewContext requires
TryAddSingleton<IRazorEngine, MvcRazorEngine>The engine itself

Both are additive. AddRazorViewEngine is TryAdd-based internally, so an application that also calls AddControllersWithViews or AddRazorPages is unaffected — and unaffected by the order of the two calls, which is worth knowing because the failure mode of a non-additive registration is a working build and a broken host.

The consequence worth stating plainly: you do not need AddControllersWithViews. A minimal-API host that serves no views of its own gets a working engine from this one call. The example application in this repository is exactly that — its Program.cs contains no MVC registration at all.

IRazorEngine is a singleton, and registered with TryAddSingleton, so your own implementation registered beforehand is left in place.

Where templates may live

Templates are compiled by the Razor SDK into an assembly, and the engine finds them there.

ProjectSetupFound
The web application itselfNothing — Microsoft.NET.Sdk.Web compiles .cshtml alreadyYes
A referenced class libraryMicrosoft.NET.Sdk.Razor with <AddRazorSupportForMvc>true</AddRazorSupportForMvc>Yes, with no registration of application parts

A shared library of e-mail templates is therefore a plain project reference, which is the arrangement most teams want: the templates live next to the code that sends the mail, not in the web host.

Warning

Give a template in a class library a public model type and a @model declaration. An anonymous type will not work across the assembly boundary — the dynamic binding inside the library's compiled view cannot see a type that is internal to your application, and the failure arrives at render time.

When rendering may happen

Any time after the host is built. There is no dependency on an in-flight request, so this is valid:

var app = builder.Build();

// Before the host listens.
var engine = app.Services.GetRequiredService<IRazorEngine>();
var html = await engine.RenderAsync("~/Templates/Startup.cshtml", model);

app.Run();

...as is rendering from an IHostedService, a message consumer, or a scheduled job.

Hosts that are not web hosts

The MVC Razor view engine expects the services a web host provides. Two of them are not registered by any MVC call, and AddRazorEngine deliberately does not fabricate them:

ServiceSupplied byNeeded for
DiagnosticSourceThe web host, as a DiagnosticListenerActivating the Razor page activator — without it, resolving IRazorEngine throws
IWebHostEnvironmentThe web hostDiscovering compiled views — without it, every lookup reports nothing found

In a WebApplication host both are present and there is nothing to do. In a plain Host.CreateApplicationBuilder worker or console host, neither is, and you have to register them yourself before the engine will work:

var listener = new DiagnosticListener("MyWorker");
builder.Services.AddSingleton(listener);
builder.Services.AddSingleton<DiagnosticSource>(listener);
builder.Services.AddSingleton<IWebHostEnvironment>(/* your own, naming the content root */);

builder.Services.AddRazorEngine();

Note

IWebHostEnvironment is the host's own identity — its content root, web root and application name. A library cannot invent a meaningful one, which is why this is left to you rather than guessed at. If a worker needs to render templates, hosting it as a WebApplication that simply maps no endpoints is usually less work than shimming the environment.

Lifetimes and scopes

The engine is a singleton and creates one service scope per render, from the root provider. Nothing is shared between two renders, and nothing is shared with a surrounding request.

That last point is the one to remember: inside a request handler, a view that resolves a scoped service does not get the request's instance. It gets one belonging to the render, which is disposed when RenderAsync returns. Templates that take their data from the model — which is nearly all of them — never notice. A template that resolves a scoped DbContext expecting the request's unit of work does.

If you need the ambient scope instead, derive from MvcRazorEngine and override GetActionContext to use the request's RequestServices; see Usage.

On this page