https://andrewlock.net/exploring-the-dotnet-8-preview-rendering-blazor-components-to-a-string/
blog post image
Andrew Lock avatar
Andrew Lock | .NET Escapades Andrew Lock
* Home
* About
* Subscribe
* Dark Light
*
*
*
*
*
*
Sponsored by Nick Chapsas--Want to learn how to build elegant REST
APIs in .NET? Get 5% off Nick's latest course "From Zero to Hero:
REST APIs in .NET"!
October 10, 2023 ~10 min read
* ASP.NET Core
* .NET 8
* AOT
Rendering Blazor components to a string
Exploring the .NET 8 preview - Part 9
Share on:
*
*
*
*
This is the ninth post in the series: Exploring the .NET 8 preview.
1. Part 1 - Using the new configuration binder source generator
2. Part 2 - The minimal API AOT compilation template
3. Part 3 - Comparing WebApplication.CreateBuilder() to the new
CreateSlimBuilder() method
4. Part 4 - Exploring the new minimal API source generator
5. Part 5 - Replacing method calls with Interceptors
6. Part 6 - Keyed service dependency injection container support
7. Part 7 - Form binding in minimal APIs
8. Part 8 - Introducing the Identity API endpoints
9. Part 9 - Rendering Blazor components to a string (this post)
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:
@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 component is missing - more on
that later!
Next lets create the Home component:
Home
Welcome to your new app.
Hello @Name!
@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 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 element in the
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
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
The razor components in the app
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 RenderComponent() where T : IComponent
=> RenderComponent(ParameterView.Empty);
// Renders a component T using the provided dictionary of parameters
public Task RenderComponent(Dictionary dictionary) where T : IComponent
=> RenderComponent(ParameterView.FromDictionary(dictionary));
private Task RenderComponent(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(parameters);
return output.ToHtmlString();
});
}
}
You use this renderer something like this:
string html = await renderer.RenderComponent();
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();
builder.Services.AddScoped();
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(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
The response is rendered as HTML
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();
_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 RenderComponent() where T : IComponent
=> RenderComponent(ParameterView.Empty);
public Task RenderComponent(Dictionary dictionary) where T : IComponent
=> RenderComponent(ParameterView.FromDictionary(dictionary));
private Task RenderComponent(ParameterView parameters) where T : IComponent
{
return _htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
var output = await _htmlRenderer.RenderComponentAsync(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();
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:
InvalidOperationException: Cannot provide a value for property
'JSRuntime' on type 'Microsoft.AspNetCore.Components.Web.HeadOutlet'.
There is no registered service of type
'Microsoft.JSInterop.IJSRuntime'.
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:
InvalidOperationException: Cannot provide a value for property
'NavigationManager' on type
'Microsoft.AspNetCore.Components.Routing.NavLink'. There is no
registered service of type
'Microsoft.AspNetCore.Components.NavigationManager'.
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.
This Series Exploring the .NET 8 preview Follow me
*
*
*
*
*
*
Enjoy this blog?
* Buy Me A Coffee
* Donate with PayPal
Can you use the .NET 8 Identity API endpoints with IdentityServer?
Previous Can you use the .NET 8 Identity API endpoints with
IdentityServer? Please enable JavaScript to view the comments powered
by Giscus.
Loading...
From Zero to Hero: REST APIs in .NET
ASP.NET Core in Action, Third Edition
My new book ASP.NET Core in Action, Third Edition is available now!
It supports .NET 7.0, and is available as an eBook or paperback.
Enjoy this blog?
* Buy Me A Coffee
* Donate with PayPal
This series Exploring the .NET 8 preview
(c) 2023 Andrew Lock | .NET Escapades. All Rights Reserved. | Image
credits
[ ]
Popular Tags
ASP.NET Core (320) .NET Core (91) DevOps (52) Configuration (50)
Docker (43) Source Code Dive (39) Dependency Injection (36) .NET Core
6 (33) Security (26) .NET Core 3.0 (23) Middleware (22) Source
Generators (22) Auth (20) C# (20) Logging (20) Minimal APIs (18)
Kubernetes (17) Routing (17) ASP.NET Core 2.1 (16) ASP.NET Core
Identity (16) ASP.NET Core 2.0 (15) ASP.NET Core in Action (14) Front
End (14) Git (14) GitHub (14) .NET 7 (13) Testing (13) .NET 8 (12)
NuGet (12) Razor Pages (12) Docker Hub (11) Blazor (10) Hosting (10)
Localisation (10) .NET CLI (9) MVC (9) Razor (9) Installation (8)
Performance (8) AWS (7) Feature Flags (7) HostBuilder (7) Middleware
Analysis Package (7) Middleware as Filters (7) Model Binding (7) .NET
Standard (6) Generic Host (6) PostgreSQL (6) This Blog (6) Visual
Studio (6) Cake (5) CSRF (5) EF Core (5) Error Handling (5)
Versioning (5) xUnit (5) .NET Core 2.0 Preview 1 (4) AOT (4) Azure
DevOps (4) Environment (4) Getting Started (4) IdentityServer (4)
IDEs (4) Image Processing (4) Node.js (4) Twilio (4) .NET Core 2.0
Preview 2 (3) ASP.NET Core 1.1 (3) Issues (3) Multi-tenancy (3) View
Components (3) CORS (2) Datadog (2) Health Checks (2) HttpClient (2)
Mono (2) RC2 (2) Roslyn (2) Session State (2) API Explorer (1)
ASP.NET Core 2.2 (1) Azure Functions (1) Caching (1) Career (1)
Formatting (1) gRPC (1) Notebooks (1) Polly (1) Rancher (1) Raygun
(1) VS Code (1)
Andrew Lock | .Net Escapades close
[appl] Want an email when
there's new posts? Subscribe
Stay up to the date with the latest posts!
Oops! Check your details and try again.
Thanks! Check your email for confirmation.
[ ] [ ] Subscribe