blog post image
Andrew Lock avatar

Andrew Lock

~6 min read

Using Source Generators with Blazor components in .NET 6

In this post I describe a problem I discovered when upgrading my Blazor app to .NET 6. Source generators that were working fine in .NET 5 failed to discover the Blazor components in my app, due to changes in the Razor compilation process. In this post I describe why it happened and show a workaround.

Using Source Generators with Blazor in .NET 5

About 6 months ago, I wrote a series of posts on using source generators with a Blazor app. The goal was to introspect the Blazor app, to generate a dictionary of "top level" routes that we could use for other purposes.

Using the techniques described in those articles, I was able to create a Blazor WebAssembly app in which each of the "entry" components in the app are statically rendered to HTML. That gives static prerendering, meaning you get pre-rendered output, but you can host the app on a static hosting provider like Netlify, without hosting in an ASP.NET Core app (which is normally required for prerendering).

The output after prerendering is complete

Another benefit of using the source generators I showed was auto-generating the menu component for an app. The source generator finds all the routable components, and automatically adds a new entry to the menu as required.

Using source generators to create a menu component

I've been happily using both these techniques. That is, until I installed the .NET 6 preview and tried upgrading my app.

Oh noes, .NET 6, you broke my app!

.NET 6 brings lots of "quality of life" features for developers, most notably, .NET Hot Reload. As described in this announcement post hot reload gives a much nicer experience for running .NET applications, including Blazor and ASP.NET Core apps.

You can see it in action on the community standup, among other places.

One of the major changes in Blazor for .NET 6 is a switch from using a two-stage custom compilation process for Razor to using source generators instead. This can massively reduce build times for both Blazor and traditional MVC apps:

Improvement in compilation time in .NET 6. Image taken from ASP.NET Core updates in .NET 6 Preview 2

These are great benefits, so I was keen to give them a try. Updating my Blazor app from .NET 5 to .NET 6 was easy, just changing a few numbers, but when I tried running it, my source generators were completely broken. The menu generator source generator was permanently empty, as though there were no routable components in the app. Or at least, my source generator couldn't find them.

Broken source generator nav in .NET 6

So what happened?

Source generators can't use the output of other source generators

The problem is that my source generators were relying on the output of the Razor compiler in .NET 5. As I described in my previous post, my source generator was looking for components in the compilation that are decorated with [RouteAttribute]:

[RouteAttribute("/test")] // <-- only present for routable copmonents
public partial class TestComponent1 : Microsoft.AspNetCore.Components.ComponentBase
{
    protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
    {
        __builder.AddMarkupContent(0, "<h1>Test</h1>");
    }
}

In .NET 5, the Razor compiler generates these components from the Razor files as a first step, and then the "main" compilation step, including my source generator, executes.

With .NET 6, the Razor tooling is a source generator, so there is no "first step"; the Razor tooling executes at the same time as my source generator. That is great for performance, but it means the files my source generator was relying on (the generated component classes) don't exist when my generator runs.

Note that this is by design. Source generators "Run un-ordered, each generator will see the same input compilation, with no access to files created by other source generators."

Given I definitely wanted to update to .NET 6 eventually, I ran through my options.

What are the work arounds?

My first option was to change the design of the source generator. Instead of relying on the output of the Razor compiler/source generators, I could use the input files, i.e. the .razor component files. This would work, but essentially would require me to re-implement the Razor compiler source generators for myself! That seemed like way more work than I was up for, so I raised an issue on GitHub.

Another "solution" would be for the c# compiler to have a concept of "ordering" for source generators, so that the output of one source generator (e.g. the Razor compiler), could be made available to subsequent generators (e.g. my source generator). Apparently the ASP.NET team have asked the compiler team to look at supporting ordering, but that doesn't help us for now.

We've asked the compiler team to look at adding support for ordering source generators which should also allow generators to look at the output from previously generated items. We're still hopeful they would be able to do some tactical, but it's more likely that this would be unable to resolve it for the 6.0 / VS 2022 release.

The only really workable solution at the moment is to go back to the previous Razor compilation method. That means my source generators will still work, but I'll lose all the build time benefits of using source generators. I also can't use .NET Hot Reload😢

Disabling the Razor source generators in .NET 6

If you're in a similar situation to me, and you need to use the old Razor compiler instead of the new Razor source generators, you can switch to the legacy version by setting the UseRazorSourceGenerator property in your project:

<PropertyGroup>
  <UseRazorSourceGenerator>false</UseRazorSourceGenerator>
</PropertyGroup>

For example, in a Blazor WebAssembly app (using .NET 6 preview 6), your project file would look something like this:

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <!-- Add this line 👇 -->
    <UseRazorSourceGenerator>false</UseRazorSourceGenerator>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0-preview.6*" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0-preview.6*" PrivateAssets="all" />
    <PackageReference Include="System.Net.Http.Json" Version="6.0.0-preview.6*" />
  </ItemGroup>

</Project>

or a Blazor server app project file might look something like this:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <!-- Add this line 👇 -->
    <UseRazorSourceGenerator>false</UseRazorSourceGenerator>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0-preview.6*" />
  </ItemGroup>

</Project>

In any case, adding the line immediately fixed my app. The menu component was generated correctly again, even if it took a bit longer than it might have done otherwise.

Source generator is working again

Ah well, here's hoping I can reenable it in .NET 7!

Summary

In this post I described an issue I discovered in .NET 6 when using a source generator that relies on the generated Razor component output. .NET 6 switched the Razor compilation to use source generators, which means the output isn't available to other source generators. To resolve the issue I set the property UseRazorSourceGenerator to false, to use the old Razor compilation process with .NET 6. You lose the build speed improvements, and can't use .NET Hot Reload, but at least my app builds correctly now!

Andrew Lock | .Net Escapades
Want an email when
there's new posts?