Razor code blocks (experimental)
Curb can format the C# in the @code and @functions blocks of your .razor components and
.cshtml views with the same rules and the same .editorconfig keys as your .cs files. Nothing else in
the file changes: markup, directives, @{ } blocks and expressions stay exactly as you wrote them. It's
experimental and off by default.
Add the key to a section that names Razor files:
[*.{razor,cshtml}]
curb_razor_format_code_blocks = true
The key counts only in a section whose glob names an extension or a file name, such as [*.razor],
[*.{cs,razor,cshtml}] or [Pages/**/*.razor]. In [*], [**] or [*.*] it is ignored, so a catch-all
section can't switch on an experimental feature by accident. A more specific section can turn it off again:
[Legacy.razor]
curb_razor_format_code_blocks = false
Without the key, curb format and curb check never read a .razor or .cshtml file and don't count
them. A file you name yourself (curb check Pages/Counter.razor) is skipped with one line saying Razor
isn't enabled for it. --language razor formats Razor files alone, and still only those that opt in. The
curb MSBuild package never formats Razor files.
A block is formatted with the C# options bound from the Razor file's own settings, exactly as a .cs
file's are. To give both the same style, put your C# keys in a section that covers both:
[*.{cs,razor,cshtml}]
indent_size = 4
csharp_new_line_before_open_brace = all
Blazor components often write [Parameter] public int Count { get; set; } on one line. Curb moves the
attribute to its own line, as it does in a .cs file and as dotnet format does. With max_line_length set,
csharp_place_property_attribute_on_same_line = if_owner_is_single_line keeps the one-line shape.
Three settings work differently from a .cs file, because the markup around a block is never rewritten:
| Setting | In a Razor file |
|---|---|
end_of_line |
Ignored. A block keeps the line endings of the line that opens it, so a file never ends up with two kinds. |
charset |
Ignored. A byte-order mark stays as the file has it. |
file_header_template |
Ignored. A header belongs at the top of a .cs file, not inside a block. |
A block that is indented (inside an element, say) keeps its column. Each member line starts with the @code
line's own leading whitespace exactly as written, followed by one level of indentation in your
indent_style. A tab-indented @code with indent_style = space gives \t int x;, and a
two-space-indented one with indent_style = tab gives \tint x;. max_line_length is measured from the
start of the line.
A block is a line whose first text is @code or @functions, then { and the end of the line (the {
may also stand alone on the next line), up to the } that closes it on a line of its own:
<h1>Counter</h1>
@code {
private int currentCount=0;
private void IncrementCount()
{
currentCount+=1;
}
}
becomes
<h1>Counter</h1>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount += 1;
}
}
Only the lines between the { line and the } line are replaced. The { line, the } line and every
byte outside them are copied from the source.
- Markup, attributes,
@page,@using,@injectand every other directive. @{ }blocks,@if,@foreach,@switchand the other code constructs inside markup.- Implicit and explicit expressions, such as
@countand@(a + b). @codeinside a Razor comment (@* *@) or an HTML comment (<!-- -->), and@@code, which is text.- A block on one line, such as
@code { int x; }. @codein a.cshtmlview. It's a directive in components only; in a view it's an expression followed by markup. Views get their@functionsblocks formatted.
Blocks are looked for in markup only. Every region Razor parses as C# is skipped whole, with its end found by the same C# lexer that matches a block's braces:
@{ }and@( );- an implicit expression with its member accesses, calls and indexers, such as
@Html.Raw(…)or@items[…]; @if/else,@for,@foreach,@while,@do … while,@switch,@using ( ),@lockand@try/catch/finally, with their bodies;- a
@codeor@functionsblock that isn't formatted, such as one with a comment after its{.
So a line reading @code { inside a string, a comment or a body in any of these is never taken for a block.
If Curb can't find where such a region ends (an unclosed @{, or @if without braces), it stops
looking: no block after that point in the file is formatted. The same goes for a @code block with no closing
brace.
Curb refuses a block it can't format safely, leaves it exactly as it is, and still formats the other blocks in the file. The run reports each refusal with its line and reason, and counts them in the summary:
Components/Pages/Grid.razor: line 41: @code block left as written, it does not parse as C# class members (Invalid expression term '<')
…
razor: 58 file(s) — 5 changed, 0 failed, 0 unparsable, 1 block(s) left as written
| Reason | Why |
|---|---|
| it does not parse as C# class members | The block holds Razor rather than C#: a template (@<p>…</p>), <text>, @:, a Razor comment, or markup in a method body. Curb doesn't parse Razor. |
| its closing brace is not alone on its line | Something shares the line with the } that closes the block, such as } <p>… or a stray };. |
| no closing brace | The C# lexer never finds the } that closes the block. |
| it nests too deeply | The same limit as a .cs file. |
| formatting moved the braces around it | The C# options joined the braces of the class the block is formatted in. |
A refused block isn't a failure: the exit code doesn't change, and a file with one is never cached, so the refusal is reported again on the next run.
- The braces are matched with the C# compiler's own lexer, so a
{or}in a string (verbatim, interpolated or raw), a character literal or a comment never ends a block early. - A block's text is formatted as the members of a class, with every check a
.csfile gets: the content verifier and the token-stream comparison. - A multi-line string, the inside of a multi-line comment and an inactive
#ifbranch keep their lines exactly when the block is re-indented. - Before anything is written, Curb scans the output again and requires the same blocks, every byte outside them identical to the source, and each block holding exactly the formatted text. If any check fails, the whole file is reported as failed and left as it was.
Each repository was formatted with the key forced on, then formatted a second time. A separate script with its own block finder masked every block's inner lines and compared the rest of each file byte for byte.
| Repository | Razor files | Blocks | Blocks changed | Left as written | Failed | Markup changed | Second pass |
|---|---|---|---|---|---|---|---|
dotnet/aspnetcore (src/Components, src/Mvc) |
1,260 | 563 | 190 | 5 | 0 | 0 | no change |
| dotnet/blazor-samples | 3,091 | 2,296 | 427 | 16 | 0 | 0 | no change |
| dotnet/eShop | 58 | 23 | 5 | 0 | 0 | 0 | no change |
elastic/docs-builder (.cshtml views) |
242 | 41 | 4 | 13 | 0 | 0 | no change |
Every change in those runs was whitespace. Every refusal was a block holding Razor markup (a template, or
markup in a method body), except two copies of a sample with a stray }; after its last member.
With the key absent, a repository's Razor files cost almost nothing: about 3 ms on dotnet/blazor-samples'
3,080 components, the price of looking for the key in each folder's .editorconfig files.
- Experimental. The output may change between releases.
- Only
@codeand@functionsblocks. There are no plans to format markup. print-configanddoc-treework on.csfiles only.- The C# lexer reads the markup inside an
@ifor@foreachbody as C#, so a brace in that markup's text (<p>{</p>) is counted. The body's end is then not found, and nothing after it in the file is formatted.
See the option reference for the key.