Using .NET 7's Blazor Custom Elements to render dynamic content
October 5, 2022 · 6 minute read · Tags: blazor
I’m currently working a new version of the “courseware” that powers https://practicaldotnet.io.
As part of that I want a way to write course content using markdown, but still embed Razor components.
For example, I have a Blazor component to show “Code Examples”, complete with navigation and the ability to view each file’s contents:
I want to use markdown to create pages for a course, which can include this component like this:
---
title: Button Components Complete Source
---
Here's the complete source for the various buttons we've ended up with.
Notice I also wired up the "No" button in the "ButtonDanger" example to hide the confirmation UI.
<FileViewer id="REUSE01_Buttons"></FileViewer>
The idea being to convert that markdown to html at runtime, also rendering the Blazor component for that FileViewer
in the browser when someone views this “page”.
But is this even possible, and if so, how?
The simple bit (markdown to HTML)
Markdig is a great library which makes light work of converting markdown to HTML:
Markdown.ToHtml("markdown content")
With this I can have a backend API read the contents of the markdown file, convert it to HTML, and send that up to the Blazor Client.
The Blazor client will get HTML back from the API which looks something like this:
<h2>title: Button Components Complete Source</h2>
<p>Here's the complete source for the various buttons we've ended up with.</p>
<p>Notice I also wired up the "No" button in the "ButtonDanger" example to hide the confirmation UI.</p>
<FileViewer id="REUSE01_Buttons"></FileViewer>
Which it can render as a raw HTML string:
(MarkupString)@content
The problem
If we run this in the browser at this point we’ll see headings and paragraphs, but the browser hasn’t a clue what to do with the FileViewer
tag, so it will simply ignore it.
The challenge here lies in the way Razor components work.
When you create a Blazor component and build your app, the Razor component is turned into a CSharp file (via source generation).
Here’s a simplified example of what a part of that generated output looks like:
// <auto-generated/>
namespace LearnUI.Components.Content.Widgets.FileViewer
{
public partial class FileViewer : global::Microsoft.AspNetCore.Components.ComponentBase
{
protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
__builder.OpenElement(0, "div");
__builder.AddAttribute(1, "class", "panel");
...
}
}
}
This all happens at compile-time.
Say you have a page, called “HelloWorld.razor” and on that page you reference an instance of the FileViewer
component.
<h2>Hello World</h2>
<FileViewer />
The generated CSharp for “HelloWorld” will include code to render the FileViewer
component.
namespace LearnUI.Components.Content.Widgets
{
...
[global::Microsoft.AspNetCore.Components.RouteAttribute("/HelloWorld")]
public partial class HelloWorld : global::Microsoft.AspNetCore.Components.ComponentBase
{
protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
__builder.OpenComponent<global::LearnUI.Components.Content.Widgets.FileViewer.FileViewer>(0);
__builder.CloseComponent();
}
}
}
This code then gets shipped to the client (specifically the browser, if using Blazor WASM), at which point the .NET framework (running in the browser) takes on the job of routing requests for “/HelloWorld” to this code, which renders the FileViewer
component.
However, in this case, I don’t want to create a .razor
file for every page of my course.
Rather, I want to fetch course contents from .md files on the server, and there’s no obvious way to take references to Blazor Components embedded within that content and render them at runtime.
Or is there?!
Custom Elements To The Rescue?
.NET 7 introduces official support for registering your Blazor components as custom elements.
You can see some examples of how this works in this BlazorCustomElements github repo.
Custom elements are part of the HTML spec and make it possible for the HTML parser in your browser to recognise “non standard” elements.
For example, you could create an Avatar
component and write a little bit of JavaScript to register it as an HTMLElement:
class Avatar extends HTMLElement {
...
}
From there you could use this in your HTML and whenever the HTML parser saw the <avatar />
tag it would construct a new instance of the Avatar
class.
You can read more about custom elements in the HTML spec here.
From a Blazor perspective, we can now register Blazor components as custom elements and use them on any HTML page (including via other web frameworks like React and Angular).
To make this work you need to reference the CustomElements NuGet package:
dotnet add package Microsoft.AspNetCore.Components.CustomElements --version 7.0.0-rc.1.22427.2
Then you can register your component as a CustomElement in Program.cs (Blazor Client App).
options.RootComponents.RegisterCustomElement<FileViewer>("file-viewer");
In Index.html
or _Host.cshtml
(depending on whether you’re using Blazor WASM or Server) you’ll also need a reference to the CustomElements JavaScript file.
_Host.cshtml
...
<!-- add this -->
<script src="_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
With that setup in place it’s now possible to render the FileViewer component via its custom element tag:
<file-viewer id="REUSE01_Buttons"></file-viewer>
But what about that markdown I want to use?
We need to use the correct tags (for the custom element) in the markdown content:
---
title: Button Components Complete Source
---
Here's the complete source for the various buttons we've ended up with.
Notice I also wired up the "No" button in the "ButtonDanger" example to hide the confirmation UI.
<file-viewer id="REUSE01_Buttons"></file-viewer>
And we get this in the browser:
Here’s the entire flow for viewing a “course page”:
- The Blazor Client requests the relevant page content from the API
- The backend API returns HTML (which includes
<file-viewer>
tags) - The Blazor Client renders that raw HTML
- The Browser recognises the
<file-viewer>
tag and renders the custom element - Behold, the
FileViewer
is rendered
Caveats (always think security)
One slight downside of all this is that there’s nothing to check that I’m referencing the custom element correctly. If I misspell <file-viewer />
or pass an incorrectly named parameter, there’s no compiler to point out my stupidity.
A bigger (and more important) concern is security.
As ever, it’s important to think carefully about the security holes this kind of dynamic content can open up.
This becomes especially important if you enable users to edit content (in this case markdown) via the Blazor client.
You need to consider what a malicious user could do with the ability to render your custom elements.
For example, it’s feasible that someone could write markdown which includes an instance of the <file-viewer>
component, and try to navigate “up” to parent folders, and retrieve source code which you probably don’t want them to access!
This can be mitigated by making sure your custom elements, especially where they make calls to the backend, are protected by employing standard security measures, like:
- Checking the user’s permissions to perform actions
- Avoiding directly constructing things like SQL scripts, or even folder paths, from user input
- Only registering custom elements that are safe to expose
In and of themselves custom elements don’t present any more risk than any other client-side frameworks/code but it always pays to think about the security implications of that shiny new feature you’re adding to your site :)
In Summary
Blazor’s upcoming support for Custom Elements opens up some interesting possibilities for rendering “dynamic” content, including Blazor components.
By registering your Blazor components as Custom Elements you can use them in the browser, including in other web frameworks like Angular and React, but also in “dynamic” content where you’re rendering raw HTML in your Blazor app.
It’s important to watch out for unintended security implications, especially if you enable users to write their own content which renders custom elements.