In this post I discuss the new support for rendering Blazor components (AKA Razor components) to a string
without running a full Blazor app. That can be useful if you want to, for example, generate HTML in a background service.
As this post is using the release candidate 1 build, some of the features may change, be fixed, or be removed before .NET 8 finally ships in November 2023!
Blazor in .NET 8
In this series looking at new .NET 8 features as they're introduced, I've focused primarily on ASP.NET Core features. I've looked at things like the new source generators and native AOT support because native AOT feels like one of the main headline features in .NET 8. However, arguably, the real star of ASP.NET Core in .NET 8 is Blazor.
Historically, Blazor has operated in two modes:
- Blazor Server—A SignalR connection is maintained between the browser and the server, with the server holding a stateful session. This mode is fast to start up and doesn't require an additional API layer but must maintain a persistent connection.
- Blazor WASM—The Blazor app runs in the browser, and doesn't require a server app at all. This mode needs a large up-front download of your app, and must communicate with a server using normal APIs, but doesn't require a persistent connection.
There were always variations on these two main approaches (for example pre-rendering of hosted Blazor WASM sites), but .NET 8 brings two additional huge changes.
First of all, an entire new mode has been added—Static Server Rendering (SSR). In this mode, there's no WASM running in the browser, but there's also no persistent SignalR connection. Instead, the site operates mostly the same way as "traditional" MVC Razor application would. The server renders the content to HTML and sends it to the browser. The server doesn't maintain any persistent state, and the client doesn't have a big initial download!
There are various features that come as part of the SSR support such as form handling, streaming rendering, and enhanced navigation (which can give some of the feel of a SPA app).
In addition to the new SSR render mode, Blazor now allows you to mix rendering modes in your application, for example including Blazor Server components in an otherwise statically rendered application. You can even mix WASM and Blazor Server components in the same app, though that sounds a bit like a recipe for confusion to me! 😅
There's loads of more features added to BLazor for .NET 8, but I'm mostly not going to cover them on my blog. I think Blazor is a great technology, but it's not one I've used extensively. However, there's one new feature of Blazor that I think will be useful even if you're not building a Blazor app per se: rending components outside of an ASP.NET Core context.
Rendering Blazor components outside of an ASP.NET Core context
I've been in many situations where you need to render some HTML to a string
outside of the context of an HTML request. The canonical example is that you need to create an HTML email template to send to users.
Everyone has their own approaches to this: maybe you've gone low-tech with simple string concatenation/interpolation; maybe you tried to use the Razor engine directly; or maybe you used a helper library like RazorLight. All of these approaches work (and I've personally used all of them), but in .NET 8 you have another option: use Blazor components.
There have been a lot of improvements to basic Blazor capabilities in .NET 8. Support was added for sections and you can now place all your HTML layout in .razor components (instead of needing a top-level .cshtml file). All that, combined with exposing the required rendering APIs mean this is actually quite a nice option!
Trying it out in an ASP.NET Core app
In this section I'll show how you can use this in your own apps. I create a helper class for easily rendering a component and providing any parameters. In the following example I show an example in the context of ASP.NET Core, mostly because the docs already show how to do this in a console app, and I suspect most people won't need to do the extra steps described in that post.
We'll start by creating the app. I created a simple minimal API application using
dotnet new web
Make sure you're using the .NET 8 SDK. In this post I'm using the .NET 8 RC 1 build.
We're going to need some components to render, and because I want to explore what does (and doesn't) work, I created several components.
Creating the Blazor components
We'll start with some boilerplate. I created a Components folder and added an _Imports.razor file with the following using statements:
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Sections
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
Next I added the outermost component that I (imaginatively) called App
by creating an App.Razor file inside the Components folder. Inside this component I added the "root" HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="icon" type="image/png" href="favicon.png" />
</head>
<body>
<Home Name="@Name" />
</body>
</html>
@code {
[Parameter]
public string? Name { get; set; }
}
This is pretty standard, except that the body contains a single component Home
, which I'm binding the Name
parameter to. That's a little unnecessary, it just tests the general Blazor capabilities.
Note that the usual
<HeadOutlet>
component is missing - more on that later!
Next lets create the Home
component:
<LayoutView Layout="@typeof(MainLayout)">
<PageTitle>Home</PageTitle>
<h2>Welcome to your new app.</h2>
</LayoutView>
<SectionContent SectionName="side-bar">
Hello @Name!
</SectionContent>
@code {
[Parameter, EditorRequired]
public string Name { get; set; }
}
This component is again mostly intended to demonstrate some Blazor features. I'm using a LayoutView
to "wrap" our component in a layout. Normally this would be handled by a Router
component, but we don't need a full router component for rendering HTML to a string
, and this achieves much the same thing.
We also have a <SectionContent>
component which is a new feature in .NET 8, providing section support to Blazor. If you've ever used sections with Razor view or Razor Pages then you won't have any difficulty with the Blazor section support.
The final thing to point out is that we have used a PageTitle
component. This is normally used to set the <title>
element in the <head>
of the response. Unfortunately it won't work in this example, though it doesn't cause any errors.
Finally, we have the MainLayout
layout component:
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<SectionOutlet SectionName="side-bar" />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
There's nothing very exciting here. As it's a layout component, it derives from LayoutComponentBase
. You can also see the SectionOutlet
component which defines where the SectionContent
from the Home
component should be rendered.
Putting it all together we have 4 razor files: _Imports.razor, App.razor, Home.razor, and MainLayout.razor
I wanted to explore how much "normal" Blazor I could use in the static rendering, hence my use of multiple components, but there's no reason you have to do this. If you have a very simple use case, you could easily use a single component.
Rendering a component to a string
HtmlRenderer
is the new type that was added to .NET 8 which provides the mechanism for rendering components to a string
. Unfortunately, it's slightly clumsy to work with due to the need to use Blazor's sync context, so I created a simple wrapper class for it, BlazorRenderer
.
BlazorRenderer
, shown below, takes care of calling the HtmlRenderer
correctly using a dispatcher and converting the parameters as the required ParameterView
object.
internal class BlazorRenderer
{
private readonly HtmlRenderer _htmlRenderer;
public BlazorRenderer(HtmlRenderer htmlRenderer)
{
_htmlRenderer = htmlRenderer;
}
// Renders a component T which doesn't require any parameters
public Task<string> RenderComponent<T>() where T : IComponent
=> RenderComponent<T>(ParameterView.Empty);
// Renders a component T using the provided dictionary of parameters
public Task<string> RenderComponent<T>(Dictionary<string, object?> dictionary) where T : IComponent
=> RenderComponent<T>(ParameterView.FromDictionary(dictionary));
private Task<string> RenderComponent<T>(ParameterView parameters) where T : IComponent
{
// Use the default dispatcher to invoke actions in the context of the
// static HTML renderer and return as a string
return _htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
HtmlRootComponent output = await _htmlRenderer.RenderComponentAsync<T>(parameters);
return output.ToHtmlString();
});
}
}
You use this renderer something like this:
string html = await renderer.RenderComponent<App>();
Easy!
Defining the application
Just to flesh out the example, we'll use the renderer in an ASP.NET Core application and return the result in a minimal API. The following example shows how to use the BlazorRenderer
in practice, as well as how to pass parameters to the component
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
var builder = WebApplication.CreateBuilder();
// Add the renderer and wrapper to services
builder.Services.AddScoped<HtmlRenderer>();
builder.Services.AddScoped<BlazorRenderer>();
var app = builder.Build();
// Inject the renderer and optionally a name from the querystring
app.MapGet("/", async (BlazorRenderer renderer, string name = "world") =>
{
// Pass the parameters and render the component
var html = await renderer.RenderComponent<App>(new() {{nameof(App.Name), name}});
// Return the result as HTML
return Results.Content(html, "text/html");
});
app.Run();
Note that you have to pass the component parameters by name, but as the parameters are public you can use nameof
to make the calls refactor safe!
Trying it out
That's all we need to try it out. If we run the application and hit /?name=Andrew
you can see the component is being rendered to HTML and returned in the response 🎉
Obviously I'm not suggesting that you use this approach to render HTML in your minimal API apps (though I can certainly envisage a Blazor-based version of Damian Edwards' Razor Slices project). I just used it here as an easy demonstration of rendering the components in the context of an ASP.NET Core (or worker service app) where you already have a DI container configured.
Rendering components without a DI container
If you want to use the HtmlRenderer
without a DI container you'll need to create one yourself, as shown in the documentation.
To make things a little easier on yourself you could create a similar BlazorRenderer
wrapper for this scenario. This is similar to the "ASP.NET Core" definition I showed earlier, but it creates its own ServiceProvider
that lives for the lifetime of the BlazorRenderer
:
internal class BlazorRenderer : IAsyncDisposable
{
private readonly ServiceProvider _serviceProvider;
private readonly ILoggerFactory _loggerFactory;
private readonly HtmlRenderer _htmlRenderer;
public BlazorRenderer()
{
// Build all the dependencies for the HtmlRenderer
var services = new ServiceCollection();
services.AddLogging();
_serviceProvider = services.BuildServiceProvider();
_loggerFactory = _serviceProvider.GetRequiredService<ILoggerFactory>();
_htmlRenderer = new HtmlRenderer(_serviceProvider, _loggerFactory);
}
// Dispose the services and DI container we created
public async ValueTask DisposeAsync()
{
await _htmlRenderer.DisposeAsync();
_loggerFactory.Dispose();
await _serviceProvider.DisposeAsync();
}
// The other public methods are identical
public Task<string> RenderComponent<T>() where T : IComponent
=> RenderComponent<T>(ParameterView.Empty);
public Task<string> RenderComponent<T>(Dictionary<string, object?> dictionary) where T : IComponent
=> RenderComponent<T>(ParameterView.FromDictionary(dictionary));
private Task<string> RenderComponent<T>(ParameterView parameters) where T : IComponent
{
return _htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var output = await _htmlRenderer.RenderComponentAsync<T>(parameters);
return output.ToHtmlString();
});
}
}
With that, you can now simply create a new BlazorRenderer
and render any components:
await using var blazorRenderer = new BlazorRenderer();
var html = await blazorRenderer.RenderComponent<App>();
Console.WriteLine(html);
OK, so that's all great, now let's look at what doesn't work!
What doesn't work?
I've already pointed out one Blazor component that doesn't work correctly when rendering to a string: the PageTitle
component. Normally this renders content to a HeadOutlet
component, changing the title for the page. Unfortunately, if you try to add the HeadOutlet
when rendering to a string
, you'll get an error:
I gave a brief shot at working around this by calling AddServerSideBlazor()
, but that just brought a whole new set of service requirements, so I think it's safe to say this one just doesn't work.
Similarly, anything related to routing gives missing service issues. Attempting to use the NavMenu
or Router
components gives similar errors:
It kind of makes sense that this doesn't work—you're not rendering a full Blazor app, just a component hierarchy. So that's something else to bear in mind if you're trying to reuse components from a "real" Blazor app. Aside from that, I didn't run into anything else that didn't work, so hopefully you won't have any issues either!
Summary
In this post I described the new feature in .NET Blazor for rendering components to a string, outside the context of a normal Blazor Server or Blazor WASM application. It's possible to render components completely outside of ASP.NET Core, but in this app I showed how to use ASP.NET Core's DI container to simplify the process somewhat. Additionally, I showed how to create a small wrapper around HtmlRenderer
to make it easier to render components, and described some of the components you can't use.