Announcement

Blazor and Razor in VS Code: C# Tooling for .razor Files

A polished metal @ sculpture glowing with amber fire above layered glass, edged in cyan light

SharpLsp now understands Razor. Blazor components (.razor files), and Razor Pages and MVC views (.cshtml files), get the same Roslyn engine as the rest of your C#. You get hover, completion, diagnostics, go to definition, references and rename, inside the markup. It ships in SharpLsp 0.30.

Here it is on a small Blazor Web App:

Narrated, with original music and captions. Recorded in VS Code on a Blazor Web App: Roslyn hovers in the markup, completion inside a Razor expression, HTML completion and hover, a C# error on its Razor line, F12 into the @code block, references and rename, F12 from Program.cs into App.razor, then linked tag editing and folding. Watch on its own page

What is Blazor? Blazor vs Razor in one minute

The two names describe different layers, which is why they are easy to mix up.

Under all three sits one Razor compiler, which turns every Razor file into C#. That is why one implementation in SharpLsp serves Blazor Server, Blazor WebAssembly, Razor Pages and MVC alike.

What you get in a .razor file

Everything below is shown on the Basket.razor page of the demo in the video, and each item is covered by an automated test.

The markup around the C# is still HTML. In VS Code, SharpLsp forwards element and attribute completion and HTML hovers to VS Code's own HTML language service. It sends an HTML view of the file in which every C# fragment is blanked, so the HTML service never mistakes C# for markup.

How it works: the Razor compiler is the source of truth

The tempting way to build Razor tooling is to write a Razor parser. SharpLsp doesn't. Razor's semantics belong to the Razor compiler, and any second parser would drift from it with every release.

Since .NET 6, the Razor compiler has been a C# source generator. It ships in the .NET SDK, and the C# sidecar's design-time build runs it exactly as dotnet build does. The generator turns each Razor file into a C# class. Every C# fragment is copied into that class verbatim, inside an enhanced #line directive that points back at the Razor text.

SharpLsp builds a two-way map from those directives:

  1. A request at a Razor position maps to the same character in the generated C#.
  2. Roslyn answers there, exactly as it would for a .cs file.
  3. Every location in the answer maps back to the Razor file.

The map never trusts a directive blindly. Roslyn reports each mapped span's end one character past the fragment, so SharpLsp measures every fragment by comparing the Razor text with the generated text, character by character. A position in code the generator wrote by itself, such as the __builder calls that render the markup, has no Razor text and never reaches you.

Because the generator is the compiler's, the C# inside every construct it maps gets the same treatment. That includes @bind values, event handlers, generic components and @code blocks. The markup side, which drives folding, the outline, linked editing and the HTML view, comes from the compiler's public intermediate representation, not from a reimplementation.

Found while building it

Running the real host over a fresh dotnet new blazor app found one false report: every @using in _Imports.razor was faded as unnecessary. Roslyn only sees each file's own generated C#, so it can't see that an import file's usings serve the components beside and below it. A component's own usings had the same problem when only its markup needed them. Razor files no longer report unnecessary usings, and C# files, code-behind included, still do.

The second finding is about SDK versions. The Razor generator comes from the SDK that builds your project, which is the newest one installed unless a global.json pins another. SDK 10.0.401's generator needs a newer Roslyn than the one SharpLsp bundles, so at first its Razor files got no C# features (issue #399). SharpLsp now loads the Razor compiler it bundles in that generator's place, and logs a warning that names both Roslyn versions, so a newer SDK works without a pin. The Blazor and Razor docs have the details.

Blazor on .NET 10: Server, WebAssembly and Razor class libraries

SharpLsp reads the projects your solution already has. They all load the same way, through Microsoft.NET.Sdk.Web, Microsoft.NET.Sdk.BlazorWebAssembly or Microsoft.NET.Sdk.Razor:

ASP.NET Core 10 on the .NET 10 SDK is verified, including a fresh dotnet new blazor app. Projects on net8.0 and net9.0 compile Razor through the same source generator, so they should work, but we haven't verified them yet. Projects older than .NET 6 compile Razor without a source generator and aren't supported, and neither are .NET Framework's MVC 5 and Web Forms.

What's next

This release covers the C# in Razor files. The markup itself is next, tracked in issue #397:

Try it

  1. Install SharpLsp from the VS Code Marketplace, or from Open VSX in Cursor, Windsurf and VSCodium. Razor support needs version 0.30 or later.
  2. Create a Blazor Web App with dotnet new blazor -o BlazorDemo, and open the folder.
  3. Open Components/Pages/Counter.razor and hover currentCount.

The Blazor and Razor docs list every feature, the supported projects and what is not supported yet.

Further reading