Safely Rendering Markdown in Blazor

This week I added the ability to post and properly display markdown content in my Blazor (server-side Blazor, actually… Razor Components) project. Markdown is a lightweight, standardized way of formatting text without having to resort to HTML or depend on a WYSIWYG editor. There’s a nice markdown quick reference here.

Leveraging the Work of Others

My need was simple: display formatted markdown on one screen that was saved as plain text on another. Accomplishing this was almost as simple, thanks to the work of those who have gone before me. I used two libraries:

  • Markdig – “a fast, powerful, CommonMark compliant, extensible Markdown processor for .NET.”
  • HtmlSanitizer – “a .NET library for cleaning HTML fragments and documents from constructs that can lead to XSS attacks.”

I also drew from Ed Charbeneau’s fantastic work on BlazeDown, an experimental markdown editor he wrote using Blazor. He first built it back when Blazor was on release 0.2, and he had to do a little extra work to get it to work due to some deficiencies in Blazor at the time (namely, the inability to render raw HTML). The Blazor team added MarkupString for rendering raw HTML with release 0.5, which made the task of rendering markup much simpler. He revisited BlazeDown with release 0.5.1 of Blazor, and updated the project to use the new feature.

This Example

What I’ll show here is just enough code to meet the requirements that I had in my project – simply render a string of in-memory markdown as HTML on the screen and do it safely (more on that later).

The code for this short sample can be found here.

markdown entered as plain text and rendered as HTML

MarkdownView Component

Part of the power of Blazor is the ability to componentize commonly used controls and logic for easy reuse.  I created a MarkdownView Blazor component that is responsible for safely rendering a string of markdown as HTML.

MarkdownView is just two lines:

@inherits MarkdownModel

@HtmlContent

The corresponding MarkdownModel is as follows:

    public class MarkdownModel: BlazorComponent
    {
        private string _content;

        [Inject] public IHtmlSanitizer HtmlSanitizer { get; set; }

        [Parameter]
        protected string Content
        {
            get => _content;
            set
            {
                _content = value;
                HtmlContent = ConvertStringToMarkupString(_content);
            }
        }

        public MarkupString HtmlContent { get; private set; }

        private MarkupString ConvertStringToMarkupString(string value)
        {
            if (!string.IsNullOrWhiteSpace(_content))
            {
                // Convert markdown string to HTML
                var html = Markdig.Markdown.ToHtml(value, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());

                // Sanitize HTML before rendering
                var sanitizedHtml = HtmlSanitizer.Sanitize(html);

                // Return sanitized HTML as a MarkupString that Blazor can render
                return new MarkupString(sanitizedHtml);
            }

            return new MarkupString();
        }
    }

This is a good simple example to demonstrate a few different concepts. First, I’ve specified a service to inject into the component on instantiation, HtmlSanitizer. We’ll discuss this more in a bit, but for now just know that it is a dependency registered with the IoC container.

Second, I’ve specified a parameter, Content, that is bound to a to a property on the model of parent view. This is how I pass a string of markdown into this component.

Third, I’ve exposed an HtmlContent property of type MarkupString. This is the property that will expose the string of markdown converted to a string of HTML that this component will display.

When Content is set, I use a function ConvertStringToMarkupString(..) to convert the string to HTML, sanitize the string of HTML, and return it as a MarkupString.

Usage of the component consists of simply binding it to a string that we want to render:

<MarkdownView Content="@MarkdownContent"/>

Be Safe – Sanitize Your HTML

It’s important to sanitize any user-supplied HTML that you will be rendering back as raw HTML to prevent malicious users from injecting scripts into you app and making it vulnerable to cross-site scripting (XSS) attacks. For this task, I use HtmlSanitizer, an actively-maintained, highly-configurable .NET library. I already showed above how it is injected and used in my MarkdownView component. The only remaining piece is the registration of the HtmlSanitizer with my IoC container in the ConfigureServices method in my Startup class:

            services.AddScoped<IHtmlSanitizer, HtmlSanitizer>(x =>
            {
                // Configure sanitizer rules as needed here.
                // For now, just use default rules + allow class attributes
                var sanitizer = new Ganss.XSS.HtmlSanitizer();
                sanitizer.AllowedAttributes.Add("class");
                return sanitizer;
            });

