In my previous post I showed how you could statically prerender all the routable components in your Blazor WebAssembly app ahead-of-time so that you could host your app in static-file hosting.
One of the downsides of the approach in that post, is that you have to manually list all of the routes in your app, so the prerenderer can generate each page. In this post I show one way to find all the routes in your application using a bit of reflection.
Recap: static prerendering for WebAssembly apps
Prerendering is where a client-side application is rendered on the server as part of the initial response. It is useful (among other reasons) for improving the perceived speed of a client-side app. Typically, you need to use a "host" ASP.NET Core application to prerender a Blazor WebAssembly app, as I described in a recent post:
In my previous post I discussed the desire for static prerendering of a Blazor WebAssembly app, so that you could host the app using static file hosting. With static prerendering you render all of the pages in your application to raw HTML at build time. This removes the need for a host app, though the approach has various limitations.
In my previous post I described an approach that uses the TestHost
to render all the pages in a sample Blazor WebAssembly app to HTML. The final output of the prerendering stage was the following, in which the index
, counter
and fetch-data
pages have been rendered as HTML index.html files nested inside the appropriate folder.
When combined with the additional WebAssembly publish output, we get:
One of the downsides of the approach in my previous post was that you had to manually list the routes to prerender as data for a Theory
test:
[Theory, Trait("Category", "PreRender")]
[InlineData("/")]
[InlineData("/counter")]
[InlineData("/fetchdata")]
public async Task Render(string route)
In this post, I show how to improve that with a little bit of .NET reflection.
Comparing routable Razor components with standard components
You can create a "routable" component in Blazor using the @page
directive, and adding a route template to the top of your .razor file, for example:
@page "/test"
<h1>Test</h1>
But what does this actually do? To see what the impact of the @page
directive was, I created two components, TestComponent1.razor, which contains the code above, and TestComponent2.razor which contains the code below:
<h1>Test</h1>
The only difference between the components is the @page
directive in the first case. So how can we see the impact?
The easiest approach is to examine the intermediate .cs files generated for each of the components. You can find these files buried deep in the obj folder of your project after you build. For my sample app, these were found in obj/Debug/net5.0/Razor/Pages/:
These intermediate files, suffixed with .g.cs, contain the code for your Razor components. The following is the TestComponent2.razor.g.cs file, tidied up slightly, and with various #pragma
and other symbols removed
// <auto-generated/>
namespace BlazorApp1.Pages
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.AspNetCore.Components.WebAssembly.Http;
using Microsoft.JSInterop;
using BlazorApp1;
using BlazorApp1.Shared;
public partial class TestComponent2 : Microsoft.AspNetCore.Components.ComponentBase
{
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(0, "<h1>Test</h1>");
}
}
}
As you can see, there's not much to it! The Razor Component compiles to a simple class with a BuildRenderTree
method, which describes how to render the component. Obviously for a more complex component this class would be more complex, but it's all very much understandable as C#!
Now lets see what the routable Razor component looks like:
// <auto-generated/>
namespace BlazorApp1.Pages
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.AspNetCore.Components.WebAssembly.Http;
using Microsoft.JSInterop;
using BlazorApp1;
using BlazorApp1.Shared;
[Microsoft.AspNetCore.Components.RouteAttribute("/test")] // <-- the only difference!
public partial class TestComponent1 : Microsoft.AspNetCore.Components.ComponentBase
{
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(0, "<h1>Test</h1>");
}
}
}
The only difference in the C# generated for the routable component is the addition of the [Route]
attribute. We can use that fact to find all the routable components in the app using reflection.
Finding all the routable components in a Blazor WebAssembly app using reflection
In the previous post, I just listed out the routes that need to be prerendered, but we can remove that duplication with a bit of reflection.
We'll start by creating a simple helper class for fetching all the routes to prerender in a Blazor WebAssembly app using some manual reflection. The example below:
- Searches an
Assembly
for all classes derived fromComponentBase
- Filters the list, so only components with a
RouteAttribute
are retained - Extracts the route template from the attribute
public static class PrerenderRouteHelper
{
public static List<string> GetRoutesToRender(Assembly assembly)
{
// Get all the components whose base class is ComponentBase
var components = assembly
.ExportedTypes
.Where(t => t.IsSubclassOf(typeof(ComponentBase)));
var routes = components
.Select(component => GetRouteFromComponent(component))
.Where(config => config is not null)
.ToList();
return new PrerenderRoutes(routes);
}
private static string GetRouteFromComponent(Type component)
{
var attributes = component.GetCustomAttributes(inherit: true);
var routeAttribute = attributes.OfType<RouteAttribute>().FirstOrDefault();
if (routeAttribute is null)
{
// Only map routable components
return null;
}
var route = routeAttribute.Template;
if (string.IsNullOrEmpty(route))
{
throw new Exception($"RouteAttribute in component '{component}' has empty route template");
}
// Doesn't support tokens yet
if (route.Contains('{'))
{
throw new Exception($"RouteAttribute for component '{component}' contains route values. Route values are invalid for prerendering");
}
return route;
}
}
Note that this example only supports simple routes, without route parameters and tokens.
All that remains is to expose the list of routes as a public static member on our GenerateOutput
class, and wrap each route in an object[]
for use with xUnit theory tests.
We can then replace the previous [InlineData]
with a [MemberData]
attribute on the Render
theory test, and the body of the test can remain untouched:
public class GenerateOutput
{
// Get all the pages to render
public static IEnumerable<object[]> GetPagesToPreRender()
=> PrerenderRouteHelper
.GetRoutes(typeof(BlazorApp1.App).Assembly) // Pass in the WebAssembly app's Assembly
.Select(config => new object[] { config }); // wrap each route as an object array to satisfy xUnit
[Theory, Trait("Category", "PreRender")]
[MemberData(nameof(GetPagesToPreRender))]
public async Task Render(string route)
{
// ... see previous post for details
}
}
And that's all there is to it - you can now add routable components as necessary, and you don't have to worry about updating them in the test project too. Much cleaner!
Limitations
In addition to the (many) limitations of static prerendering that I described in my previous post, you should be aware that we're not doing anything fancy here. There's no real validation of the routes happening, to confirm they contain valid values, and we can't handle anything other than literal segments currently.
Initially, I hoped to use the same routing infrastructure that Blazor does for building the route tree, but unfortunately the RouteTableFactory
, RouteTable
, and RouteEntry
etc are all internal
, so working with them would involve more reflection or dynamic nastiness that I just couldn't face. For what I was trying to achieve, my simple approach worked well!
Summary
In this post I show a simple extension to my previous post on static prerendering of Blazor WebAssembly apps. Instead of requiring you to manually enumerate the routes in your application, we use reflection to find all the [Route]
attributes on Razor components in your app, and use that to build the list of possible routes. The result has various limitations, but it works well enough for simple applications, meaning you don't have to remember to update your prerender list every time you add a new routable component.