Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Rendering Blazor components to a string: Exploring the .NET 8 preview - Part 9

$
0
0

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

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<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 🎉

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<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:

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.


Viewing all articles
Browse latest Browse all 743

Trending Articles