By making the sanitation of the HTML a part of the MarkdownView component’s logic, I ensure that I won’t forget to sanitize a piece of content as long as I always use the component to render my markdown. It’s also wise to sanitize markdown and HTML on ingress prior to writing it to storage.

Wrapping Up

This was a pretty short example demonstrating how to add a feature that can have a big impact. The tools available to us in Blazor and a couple of existing libraries made this a pretty simple task, which is one of the reasons I’m so excited about Blazor: the ability to leverage existing .NET libraries directly in the browser directly translates to a number of significant benefits including faster delivery times, smaller codebases, lower total cost of ownership, etc…

Thanks for reading!

–Jon


If you’d like to receive new posts via e-mail, consider joining the newsletter.

Experiences Converting from Client-Side to Server-Side Blazor

I’ve been using client-side Blazor for a couple of months now on one of my side projects and I’ve become a pretty big fan, because it allows me to write a modern, dynamic web app in C# with minimal JavaScript.  The Blazor docs give a nice synopsis of how this happens here:

1. C# code files and Razor files are compiled into .NET assemblies.
2. The assemblies and the .NET runtime are downloaded to the browser.
3. Blazor uses JavaScript to bootstrap the .NET runtime and configures the runtime to load required assembly references. Document object model (DOM) manipulation and browser API calls are handled by the Blazor runtime via JavaScript interoperability.

With Blazor I’m able to build single-page applications using my preferred language in a natural and enjoyable programming paradigm.

(Disclaimer:  Blazor is still considered an experimental framework by Microsoft, so proceed with caution only if you are brave, daring, and have a penchant for adventure.  You’ve been warned…)

Server-Side Blazor

In late July 2018, the Blazor team shipped release 0.5.0, which introduced server-side Blazor.  Initially I dismissed server-side Blazor, quite content to continue working completely client-side.  But as I saw that it seemed the team was putting quite a bit of emphasis on server-side Blazor and I read about the benefits it promised over client-side Blazor, I became intrigued.

The release notes do a really nice job of explaining what server-side Blazor is and how it works.  In a nutshell, Blazor was designed to be able to be run in a web worker thread separate from the main UI thread, like this:

Blazor running in a separate web worker thread in the browser.

Server-side Blazor leverages this model and stretches the communication over a network connection, using SignalR to send UI updates, raise events, and invoke JavaScript interop calls between the browser and the server, like this:

Server-side Blazor running on the server and communicating with the Browser via SignalR.

The release notes also provide a good breakdown of the benefits and downsides of the server-side model compared to the client-side model.  I’ll highlight just a couple benefits here that I’ve experienced from the get-go:

  • Faster app load time:  I received early feedback on my client-side Blazor app that it took a long time to load the app initially (on the order of tens of seconds).  This is understandable, as the framework has to ship the entire app, the .NET runtime, and any dependencies down to the client on load.  After switching over to server-side Blazor, my load time went down to sub-second.
  • Much better debugging:  With client-side Blazor there are ways to get basic debugging of the Blazor components working in Chrome developer tools, but it is a far cry from the rich debugging experience that we’re used to in Visual Studio.  I found myself using Debug.WriteLine(..) a lot.  With server-side Blazor, since Blazor component code is running on .NET Core on the server, the Visual Studio debugging tooling just works.
  • Feels like client-side Blazor:  Apart from the improved load time and debugging support, server-side Blazor is almost indistinguishable from client-side Blazor to both the developer and the end-user.  As we’ll see in a moment, apart from a couple of small changes at startup, you develop a server-side Blazor app just like a client-side Blazor app: composing Blazor components in exactly the same way regardless of where they will be running.  And the end-user still has a rich, interactive SPA experience.

Now the downsides to server-side Blazor are what you might expect: increased latency on every UI interaction, no offline support, and scalability limited by the number of client connections that SignalR can manage.  It’s too early for me to speak to the scalability concern, but increased latency is mostly imperceptible to the user given a decent internet connection.

