Skip to content

Instantly share code, notes, and snippets.

@boukeversteegh
Last active January 12, 2022 21:57
Show Gist options
  • Save boukeversteegh/67d26a44d61d9f35a0cba63a636d9c97 to your computer and use it in GitHub Desktop.
Save boukeversteegh/67d26a44d61d9f35a0cba63a636d9c97 to your computer and use it in GitHub Desktop.
Render Razor views from a Console Application (Dotnet 6)

How to render Razor View templates on demand?

Use-cases

  • Email server that uses Razor for its Email templates
  • Console application without a web-site

Solution

  • Build a very minimal web application with Razor support
  • Don't start the web application
  • Add services from the Razor app to your main app

Why this solution

  • To compile Razor templates, you need a LOT of dependencies
    • Most of these are part of ASP.NET Web Application
  • Converting your application to a Web Application means it will also start listening to HTTP requests
    • You may not want this for security and performance reasons
  • Even when resolving all dependencies manually (without using web host), the ViewEngine is still not be able to locate the templates

Implementation

Creating a minimal WebApplication that can compile Razor, without running any web server.

WebApplication CreateRazorApp()
{
    var builder = WebApplication.CreateBuilder();
    builder.Services
        .AddRazorPages()
        .AddApplicationPart(typeof(EmailTemplates).Assembly); // Import views from another assembly
    return builder.Build();
}

Taking some services from the Razor App and injecting them into a ViewRenderer, for use in the main app

services.AddSingleton<ViewRenderer>(_ => {
    var webApp = CreateRazorApp();

    return new ViewRenderer(
        // The view renderer requires a scoped service provider to resolve some scoped services
        webApp.Services.CreateScope().ServiceProvider,
        webApp.Services.GetRequiredService<IRazorViewEngine>(),
        webApp.Services.GetRequiredService<ITempDataProvider>());
});

The ViewRenderer itself

See ViewRenderer.cs

Usage

Here's a fake example of how you might use the IViewRenderer to render a template and send it as an email.

class EmailSender {
    private ViewRenderer viewRenderer;
    public SomeService(IViewRenderer viewRenderer) {
       this.viewRenderer = viewRenderer; 
    }
  
    public SendWelcomeMail() {
       MailHelper.SendMail("foo@example.com", await viewRenderer.RenderAsync("EmailTemplates/welcome", new EmailData {
         To = "John Doe"
       });
    }
}
<!-- If you wish to keep your templates in a separate project, this is what the project file should contain, at least. -->
<!-- I'm using Dotnet 5 to be able to use the templates in Dotnet 5 and 6. -->
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.5"/>
</ItemGroup>
</Project>
<!-- You MUST reference the Web SDK. When you use the Worker SDK, the templates engine won't work. -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Emails.Templates\Emails.Templates.csproj" />
</ItemGroup>
</Project>
// Main program
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Example;
WebApplication CreateRazorApp()
{
var builder = WebApplication.CreateBuilder();
builder.Services
.AddRazorPages()
.AddApplicationPart(typeof(EmailTemplates).Assembly); // Import views from another assembly
return builder.Build();
}
await Host
.CreateDefaultBuilder(args)
.ConfigureServices((context, services) => {
// ...
services.AddSingleton<ViewRenderer>(_ => {
var webApp = CreateRazorApp();
return new ViewRenderer(
webApp.Services.CreateScope().ServiceProvider,
webApp.Services.GetRequiredService<IRazorViewEngine>(),
webApp.Services.GetRequiredService<ITempDataProvider>());
});
})
.Build()
.RunAsync();
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Example;
// Injectable Service that can render a view to string.
// Pass the view's name without '.cshtml', just like you would in a normal MVC app.
// You can configure view locations in CreateRazorApp().
// For example, you can store a template file in '/Views/Shared/Emails/Welcome.cshtml', either in the main project
// or in another project, as shown in CreateRazorApp.
// You can then render this view using RenderAsync('Emails/Welcome', model)
public class ViewRenderer
{
private readonly IServiceProvider services;
private readonly IRazorViewEngine razorViewEngine;
private readonly ITempDataProvider tempDataProvider;
public ViewRenderer(
IServiceProvider services,
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider
)
{
this.services = services;
this.razorViewEngine = razorViewEngine;
this.tempDataProvider = tempDataProvider;
}
public async Task<string?> RenderAsync<TModel>(string templateName, TModel model)
{
var actionContext = new ActionContext(
new DefaultHttpContext { RequestServices = services },
new RouteData(),
new ActionDescriptor());
var viewResult = razorViewEngine.FindView(actionContext, templateName, isMainPage: false);
if (!viewResult.Success) {
throw new InvalidOperationException($"Could not find view '{templateName}'");
}
var viewContext = new ViewContext(
actionContext,
viewResult.View,
new ViewDataDictionary<TModel>(
new EmptyModelMetadataProvider(),
new ModelStateDictionary()) {
Model = model,
},
new TempDataDictionary(actionContext.HttpContext, tempDataProvider),
new StringWriter(),
new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
return viewContext.Writer.ToString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment