Razor View Rendering in .NET 6

Razor is a markup syntax that allows embedding server-side C# or Visual Basic code into an HTML and rendering it.

Razor files could have .cshtml.vbhtml or .razor extensions.

Sometimes you need to render the view template into a string from a command prompt, console application, Windows ServiceAzure FunctionsAWS Lambda and embed it into Emails as a template, generate PDF reports based on an HTML page, or save it as a file. Razor view is a great mechanism for these purposes.

So how to render Razor view into a string using .NET 6? We will figure it out below.

Getting Started

Let’s create a class library project. Open any terminal and run the following command:

dotnet new classlib -o RazorViewRendering

Open the newly created project in VisualStudio 2022 or VisualStudio Code and start implementing the rendering functionality.

Host Environment

IWebHostEnvironment provides information about the web hosting environment an application is running in. After releasing .NET Core 3.1Microsoft broke an abstraction layer and renamed IHostingEnvironment to IHostEnvironment, and add additional abstraction for web applications named IWebHostEnvironment.

The previous version of abstraction is still being used, but it is marked as obsolete and will be removed in a future version.

[System.Obsolete("This type is obsolete and will be removed in a future version. The recommended alternative is Microsoft.AspNetCore.Hosting.IWebHostEnvironment.", false)]
public interface IHostingEnvironment

The obsolete version of the IHostingEnvironment looks like this.

public interface IHostingEnvironment
{
    string EnvironmentName { get; set; }
    string ApplicationName { get; set; }
    string WebRootPath { get; set; }
    IFileProvider WebRootFileProvider { get; set; }
    string ContentRootPath { get; set; }
    IFileProvider ContentRootFileProvider { get; set; }
}

The new version of IHostEnvironment looks like this.

public interface IHostEnvironment
{
    string EnvironmentName { get; set; }
    string ApplicationName { get; set; }
    string ContentRootPath { get; set; }
    IFileProvider ContentRootFileProvider { get; set; }
}

As you can see they removed two properties as WebRootPath and WebRootFileProvier.

These two properties were added to IWebHostEnvironment.

public interface IWebHostEnvironment : Microsoft.Extensions.Hosting.IHostEnvironment
{
    string WebRootPath { get; set; }
    IFileProvider WebRootFileProvider { get; set; }
}

After the separation of abstraction layers into two additional interfaces, we got the flexibility of using the Hosting Environment not only on the web but in console applications or other environments.

Thus returning to our implementation using .NET CLI add NuGet package.

dotnet add package Microsoft.AspNetCore.Hosting --version 2.2.7

Microsoft.AspNetCore.Hosting – ASP.NET Core hosting infrastructure and startup logic for web applications.

Add the file WebHostEnvironment.cs to the project.

using Microsoft.AspNetCore.Hosting
using Microsoft.Extensions.FileProviders;

namespace RazorViewRendering;

public sealed class WebHosEnvironment : IWebHostEnvironment
{
    public string ApplicationName { get; set; } = default!;
    public IFileProvider ContentRootFileProvider { get; set; } = default!;
    public string ContentRootPath { get; set; } = default!;
    public string EnvironmentName { get; set; } = default!;
    public string WebRootPath { get; set; } = default!;
    public IFileProvider WebRootFileProvider { get; set; } = default!;
};

Razor View Engine

The Razor View Engine is responsible for rendering the view into pure HTML.

No alt text provided for this image

The rendering flow steps for Razor View Engine are:

  1. Locate the view by the provided physical path
  2. Read the view
  3. Render the view.

Before rendering the view, the engine could be extended by view features.

  • ViewData/TempData is a dictionary of un-typed objects with a string-based key that could be used as a container for data passed to the page.
  • ViewModel is a strongly typed class that is used as a representation of a specific form of data for rendering.
  • ViewBag is a dynamic object which is wrapped around the ViewData and does not have any properties pre-defined in it.

Let’s build our rendering wrapper over Razor View Engine.

Add the file RazorViewRenderer.cs

using Microsoft.AspNetCore.Http
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.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;

namespace RazorViewRendering;

public sealed class RazorViewRenderer
{
    private readonly IRazorViewEngine _viewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;

    public RazorViewRenderer(
        IRazorViewEngine viewEngine,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider
    )
    {
        _viewEngine = viewEngine ?? throw new ArgumentNullException(nameof(viewEngine));
        _tempDataProvider = tempDataProvider ?? throw new ArgumentNullException(nameof(tempDataProvider));
        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
    }

    public async Task<string> RenderAsync<T>(
        string viewPath,
        T model,
        CancellationToken cancellationToken = default
    ) where T : class
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (string.IsNullOrWhiteSpace(viewPath))
            throw new ArgumentNullException(nameof(viewPath));
        if (model == null)
            throw new ArgumentNullException(nameof(model));

        DefaultHttpContext defaultHttpContext = new()
        {
            RequestServices = _serviceProvider
        };
        ActionContext actionContext = new(defaultHttpContext, new RouteData(), new ActionDescriptor());

        IView? view;
        ViewEngineResult viewEngineResult = _viewEngine.GetView(null, viewPath, true);
        if (viewEngineResult.Success)
            view = viewEngineResult.View;
        else
            throw new InvalidOperationException($"Unable to find View {viewPath}.");

        await using StringWriter stringWriter = new();
        ViewDataDictionary<T> viewDataDictionary = new(new EmptyModelMetadataProvider(), new ModelStateDictionary())
        {
            Model = model
        };
        TempDataDictionary tempDataDictionary = new(actionContext.HttpContext, _tempDataProvider);
        ViewContext viewContext = new(
            actionContext,
            view,
            viewDataDictionary,
            tempDataDictionary,
            stringWriter,
            new HtmlHelperOptions()
        );
        await view.RenderAsync(viewContext);

        return stringWriter.ToString();
    }
};

Before using our rendering wrapper we need to configure and create an instance of it. Using .NET CLI add NuGet packages.

dotnet add package Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation --version 6.0.4

Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation – Runtime compilation support for Razor views and Razor Pages in ASP.NET Core MVC.

dotnet add package Microsoft.Extensions.PlatformAbstractions --version 1.1.0

Microsoft.Extensions.PlatformAbstractions – Abstractions that unify behavior and API across .NET Framework.NET Core, and Mono.

Add file RazorViewRendererFactory.cs to the project.

using System.Diagnostics
using System.Reflection;
using Carcass.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.PlatformAbstractions;

namespace RazorViewRendering;

public static class RazorViewRendererFactory
{
    public static RazorViewRenderer New(string inputDirectory, string defaultApplicationName)
    {
        if (string.IsNullOrWhiteSpace(inputDirectory))
            throw new ArgumentNullException(nameof(inputDirectory));
        if (string.IsNullOrWhiteSpace(defaultApplicationName))
            throw new ArgumentNullException(nameof(defaultApplicationName));

        ServiceCollection services = new();
        ApplicationEnvironment? applicationEnvironment = PlatformServices.Default.Application;
        services.AddSingleton(applicationEnvironment);

        WebHosEnvironment environment = new()
        {
            ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name ?? defaultApplicationName,
            ContentRootPath = inputDirectory,
            ContentRootFileProvider = new PhysicalFileProvider(inputDirectory),
            WebRootPath = inputDirectory,
            WebRootFileProvider = new PhysicalFileProvider(inputDirectory)
        };
        services.AddSingleton<IWebHostEnvironment>(environment);

        services.Configure<MvcRazorRuntimeCompilationOptions>(mrrco =>
            {
                mrrco.FileProviders.Clear();
                mrrco.FileProviders.Add(new PhysicalFileProvider(inputDirectory));
            }
        );
        services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
        services.TryAddSingleton(new DiagnosticListener("Microsoft.AspNetCore"));
        services.TryAddSingleton<DiagnosticSource>(sp => sp.GetRequiredService<DiagnosticListener>());
        services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
        services.TryAddSingleton<ConsolidatedAssemblyApplicationPartFactory>();
        services.AddLogging()
            .AddHttpContextAccessor()
            .AddMvcCore()
            .AddRazorPages()
            .AddRazorRuntimeCompilation();
        services.AddSingleton<RazorViewRenderer>();

        ServiceProvider serviceProvider = services.BuildServiceProvider();

        return serviceProvider.GetRequiredService<RazorViewRenderer>();
    }
};

Sample Usage

Create RazorViewContext object that is represented the ViewModel of the page.

public sealed record RazorViewContext(string PageTitle, string PageRoute)
{
    public List<string> PageTags { get; set; } = new();
}

To render input-razor-view.cshtml view into a string use the code below.

RazorViewContext razorViewContext = new(
    "Page Title 1",
    "/pages/page-1.html"
)
{
    PageTags = new List<string>()
    {
        "Tag 1", "Tag 2", "Tag 3"
    }
};

string renderedView = await RazorViewRendererFactory.New(
    "wwwroot",
    "SampleWebApplication"
).RenderAsync("input-razor-view.cshtml", razorViewContext);

Additional Resources

Written by Serhii Kokhan, .Net Architect

Comments are closed.