Converting a Solution to Server-Side Blazor

Converting a client-side Blazor solution to server-side Blazor is much easier than you might expect and can be done in a matter of minutes.  (In fact, Suchiman has a demo showing how to dynamically switch between client-side and server-side at runtime based on a querystring parameter.)

A server-side Blazor application consists of two projects, typically named {SolutionName}.App and {SolutionName}.Server, both created by the VS tooling when you create a new server-side Blazor application.  I already had a client-side project that I had named ProjectX.Web.  The first step I took was Add > New Project… > ASP.NET Core Web Application on my solution:Visual Studio Add New Project dialog - ASP.NET Core Web Application

After entering my desired name and clicking Next, I selected Blazor (Server-side in ASP.NET Core):New ASP.NET Core Web Application - Blazor (Server-side in ASP.NET Core)

This added two new projects to my solution: ProjectX.App and ProjectX.Server.  Since I already had a ProjectX.Web project representing the client-side piece, I deleted the newly created ProjectX.App project.  I made ProjectX.Server the startup project and added a reference in it to my existing ProjectX.Web.

In my index.html in ProjectX.Web, I replaced this:

<script src="_framework/blazor.webassembly.js" />

With this:

<script src="_framework/blazor.server.js" />

In ProjectX.Server.Startup.cs, there are two places where it was referencing App.Startup; I changed them to use Web.Startup instead.

public void ConfigureServices(IServiceCollection services)
{
    services.AddServerSideBlazor<ProjectX.Web.Startup>();

    // snip...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // snip...

    app.UseServerSideBlazor<ProjectX.Web.Startup>();
}

… and that’s all there was to it!  Well, almost…

Gotchas

I ran into a few minor issues that you may or may not encounter, depending on what you’re doing in your application.

  1. Blank Screen After App Load

    After making the changes described above, I built and ran my project.  The loading screen displayed as expected, but then it just displayed a blank white screen with no errors logged to the console.

    Following the guidance in this thread, I tried removing the global.json file that was created when I created by client-side project, which pinned the SDK version to 2.1.3xx.  This didn’t help in my case.  I then checked my .NET Core SDK version by running dotnet --info at the VS Developer Command Prompt.  I was running 2.1.500-preview-009335.  I deleted the preview version of the SDK and then my app started loading and running.

  2. JS Errors Invoking Blazor Component Methods using .invokeMethod

    With my app running now, I started testing some of the functionality.  I have a couple of controls that invoke methods on my Blazor components via JavaScript that I found were no longer working.  A quick peek at the Chrome dev tools console showed the culprit (thank you, Blazor team, for good error messages!):
    Uncaught Error: The current dispatcher does not support synchronous calls blazor.server.js from JS to .NET. Use invokeMethodAsync instead.Easy fix: I just changed the couple of spots where I was using .invokeMethod(..) to invoke methods on my Blazor components to .invokeMethodAsync(..), and most of them started working.

  3. JSInvokable Methods Not Marked Virtual

    Even after switching to use .invokeMethodAsync(..), I still had a couple of Blazor component methods that were failing to be invoked from my JS.  I found that the difference between the ones that worked and the ones that didn’t was the virtual keyword on the method declaration.  I added virtual to the methods that weren’t executing and they started working.

That said, sitting here a few days later, I tried removing the virtual keyword from those JSInvokable methods again, and they continue to work.  So I’m not sure why this worked in the first place or if I had another change at that time that actually fixed it (I don’t think so).  Your mileage may vary…

Wrapping Up

Switching my solution from client-side to server-side Blazor was a piece of cake, and the benefits were well worth it.  I can now see the reasons for the recent buzz around server-side Blazor.  If I get to the point where I need to support a larger number of concurrent SignalR connections, I’ll start using Azure SignalR Service and route the communication through it, as described in the Blazor 0.6.0 release notes, but for now I’m content to run it through my app service.


If you’d like to receive new posts via e-mail, consider joining the newsletter.