Skip to content

Getting started

From install to the first dotnet format run. Pick the path that fits:

Both use two packages: StyleBro.Analyzers (the rules, referenced by your projects) and StyleBro.Migrate (the stylebro-migrate command line tool, used once to write settings):

Which version

The latest release on nuget.org is 0.7.0-alpha.1, a prerelease: install the tool with --prerelease and reference that exact version. This page describes main, which matched that release when it shipped; what changed since then ships in the next one and is listed in changes/. To try the code on main before the next release, build both packages from a clone into a local folder (any version string works; 0.8.0-dev.1 here):

git clone https://github.com/bisforboman/stylebro
dotnet pack stylebro/src/StyleBro.Package -o stylebro-feed -p:Version=0.8.0-dev.1
dotnet pack stylebro/src/StyleBro.Migrate -o stylebro-feed -p:Version=0.8.0-dev.1
dotnet tool install --global StyleBro.Migrate --add-source stylebro-feed --version 0.8.0-dev.1

Then add the folder as a package source in your repository's nuget.config and use 0.8.0-dev.1 wherever this page says 0.7.0-alpha.1. When the nuget.config has a <packageSourceMapping>, the new source needs a mapping too: NuGet only looks for a package in the sources mapped to it, so the source alone gives NU1102 (package not found):

<configuration>
  <packageSources>
    <add key="stylebro" value="path/to/stylebro-feed" />
  </packageSources>
  <packageSourceMapping>
    <!-- the mappings already there stay -->
    <packageSource key="stylebro">
      <package pattern="StyleBro.*" />
    </packageSource>
  </packageSourceMapping>
</configuration>

Preview first

Before changing anything, --diff shows what a path would do to your code. It works on all three commands:

stylebro-migrate init --diff          # path A: init's settings, then format
stylebro-migrate path/to/repo --diff  # path B: the migration's settings (--write), then format
stylebro-migrate format --diff        # only the format run

The preview copies the repository to a temporary folder (git's tracked and untracked files, not the ignored ones; the working trees of submodules too), runs the command there, then stylebro-migrate format until a run changes nothing, and deletes the copy. Your repository isn't touched. If it doesn't reference StyleBro.Analyzers yet, the copy gets the reference from step 1 (the tool's own version) and the output says so. It prints a summary and writes the full diff to .git/stylebro-preview.patch in the repository's git folder, where it isn't an untracked file (outside a git repository: the temp folder; --diff=other.patch names another file). --keep keeps the copy and prints where it is; --all passes on to format; with several solutions or projects in the folder, --project MySolution.sln names the one to format. --no-restore skips the restore in the copy: it takes the repository's own restore output, so run dotnet restore first (it's ignored when the preview adds the package, which needs a restore). Where the preview adds the reference under central package management: next to StyleCop's (a GlobalPackageReference gets a GlobalPackageReference, in the same Directory.Packages.props), else a version-less PackageReference with the version in the Directory.Packages.props that applies (also one that a folder's Directory.Packages.props imports). FFMpegCore, stylebro-migrate init --diff:

Previewing 'stylebro-migrate init --write' and 'stylebro-migrate format' on a copy; nothing in C:\src\FFMpegCore changes.
Copying the repository to C:\Users\me\AppData\Local\Temp\stylebro-preview-fmsw0dps.3qa\FFMpegCore... 0s
Settings: 31 lines in .editorconfig; 3 lines in Directory.Build.props (StyleBro.Analyzers)
  StyleBro.Analyzers wasn't referenced: added StyleBro.Analyzers 0.7.0-alpha.1 to Directory.Build.props for the preview, as docs/getting-started.md says (the patch includes it).
Finding what format fixes ('dotnet format --verify-no-changes')... 16s
Format run 1... 29s
Format run 2... 14s
Format: 124 files, 579 reported changes, clean after 1 run
  BRO1603  Documentation text should end with a ...    65 files
  BRO1001  Members should be ordered                   33 files
  BRO1401  Use a trailing comma in multi-line in...    33 files
  ...
  ... and 24 more rules
Sample (BRO1603, FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs):
  -///     Custom exception for FFMpegDownloader
  +///     Custom exception for FFMpegDownloader.
Sample (BRO1401, FFMpegCore.Examples/Program.cs):
  -            FrameRate = 30 //set source frame rate
  +            FrameRate = 30, // set source frame rate
Full diff: .git\stylebro-preview.patch (5,966 lines)
Done in 1m 2s.

Files are counted under every rule dotnet format --verify-no-changes reported in them before the fixes; a file with only whitespace changes counts as IDE0055, one with nothing reported (another fix's side effect) as other. The patch is what the real commands write (byte for byte on FFMpegCore); git apply .git/stylebro-preview.patch applies it.

A. A new project, or no StyleCop

1. Add the package

Add it to every project, for example in Directory.Build.props at the repository root:

<Project>
  <ItemGroup>
    <PackageReference Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" PrivateAssets="all" />
  </ItemGroup>
</Project>

With central package management, the version goes in Directory.Packages.props and the reference has none:

<!-- Directory.Packages.props -->
<ItemGroup>
  <PackageVersion Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" />
</ItemGroup>

<!-- Directory.Build.props -->
<ItemGroup>
  <PackageReference Include="StyleBro.Analyzers" PrivateAssets="all" />
</ItemGroup>

A project that turns central package management off itself (<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>, often an analyzer or source generator project) fails its restore with the version-less reference (NU1015). Give such projects a reference with a version, next to the version-less one:

<!-- Directory.Build.props -->
<ItemGroup>
  <PackageReference Include="StyleBro.Analyzers" PrivateAssets="all" Condition="'$(ManagePackageVersionsCentrally)' == 'true'" />
  <PackageReference Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" PrivateAssets="all" Condition="'$(ManagePackageVersionsCentrally)' != 'true'" />
</ItemGroup>

MSBuild imports only the nearest Directory.Build.props above a project: if a folder (say src/) has its own and it doesn't import the root one, put the reference there (or in each such file). If it does import it (GetPathOfFileAbove, or a property like Orleans' $(_ParentDirectoryBuildPropsPath)), the root's reference is enough: a second one fails the restore (NU1504, duplicate reference). A reference inside a conditional ItemGroup (Condition="'$(TargetFramework)' == 'net8.0'") reaches only that target framework, and <RunAnalyzers>false</RunAnalyzers> turns all analyzers off where it applies. --diff adds the reference to every Directory.Build.props the projects use and names the projects whose restore output has no StyleBro.Analyzers (a code base that already follows every rule reports nothing, and that's fine).

With lock files (<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>), the next restore adds StyleBro.Analyzers to every packages.lock.json: commit them with the reference. Where <RestoreLockedMode>true</RestoreLockedMode> is on (always, or in CI), that restore fails instead (NU1004, "the package references have changed"): update the lock files once with locked mode off, dotnet restore -p:RestoreLockedMode=false, and commit them. --diff does this in its copy by itself, and its patch shows the updated lock files.

Two things that get in the way of a package from a local feed (trying a build before it's on nuget.org):

  • A <RestoreSources> property (often in Directory.Build.props) replaces the sources of every nuget.config: a feed added there is never asked, and the restore fails with NU1101/NU1102. Add the feed to RestoreSources too (<RestoreSources>$(RestoreSources);C:\feed</RestoreSources>), or use RestoreAdditionalProjectSources.
  • Projects a test run generates inside the repository (say under artifacts/ or a test's temp folder) inherit the root Directory.Build.props, and with it the StyleBro reference, which their own restore may not find. Leave them out with a condition on the reference, e.g. Condition="!$(MSBuildProjectDirectory.Contains('artifacts'))", or give that folder a Directory.Build.props of its own that doesn't import the root one.

The package brings StyleBro's rules and its preset: rule severities and formatting options, close to StyleCop's defaults. Your own .editorconfig wins over the preset. The preset has a version, and stylebro-migrate init --write (step 2) pins it in Directory.Build.props (<StyleBroPreset>1.0</StyleBroPreset>), so a later package upgrade adds no new rules until you raise it (versioning). To configure everything yourself instead: <StyleBroPreset>none</StyleBroPreset>.

2. Turn on the built-in rules

StyleBro relies on some built-in .NET rules (IDE0055 formatting, IDE0036 modifier order, IDE0049 type aliases, ...). Their severities have to be in .editorconfig, because dotnet format ignores rule severities in a package's preset. In the repository root:

stylebro-migrate init            # shows the block
stylebro-migrate init --write    # adds it to .editorconfig (created if missing)

stylebro-migrate init --diff shows what the block and the format run in step 3 would change, without writing anything (Preview first).

The block sits between # BEGIN stylebro-migrate and # END stylebro-migrate; running the command again replaces it. Put your own settings outside it. To get these rules reported by the build too, add <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> to Directory.Build.props (init says so when no project file sets it yet).

An .editorconfig that silences analyzers in bulk (dotnet_analyzer_diagnostic.severity = silent, or a category's severity below warning) would hide StyleBro's rules from dotnet format, while the build still reports them at the preset's severities. init then writes each rule the preset turns on by its id, so both see the same rules; a rule your .editorconfig sets by id stays as you set it. stylebro-migrate format never says clean while such a bulk setting hides rules from it.

A folder whose own .editorconfig turns every analyzer off (dotnet_analyzer_diagnostic.severity = none or silent, with or without root = true; test samples, for example) is left alone instead: init writes StyleBro's rules, IDE0055 and the other built-in rules it manages as none by id there (a rule's own entry wins over the bulk one, in the preset as in the root block), so the build reports nothing there and format doesn't touch those files, not even their whitespace. A rule that folder's .editorconfig sets by id stays as you set it. These lines go into a block between # BEGIN stylebro-folder and # END stylebro-folder (in a root = true folder: its stylebro-migrate block, with nothing else in it). Your own IDE0055 severity (say suggestion) is kept everywhere: the block doesn't set it then, and format still formats whitespace, except where your severity is none (by id, for the Style category or in bulk): format leaves the whitespace of those files alone, as it does where a block turns IDE0055 off.

init also looks at your code

init looks at your code first and keeps the conventions it clearly follows. For each setting below it counts both forms in your C# (generated code, EF Core migrations, vendored folders and submodules don't count; code inside #if counts too, as format sees it in the target framework that defines the symbol):

Vendored folders are recognized by their name (vendor, third_party, ...). Code copied in under another name (a src/Sdk from another product) counts as yours and can decide a convention; mark its folder generated_code = true in your root .editorconfig ([src/Sdk/**], see excluding vendored code) and run init again: folders marked that way don't count.

  • fewer than 3 places (or none): too few to tell, StyleBro's default stays and nothing is written;
  • 3 to 9 places: followed when they all agree;
  • 10 places or more: followed when one form has at least 75 %;
  • mixed (no form gets there): the StyleBro rule that enforces the setting is turned off, so the code stays as it is (off: dotnet_diagnostic.BRO1520.severity = none; both: for braces on one-line bodies csharp_prefer_braces = when_multiline, which allows both). Where no StyleBro rule enforces the setting ({ placement, <inheritdoc/> spacing, default(T)), the verdict is mixed: the default stays and nothing is turned off.

A followed form that is StyleBro's default needs no line; any other is written into the block. A key your root .editorconfig sets wins, so does a Sonar setup's, and a dotnet_naming_rule for private fields decides the field style (naming rules for interfaces or constants don't). One exception: when your .editorconfig sets an SDK key that only a StyleBro rule enforces (operator placement, braces = true, using placement, arithmetic parentheses = always_for_clarity) and your code clearly does the other thing, nothing ever enforced that key. Then init leaves the key alone and turns that rule off (your .editorconfig says end_of_line, ... so BRO1520 is off), instead of letting it rewrite the code. The same goes when the code mixes both forms under such a key (the starter kit says beginning_of_line, 486 operators start the line and 285 end it): nothing enforced the key, so the rule goes off as for any mixed convention (.editorconfig says beginning_of_line, unenforced, code mixed: BRO1520 is off). Running init again keeps such a line, even when format changed the counts.

A key your .editorconfig sets at severity silent or none (csharp_prefer_braces = true:silent, or dotnet_diagnostic.IDE0011.severity = silent next to it) isn't enforced by anything: it's what the IDE wrote, not what the team checks. init treats it like a key that isn't set and follows the code; where the code's form differs from the key, it writes the code's form into the block, which comes later in the file and wins (also when that form is StyleBro's default). This applies to every convention whose key takes a severity: braces (IDE0011), using placement (IDE0065), arithmetic parentheses (IDE0048), default values (IDE0034) and operator placement.

Folders with their own root = true .editorconfig (a sample/ with its own solution): the root's settings never reach their files, since root = true stops the lookup there. init gives each such folder with C# code a block of its own in its .editorconfig, with the built-in rule severities, judged from that folder's code and settings alone (the report has a section per folder). A folder with too little code to tell keeps StyleBro's defaults, and the report says so. stylebro-migrate --write writes the whole block into such a folder too.

A few settings work a little differently:

  • One-line statements (if (a) return;, case 1: a(); break;): the preset sets csharp_preserve_single_line_statements = false, which moves each to a line of its own. From 3 such statements on, init writes true, majority or not, unless your .editorconfig sets the key.
  • <inheritdoc/> on overrides and implementations (BRO1601): in projects that generate documentation, init counts the overrides and interface implementations BRO1601 checks, with and without a doc comment. Mostly documented: the rule stays on; mostly undocumented or mixed: it's turned off.
  • Line endings count files. Without end_of_line, format writes your OS's line ending into the lines it rewrites (on Windows CRLF into LF files), so init writes the files' ending when it's clear and nothing when it's mixed. Files git converts on checkout (core.autocrlf, or a .gitattributes text attribute without eol: LF in the repository, CRLF in a Windows checkout) don't count, and the report names which of the two converts them: there the OS's ending is what the checkout has, and a fixed one would make format rewrite every file on the other OS. When git converts at least as many files as it leaves alone, those are the convention and nothing is written: a few files committed with CRLF don't decide for the rest (one trial repository had 3 such files next to 145 converted ones, and end_of_line = crlf from those 3 meant 12,593 line-ending errors in an LF checkout). stylebro-migrate --write writes end_of_line the same way. When your .editorconfig sets end_of_line but git converts the files on checkout (end_of_line = lf with *.cs text=auto means CRLF files in a Windows checkout), init keeps endings out of it: end_of_line = unset for C# files and IDE0055 as a suggestion, so neither format nor the build touches them, and the report says how to settle it (*.cs text eol=lf in .gitattributes, or removing end_of_line). When your end_of_line says one ending but the files git leaves alone clearly have the other (crlf, with nearly every file LF), nothing ever enforced it: init writes the files' ending for C# files, so format doesn't flip every file it touches, and the report says how to switch to the key's ending instead.
  • s_/t_ prefixes count private static fields (constants aside) with and without the .NET runtime's s_ (or t_) prefix. Mostly without: BRO1307 stays on; mostly with or mixed: it's turned off, so format doesn't rename them (there is no switch for static fields alone, so m_ fields keep their prefix too). A dotnet_naming_rule of yours that requires s_ decides instead.
  • Type names count files: files that name built-in types only with keywords (string, int), and files with a framework name IDE0049 would rewrite (String, System.Int32). Mostly keywords: IDE0049 stays on; mostly framework names or mixed: it's turned off. Your own dotnet_style_predefined_type_* keys decide instead.
  • Member order (BRO1001) counts types whose members BRO1001 would sort apart (two or more members with different sort keys), with your own order settings: already in its order or not. A type that is in order once overloads stay together counts as sorted (whether they do is the overloads convention's call). Mostly sorted: the rule stays on; mostly unsorted or mixed: it's turned off, so format doesn't reorder code that was never sorted (StabilityMatrix: 370 sorted, 414 not). Only type members count: BRO1001 also sorts the types of a namespace, but most files hold one type, so they would say little.
  • Unsigned literal suffixes (BRO1135) count u/ul suffixes in upper or lower case (0U or 0u); L alone doesn't count, since nobody writes 1l. Mostly upper case: the rule stays on; mostly lower case or mixed: it's turned off (Orleans: 423 upper, 185 lower). With it off, BRO1122 writes 6u/1ul instead of 6U/1UL.
  • Items of split lists (BRO1108) count argument and parameter lists whose items start on more than one line: each item on its own line, or grouped (F(tag, 1, and "a", "b") on the next line). Mostly one per line: the rule stays on; mostly grouped or mixed: it's turned off (metadata-extractor: 104 one per line, 197 grouped).
  • Implicit private (BRO1404) counts the members of classes, structs and records that are private when nothing says otherwise (fields, methods, properties, events, constructors, nested types; not what BRO1404 skips: partial members, explicit implementations, static constructors): with private or without an access modifier. Mostly with: the default stays; mostly without: dotnet_style_require_accessibility_modifiers = omit_if_default, so neither BRO1404 nor BRO1007 adds modifiers (IDE0040, which would remove them, is off in init's block); mixed: BRO1404 is turned off. Interface members are counted apart: when the .editorconfig says always (which asks for public on interface members too), the build doesn't enforce it (no EnforceCodeStyleInBuild), and the code's interface members are mostly without an access modifier or mixed, init writes for_non_interface_members, so BRO1404 adds no public there (BaGetter: none of its 102 interface members had one).
  • Implicit internal (BRO1404) counts the types in a namespace or a file (not partial ones): with internal or without an access modifier. No setting asks for modifiers on types only, so mostly without, or mixed, turns BRO1404 off (AngleSharp: 273 types without, 26 with internal).
  • case blocks count blocks below a case label (case X:, then { on the next line): indented one level below the label or at its level. Mostly at its level: csharp_indent_case_contents_when_block = false, so the SDK formatter (IDE0055) keeps them (AngleSharp: all 131 at the label's level).
  • Combined declarations (BRO1142 for locals, BRO1114 for fields) count declarations of two or more (int n = h.Length, i = 0;) and one-variable declarations right after one of the same type and modifiers they could have been combined with (not var, not fields with attributes). Mostly one by one: the rule stays on; mostly combined or mixed: it's turned off, so format doesn't split them. Locals and fields are counted apart: a repository may combine locals in hot loops and never fields.
  • Combined attributes (BRO1102) count declarations with two or more attributes (not parameters or type parameters): in one list ([A, B]) or one per list. Mostly one per list: the rule stays on; mostly combined or mixed: it's turned off, so format doesn't split them.
  • Summary wording (BRO1604 for properties, BRO1606 for constructors) counts summaries that start the StyleCop way (Gets ..., Gets or sets ...; Initializes a new instance of ...) against ones in their own words (The samples, ..., Whether ...; Opens a file ...). Mostly the StyleCop way: the rule stays on; mostly their own words or mixed: it's turned off, so format doesn't reword public documentation. A summary that starts with an element (<see cref="..."/>) isn't counted.
  • CSharpier: when the repository uses CSharpier (a .csharpierrc, .csharpierrc.json, .csharpierrc.yaml or .csharpierrc.yml, or a csharpier tool in .config/dotnet-tools.json or dotnet-tools.json, at the root or in a solution's or project's folder or a folder above it; or a CSharpier.MsBuild package reference), the two formatters would undo each other's layout (StabilityMatrix's pre-commit check rejected 161 files StyleBro had formatted). init then turns off IDE0055 (so stylebro-migrate format skips the whitespace pass; when CSharpier formats only some folders, IDE0055 is off in those folders' sections, one section per folder CSharpier formats, and the whitespace pass leaves just them out) and every StyleBro rule for something CSharpier decides, and the layout conventions in the table are reported as set (CSharpier formats it). Off:
  • line breaks in lists: BRO1107-BRO1110, BRO1116-BRO1119; constraints BRO1111, constructor initializers BRO1105, enum values BRO1121, query clauses BRO1127-BRO1130;
  • wrapping: BRO1520-BRO1524 (operators, =>, =, call chains, ?:);
  • single-line blocks and members, accessors: BRO1508-BRO1510, BRO1527;
  • the blank lines CSharpier normalizes itself: before { and else/catch/finally (BRO1501, BRO1502), after { (BRO1503), before } (BRO1518), before a do-while's while (BRO1512), runs of them (BRO1517), and the ones CSharpier removes where StyleBro would add them: before a comment (BRO1504: CSharpier removes it inside initializers and above else) and between elements (BRO1505: CSharpier removes it between accessors).

Everything else stays: naming, member order, documentation, braces on one-line bodies, readability rules, and the blank lines CSharpier keeps as you wrote them (after }, after attributes, around doc comments, between switch sections). init also writes stylebro_layout_by = csharpier and CSharpier's line width (stylebro_layout_print_width: printWidth from the .csharpierrc, else max_line_length, else 100): an if merged into the enclosing one (BRO1149) gets its condition the way CSharpier writes it, and the brace rules count a statement CSharpier will split over lines as multi-line, so its braces come in the same run. When CSharpier formats only one folder (its tool or config there, with C# code outside it), these settings go into that folder's section ([Source/**.cs]) and the rest of the code is judged as usual. CSharpier sorts using directives itself (System first), so init writes no sort key (dotnet format sorts usings whenever dotnet_sort_system_directives_first or dotnet_separate_import_directive_groups is set, and its order differs from CSharpier's in places: App3._0 against App3_0). Where your .editorconfig sets one (false, or true for the groups), init writes the closer value in its block: a key can't be unset. Run CSharpier after format: other fixes that rewrite code (attributes split, parentheses added) leave lines in StyleBro's layout, which CSharpier then lays out its way, and StyleBro leaves that alone: after CSharpier, stylebro-migrate format changes nothing more. stylebro-migrate --write (from StyleCop) does the same. To have StyleBro format instead, remove CSharpier and run init again. - Running init again keeps the convention lines the first run wrote (the ones below # Conventions in this repository's code, with their # init: comments), and the report says kept from the last init. They describe your code as it was before StyleBro formatted it: counted again after a format run, StyleBro's own output would decide (a rule that added blank lines in some places would make your code look mixed, and the setting would flip). To have a convention counted again, delete its line from the block and run init again; to count everything again, delete the block. Conventions the first run left at StyleBro's default (no line) are counted again each time. - Private constants and static readonly fields (BRO1306) count three forms: PascalCase, camelCase and _camelCase. A clear one is written as stylebro_private_static_field_naming (PascalCase is the default and needs no line); mixed turns BRO1306 off, so format doesn't rename them (Ical.Net: 26 PascalCase, 8 camelCase, 50 _camelCase; BRO1306 renamed them in 28 files). With it off, public and internal fields aren't checked either. A dotnet_naming_rule for constants or static readonly fields decides instead. - Usings inside a namespace (BRO1126) count fully qualified ones (using Company.App.Common;) and ones relative to an enclosing namespace (using Common; inside namespace Company.App.Orders). Mostly qualified: the rule stays on; mostly relative or mixed: it's turned off (Fitnet: 91 qualified, 92 relative). A name is relative when an enclosing namespace has a namespace your code declares with its first part; a namespace from a package doesn't count. - File headers with empty // lines: when your file_header_template starts or ends with a line break (IDE0073 writes an empty // line there), or most files' headers start or end with an empty // line, init turns BRO1120 off: it would remove those lines, and every new file from the template would bring them back (Ical.Net: 222 of 223 files). - Charset and final line breaks: when your .editorconfig sets charset (utf-8 or utf-8-bom) or insert_final_newline but the C# files don't clearly follow it (BOM or not, last line break or not, the same 75 %), nothing ever enforced it, and format would add or strip the BOM or the last line break in every file. Like a line ending the files contradict, init writes the files' value for C# files (unset when they're mixed: each file keeps its own), and the report says how to switch (remove the line and run stylebro-migrate format). Ical.Net: charset = utf-8 with 108 of 223 files starting with a BOM, all 108 stripped; the starter kit: insert_final_newline = false with 1,092 of 1,597 files ending in a line break, all 1,092 removed. stylebro-migrate --write does the same. trim_trailing_whitespace = false can't be kept this way: dotnet format doesn't read the key and trims trailing whitespace on every line it formats, so the report only says so (with IDE0055 off nothing is trimmed). These keys (and end_of_line) are judged per file against the .editorconfig that decides it, as the compiler reads them: the nearest one that sets the key wins, within it the last matching section. A nearer file overrides anything the root block says, so when a folder's own .editorconfig (one without root = true, like Bogus's Source/) sets the key, init writes its line there, in a block between # BEGIN stylebro-folder and # END stylebro-folder (Bogus: the root block's insert_final_newline = unset never reached Source/, and format stripped 48 files' last line break). - Spacing that IDE0055 enforces counts places on one line: a space after if/for/foreach/while/switch/ using/lock/catch or not (if (x) or if(x)), spaces inside parentheses (control-flow statements, parenthesized expressions, casts), after a cast ((int) x), and inside call and parameter lists (F( x )). A clear form other than the SDK's default is written (csharp_space_after_keywords_in_control_flow_statements, csharp_space_between_parentheses, csharp_space_after_cast, csharp_space_between_method_call_parameter_list_parentheses, csharp_space_between_method_declaration_parameter_list_parentheses). When any of them is mixed, IDE0055 is turned off with one # init: spacing: note, since it can't be limited to one option, and format skips the whitespace pass (Bogus: foreach( var x in y ) in 341 places, 65 %, which IDE0055 had rewritten). With CSharpier, column alignment or an IDE0055 severity of your own, nothing is written. - Blank lines between members (BRO1505) count the neighbouring members of a type that need one (one spans several lines, or is documented), with a blank line between them or without; blank lines above doc comments (BRO1513) count doc comments right below code or after a blank line. Mostly with: the rule stays on; mostly without or mixed: it's turned off, so format doesn't add blank lines to compact code (Kavita: 392 and 312 files). - Blank lines below // comments (BRO1506) count the comments it checks (the last line of a comment group; not the file header, trailing comments or comments below code) right above the code or with a blank line below. Mostly right above: the rule stays on; mostly with a blank line or mixed: it's turned off (RecyclableMemoryStream keeps one below commented-out tests). - this.: when your .editorconfig requires it (a dotnet_style_qualification_for_* key = true:warning, or = true with IDE0009 at suggestion or above, by its id, the Style category's severity or the bulk one), the block leaves IDE0009 alone instead of turning it off. - Interface members: dotnet_style_require_accessibility_modifiers = always asks for public on interface members too. When your code doesn't have them and the build doesn't enforce it (EnforceCodeStyleInBuild with IDE0040 at warning or error by id, category or bulk; a :warning after the value doesn't count in the build), init writes for_non_interface_members, so BRO1404 doesn't add them. - else/catch/finally placement counts only blocks that span lines: in a one-line method ({ if (x) { return 1; } else return 2; }) the layout isn't a choice. - Blank lines below doc comments (BRO1511) count doc comments right above their element or with a blank line between. Mostly right above: the rule stays on; mostly with one or mixed: it's turned off (MoreLINQ). - Primary-constructor parameters (BRO1302) count those named like parameters (logger) or like the fields they replace (_logger). Mostly _logger or mixed turns BRO1302 off: it has no switch for these parameters alone, so it's then off for every parameter (the # init: line says so). - Tuple element names (BRO1311) count (int Count, ...) and (int count, ...). Mostly camelCase writes stylebro_tuple_element_name_casing = camelCase; mixed turns the rule off. - Preset version: init --write sets <StyleBroPreset> in each Directory.Build.props your projects read, but not in one whose projects no solution lists and that doesn't mention StyleBro.Analyzers (example or test-fixture folders). A Directory.Build.props it has to create below another one imports that one, so nothing the projects read gets lost. - Run from a subfolder: when the folder you run init (or stylebro-migrate --write) in is part of a git repository whose build reaches past it (a Directory.Build.props, Directory.Packages.props, .editorconfig or .globalconfig above it, or a solution in it that lists projects outside it, like build/App.sln), it works on the repository's root and says so. A self-contained folder stays on its own. - .globalconfig: the root's .globalconfig counts like its .editorconfig for what your team set and enforces (a this. it requires, its IDE0055 severity, the conventions' keys). With AnalysisMode All and EnforceCodeStyleInBuild, every code-style rule is a warning in the build, so a dotnet_style_qualification_for_* = true without a severity counts as enforced. - A folder that turns the Style category off (dotnet_analyzer_diagnostic.category-Style.severity = none in its .editorconfig): a rule's own entry wins over a category, so init turns the built-in rules its block sets by id off there too (in the folder's own block), and says so. - Column alignment: when your .editorconfig aligns code in columns for ReSharper or Rider (resharper_int_align_variables = true, resharper_csharp_int_align_fields = true and the other int_align keys; the comments' alignment aside), IDE0055's whitespace formatting would flatten it (RestSharp: var doc = CreateAttributesXml(); became var doc = CreateAttributesXml(); in 171 files). init turns IDE0055 off, with a # init: comment, so stylebro-migrate format skips the whitespace pass. No StyleBro rule changes the spaces inside a line, so they stay on. An IDE0055 severity of your own wins. - One-line jumps (if (x) return;, also break, continue, throw, goto, yield break on the if line): from 3 of them on, init writes stylebro_allow_single_line_jump_statements = true, majority or not, so BRO1514 doesn't brace them (the starter kit: 878 of 1,130 bodies with braces, and BRO1514 braced 194 one-line jumps). - #regions inside members (BRO1113, the starter kit's #region Arrange / Act / Assert in its tests): from 3 of them on, the rule is turned off, so format doesn't remove them. BRO1112 (regions between members) is off in the preset already. - Using order counts files with 2 or more usings: sorted with System first, sorted without, or not sorted. dotnet format sorts usings whenever dotnet_sort_system_directives_first is set (to any value), so the preset doesn't set it: init writes it when your usings are sorted (or there are too few files to tell), and only a comment when they aren't, or when some files put System first and others don't. When your .editorconfig sets the key itself and 3 or more files aren't in its order, the report says how many format will reorder and that removing the key keeps them (the starter kit: false, 262 of 1,120 files); init can't do it for you, since unset sorts them too. - No root .editorconfig without code for it: when all your C# sits in folders whose .editorconfig says root = true (the starter kit's src/) and the root has no .editorconfig, init writes none there. Likewise the preview adds the package reference only where projects with C# files read it (a template pack project without code doesn't get a root Directory.Build.props).

Setting What's counted Rule Key
Private field names field or _field BRO1303 stylebro_private_field_naming
{ placement { of a multi-line block on its own line or at the end of the line SDK formatter (IDE0055) csharp_new_line_before_open_brace
else/catch/finally after } on a new line or the same line SDK formatter (IDE0055) csharp_new_line_before_else, _catch, _finally
Braces one-line if/else/for/foreach/while/using/lock bodies with or without braces BRO1514-BRO1516 csharp_prefer_braces
Operator placement binary and ?: operators at the start or end of a wrapped line BRO1520 dotnet_style_operator_placement_when_wrapping
=> placement => of expression bodies and switch arms where a line wraps BRO1521 stylebro_arrow_placement_when_wrapping
= placement = where a line wraps BRO1522 stylebro_equals_placement_when_wrapping
Trailing commas multi-line initializers, enums and switch expressions BRO1401 stylebro_trailing_comma
Empty strings string.Empty or "" (outside constant contexts) BRO1106 stylebro_empty_string_style
Null checks is null or == null BRO1133 stylebro_null_check_style
Summaries one-line <summary> texts with the tags on their own lines or on one line BRO1616 stylebro_summary_layout
<inheritdoc/> <inheritdoc/> or <inheritdoc /> BRO1601 stylebro_inheritdoc_style
Default values default(T) or default IDE0034 csharp_prefer_simple_default_expression
) of split lists after the last item or on its own line BRO1110 stylebro_closing_parenthesis_placement
First item of split lists on the next line or after ( BRO1107 stylebro_split_list_first_item
Items of split lists one per line or grouped on lines BRO1108 dotnet_diagnostic.BRO1108.severity
Leading multi-line lambdas calls whose first argument is a multi-line lambda, anonymous method or anonymous object: the other arguments on its last line (}, Kind);) or one argument per line (mixed: both allowed) BRO1107, BRO1108 stylebro_allow_leading_multiline_lambda
Constructor initializers : base(...)/: this(...) on its own line or the constructor's BRO1105 stylebro_constructor_initializer_placement
Constraints where on its own line or the declaration's BRO1111 stylebro_constraint_placement
new T { ... } with or without () before an initializer BRO1141 stylebro_object_creation_parentheses
Switch sections after a blank line or without one BRO1526 stylebro_blank_line_between_switch_sections
Using directives files with a namespace and their usings on one level: inside it (file-scoped too) or outside; only inside_namespace is written, the preset already says outside_namespace BRO1008 csharp_using_directive_placement
Arithmetic parentheses a + (b * c) or a + b * c BRO1406 dotnet_style_parentheses_in_arithmetic_binary_operators
One-line statements a braceless if/else/loop body or a case label's statements on their owner's line or below it SDK formatter (IDE0055) csharp_preserve_single_line_statements
Documented overrides overrides and interface implementations with or without a doc comment BRO1601 dotnet_diagnostic.BRO1601.severity
Member order types whose members BRO1001 sorts apart: already in its order or not BRO1001 dotnet_diagnostic.BRO1001.severity
Overloads overload groups whose members BRO1001 sorts to different places (by access, static, ...): next to each other or apart BRO1001 stylebro_keep_overloads_together
Unsigned literal suffixes u/ul suffixes in upper or lower case (0U or 0u) BRO1135 dotnet_diagnostic.BRO1135.severity
s_/t_ prefixes private static fields with or without the s_/t_ prefix BRO1307 dotnet_diagnostic.BRO1307.severity
Implicit private members that are private by default, with or without private BRO1404 dotnet_style_require_accessibility_modifiers
Implicit internal types that are internal by default, with or without internal BRO1404 dotnet_diagnostic.BRO1404.severity
Partial parts partial class X parts with or without an access modifier BRO1007 dotnet_diagnostic.BRO1007.severity
case blocks a block below a case label indented or at the label's level SDK formatter (IDE0055) csharp_indent_case_contents_when_block
Combined attributes [A, B] or one attribute per list BRO1102 dotnet_diagnostic.BRO1102.severity
Combined locals int a, b; or one local per declaration BRO1142 dotnet_diagnostic.BRO1142.severity
Combined fields int _a, _b; or one field per declaration BRO1114 dotnet_diagnostic.BRO1114.severity
Type names files with only keywords (int) or framework names (Int32) IDE0049 dotnet_diagnostic.IDE0049.severity
Private constants and static readonly fields PascalCase, camelCase or _camelCase BRO1306 stylebro_private_static_field_naming
Usings inside a namespace fully qualified or relative to an enclosing namespace BRO1126 dotnet_diagnostic.BRO1126.severity
File headers headers with or without an empty // line at the top or bottom (or a file_header_template with one) BRO1120 dotnet_diagnostic.BRO1120.severity
One-line jumps if (x) return; on the if line, or a lone jump in braces BRO1514 stylebro_allow_single_line_jump_statements
Empty blocks { } on one line or on two (mixed: both allowed) BRO1508, BRO1509 stylebro_allow_empty_single_line_blocks
One-line properties properties with a block-bodied accessor on one line (X { get { return x; } }) or on several BRO1509 dotnet_diagnostic.BRO1509.severity
Accessor bodies multi-line properties whose accessors all have block bodies: all one-line or all multi-line, or mixed BRO1510 dotnet_diagnostic.BRO1510.severity
Auto-property accessors { get; set; } on the declaration's line or on several lines BRO1527 dotnet_diagnostic.BRO1527.severity
#regions inside members the #regions inside members (3 turn the rule off) BRO1113 dotnet_diagnostic.BRO1113.severity
Column alignment your resharper_*int_align_* keys set to true SDK formatter (IDE0055) dotnet_diagnostic.IDE0055.severity
Charset files with or without a BOM, when the charset that applies to them doesn't match them SDK formatter charset
Final line breaks files with or without a last line break, when the insert_final_newline that applies to them doesn't match them SDK formatter insert_final_newline
Line endings files with LF or CRLF line endings (not files git converts on checkout) SDK formatter end_of_line
Using order files with 2 or more usings: sorted with System first, sorted without, or not sorted dotnet format dotnet_sort_system_directives_first
Spacing if (x) or if(x); spaces inside parentheses, after casts, inside call and parameter lists (mixed: IDE0055 off) SDK formatter (IDE0055) csharp_space_after_keywords_in_control_flow_statements, csharp_space_between_parentheses, csharp_space_after_cast, csharp_space_between_method_call_parameter_list_parentheses, csharp_space_between_method_declaration_parameter_list_parentheses
Member separation neighbouring members that need a blank line, with or without one BRO1505 dotnet_diagnostic.BRO1505.severity
Blank line above doc comments doc comments right below code or after a blank line BRO1513 dotnet_diagnostic.BRO1513.severity
Blank line below comments // comments right above code or with a blank line below BRO1506 dotnet_diagnostic.BRO1506.severity
Blank line below doc comments doc comments right above their element or with a blank line between BRO1511 dotnet_diagnostic.BRO1511.severity
Primary-constructor parameters named logger or _logger (classes and structs) BRO1302 dotnet_diagnostic.BRO1302.severity
Tuple element names (int Count, ...) or (int count, ...) BRO1311 stylebro_tuple_element_name_casing

The keys are described in Settings. init prints each verdict (kept, default, off, both, mixed, too few, or set when your configuration decides) with the value that applies, then a summary that names the keys (RealWorld):

Conventions in the code (judged from 3 places: all must agree below 10, 75% from 10 on).
  off: mixed, or not what your setting says, so the rule that enforces it is turned off; both: mixed, both allowed;
  mixed: the default stays, nothing is turned off (no StyleBro rule enforces it, or your .editorconfig sets the rule):
  too few  private fields: 2 places -> default stays (camelCase)
  set      '{' of multi-line blocks: 196 of 196 on its own line -> set in .editorconfig (all)
  too few  operators where a line wraps: 1 place -> default stays (beginning_of_line)
  kept     multi-line initializers: 18 of 18 without a trailing comma -> omit
  default  empty strings: 3 of 3 string.Empty -> string_empty
  off      null checks: 7 'is null', 10 '== null' -> BRO1133 is off
  off      split lists: 13 first item on the next line, 17 first item after '(' -> BRO1107 is off
  ...
Summary:
  Kept your style for 5 settings: wrote stylebro_trailing_comma; 4 were already StyleBro's default.
  Turned 2 rules off because your code mixes both forms: BRO1133 (null checks), BRO1107 (split lists).
  To choose later: set the key in .editorconfig, remove its init line, run 'stylebro-migrate format'.
  4 settings had fewer than 3 places to tell, StyleBro's defaults stay: stylebro_private_field_naming,
    dotnet_style_operator_placement_when_wrapping, stylebro_equals_placement_when_wrapping, stylebro_summary_layout.

Each written line gets a comment in the block saying why:

# init: multi-line initializers: without a trailing comma in 18 of 18 places in your code
stylebro_trailing_comma = omit
# init: null checks: your code mixes both forms (7 'is null', 10 '== null'), so BRO1133 is off. To choose one: set stylebro_null_check_style and remove the next line.
dotnet_diagnostic.BRO1133.severity = none

Changing a detected setting later

Set the key in your own part of .editorconfig, outside the block (for example stylebro_null_check_style = equality_operator), remove the rule's severity = none line from the block, and run stylebro-migrate format to bring the code in line. The block is rewritten each time init runs, but a key you set yourself wins over what it counts: it then writes neither the setting nor the severity = none line.

EF Core migrations (files with [Migration(...)], a Migration base class or a ModelSnapshot) are written by dotnet ef, so init marks their folders generated_code = true: formatting and StyleBro leave them alone. Vendored folders (vendor, vendored, third_party, thirdparty, external) get the same section.

When it finds a StyleCop setup (stylecop.json, StyleCop rule ids in .editorconfig, rulesets or global configs, a StyleCop.Analyzers reference), it stops without writing and tells you to run stylebro-migrate --write instead: that's path B, which turns StyleBro's rules on only where StyleCop enforced them.

With SonarQube or SonarCloud (a SonarAnalyzer.CSharp reference, Sonar rule severities in rulesets or configs, or a quality profile you export and pass with --sonar-profile profile.xml), init also turns on the StyleBro and .NET rules that fix what your Sonar rules report (S927 -> BRO1313, S4136 -> overloads kept together, ...) and prints which: Coming from SonarQube. The same works for stylebro-migrate --write in path B.

3. Run stylebro-migrate format

stylebro-migrate format

This fixes whitespace, the built-in rules and every StyleBro rule. One fix can make work for another rule, so the command runs dotnet format again until a run changes no file (at most three runs) and prints what each run changed (Run 1: 293 files changed: and the first 10 of them, Run 2: 0 files changed, clean.); --once runs it once, with the same report and the findings kept in that run. It ends with the findings StyleBro's fixes leave on purpose, each with its reason (Findings kept on purpose), and exits with 0 when clean, 2 when a run still changed files. When dotnet format skips a project because its references didn't load (a broken restore or package cache), the command names it and says the run is incomplete (for one target framework only: the code only that framework compiles); --diff stops then instead of showing a partial preview, and --verify-no-changes fails when a project wasn't loaded in any run (a project that references an F# project, which dotnet format can't load, stays a warning). When no project has StyleBro.Analyzers in its restore output (obj/project.assets.json), the command stops before dotnet format runs: without the analyzers it would fix only whitespace and the built-in rules and report the code clean. Projects without it next to ones with it are named (Without StyleBro: ...). The command restores once before the runs; that restore skips NuGet's audit (-p:NuGetAudit=false): vulnerability warnings (NU1901-NU1904) are the build's business, and with TreatWarningsAsErrors they failed every run. A solution filter (.slnf) works like its solution, with only its projects. A solution with several filters of it next to it (Steeltoe's Steeltoe.All.slnx and its seven .slnf parts) counts as that solution; the command says how to name a filter instead. A folder with several solutions or projects needs one named (stylebro-migrate format MySolution.slnx): the command lists them instead of picking one. With none at the root, it looks for a Traversal project there (dirs.proj with Sdk="Microsoft.Build.Traversal", which dotnet format can't load: format runs on a temporary solution with the projects it lists), then for solutions one folder down (src/MySolution.slnx): the only one is used, and the command says so (Using src/MySolution.slnx); several are listed, as at the root. baseline and --diff find them the same way. If dotnet format crashes, the command names the projects of that run, and when the crash came after some files were written, lists every file it changed (Run 1 failed after changing 12 files:). A solution in a folder of its own (solutions/App.slnx listing ../src/...) works too: the command runs from the folder that holds all its projects. Where the repository enforces IDE0005 (unnecessary usings, by id, its Style category or the bulk severity), the command fixes it too: other fixes (UInt32 to uint) can leave a using unused. After the runs it names the generated files git tracks in the projects it changed (*.g.cs, *.Designer.cs, a T4 template's output, files with an <auto-generated> header): X is generated and checked in: rebuild and commit it. Where CSharpier formats the repository, it ends with a reminder to run CSharpier. Review the diff and commit it. In an established codebase, land that commit when few pull requests are open (they will conflict with it), and add its hash to .git-blame-ignore-revs so git blame skips it (GitHub reads that file too; locally: git config blame.ignoreRevsFile .git-blame-ignore-revs). In a large codebase you may not want one big change: see Large codebases.

Why not plain dotnet format? It also applies every other analyzer's fixes and the compiler's own, and some of those change behavior or break the build: on one trial repository it added required to properties (CS8618) and a Sonar fix removed init; accessors, nine build errors. stylebro-migrate format passes dotnet format only StyleBro's rule ids and the built-in rules init or the migration turn on (--diagnostics ...); whitespace formatting runs as usual. It also runs once per target framework where needed (Multi-targeted repositories) and never touches git submodules. The plain equivalent is dotnet format --diagnostics with the ids to fix:

dotnet format --diagnostics BRO1001 BRO1505 BRO1601 IDE0055 IDE0036 BRO1008

stylebro-migrate format --all applies everything, like plain dotnet format.

What to expect on the first run: solution names, run time, warnings as errors.

4. Check it in CI

Run stylebro-migrate format --verify-no-changes --severity warn after the build: it changes nothing and fails (exit code 2) when formatting would change a file. Findings kept on purpose don't fail it; plain dotnet format --verify-no-changes fails on those too, so a script or an AI agent that loops "until clean" never gets there with it. Set-up for GitHub Actions and Azure Pipelines: ci.md. init and the migration name the workflow lines (.github/workflows, azure-pipelines.yml) that still run plain dotnet format --verify-no-changes. To check the staged files before each commit: Pre-commit hook.

With --agents-md, init --write also puts a short StyleBro section into AGENTS.md for AI coding agents (opt-in). For scripts and agents there are format --files (just the files an edit touched) and --json: Scripts and AI agents.

B. Coming from StyleCop

Still deciding? StyleBro vs StyleCop compares the two with measured numbers.

stylebro-migrate reads your StyleCop setup (rulesets, global configs, .editorconfig files, stylecop.json) and writes StyleBro and SDK settings that enforce the same things, so code StyleCop was happy with stays as it is. A walk-through on a small project, with real output: samples/StyleCopMigration.

1. Dry run

stylebro-migrate path/to/repo

It prints what it read, the settings it would write, and the StyleCop rules nothing enforces after the switch (dropped by design, partly covered, or not covered yet). Nothing is changed. To also see what the settings and the format run would do to the code: stylebro-migrate path/to/repo --diff (Preview first).

2. Write the settings

stylebro-migrate path/to/repo --write

This writes the settings into your .editorconfig files (between the stylebro-migrate markers), turns StyleBro's preset off in the root Directory.Build.props (the settings replace it), and adds the replacing rule ids to existing StyleCop suppressions (#pragma warning disable, [SuppressMessage], <NoWarn>). You don't need init after this: the block includes the built-in rules.

3. Swap the package

Remove the StyleCop reference and add StyleBro.Analyzers instead, as in path A, step 1. Look for both package ids, StyleCop.Analyzers and the prerelease StyleCop.Analyzers.Unstable, and for a GlobalPackageReference in Directory.Packages.props (central package management): replace it with <GlobalPackageReference Include="StyleBro.Analyzers" Version="..." />. stylecop.json and the StyleCop suppressions can stay; StyleBro ignores them.

4. Run stylebro-migrate format

stylebro-migrate format

Why not plain dotnet format: step 3 of path A. In a StyleCop-clean repository this should change little or nothing. Look at what it did change: usually code StyleCop missed, or a rule where StyleBro deliberately differs (differences-from-stylecop.md). Then check the report from step 1 for rules your team relied on that nothing enforces any more. Details: migrating.md. CI: ci.md.

Newer C# and APIs (optional)

To also rewrite code into newer C# and newer APIs (target-typed new(), collection expressions, file-scoped namespaces, ArgumentNullException.ThrowIfNull, ...) with the SDK's own rules, after either path:

stylebro-migrate init --modernize --write
dotnet format

In multi-targeted projects, StyleBro's multi-target guard hides the newer-API rules where a target framework lacks the API, and the newer-C# rules are suggestions unless the project sets LangVersion. Tiers, examples and the rules left out: modernizing.md.

What to expect on the first run

  • Name the solution when the root has several. With more than one project or solution file in the folder (say Build.csproj next to MySolution.slnx), dotnet format stops with MSB1011 ("more than one project or solution file"). Pass the one to format: dotnet format MySolution.slnx (the same for stylebro-migrate format).
  • Multi-targeted repositories take longer. stylebro-migrate format runs dotnet format once per target framework, and each run loads the solution again. That's minutes, not seconds: Dapper, which builds in 13 seconds, took 345 seconds to format.
  • TreatWarningsAsErrors fails the first build until everything is fixed or recorded: StyleBro's findings are warnings, so they become errors. Run dotnet format (or stylebro-migrate format) before building, or record what's left in a baseline.
  • Some renames are left to you. The naming rules don't rename a member whose name also appears in a string (reflection, serialized names, DebuggerDisplay): the rename would compile but break at run time. Those warnings stay after dotnet format: see Findings kept on purpose.

Findings kept on purpose

Every StyleBro finding has a fix, but a fix can see more than the analyzer: the whole solution, other projects included. When a change would break something only the fix can see, it leaves the finding, the warning stays, and the fix offers no code action for it. That's the one exception to "dotnet format fixes everything StyleBro reports", and it's on purpose. The reasons, per finding:

  • the name is in a string elsewhere in the solution (serialized names and test JSON for properties and public fields; for other fields only where code reaches a member by name: GetField("_count"), GetProperty(...), Type.GetType(...), [MemberData("...")], an attribute's Name = "..."): the rename would compile but break at run time;
  • the name is in a nameof(...) elsewhere, whose text would change with it (an HTTP header name, a log key);
  • another member would hide the new name, or the new name already means something else where the name is used;
  • a use is in generated code (a source generator, a Razor page, a designer file), which a tool writes again;
  • a related override or implementation is public API, or also implements a member that isn't renamed;
  • the rename would add compile errors (tuple element names), or the namespace is also defined outside the solution.

stylebro-migrate format lists them after its last run:

Run 2: 0 files changed, clean.
Clean: 1 finding kept on purpose (listed below). StyleBro's fixes leave these: renaming by hand breaks what the check protects. Check those uses first, or suppress or baseline them (...).
  src/Counter.cs(5,17): BRO1303 Rename '_count' to 'count'. Kept: the name is in a string in the solution (reflection, serialization): renaming would compile but break at run time.

What to do with one: don't rename by hand to silence it (that's what breaks the reflection or the generated code). Check the uses the reason names; if they can change too, rename both by hand. Otherwise suppress it with a reason (#pragma warning disable BRO1303 // read by reflection in Tests), or record it in the baseline.

With TreatWarningsAsErrors the kept findings are build errors after the first run, so make dealing with them part of it: run stylebro-migrate format, then either suppress each one as above or run stylebro-migrate baseline (it records exactly what is left, the kept findings included), and commit that together with the formatting. The list is printed after every run, also when format ends "Not clean".

Exit codes of stylebro-migrate format, for CI and agents:

Exit code Plain run --verify-no-changes
0 clean: the last run changed nothing (kept findings don't count) formatting would change nothing; only kept findings are left
2 still changing after three runs formatting would change a file
1 a project no run loaded (Incomplete:): its files weren't checked
other dotnet format failed (load or build errors) the same

--verify-no-changes changes nothing: when dotnet format --verify-no-changes reports findings, it formats the files with findings (for their rules) in a temporary copy of the repository to see whether any file would change and which findings the fixes keep, then deletes the copy. After a clean stylebro-migrate format on the same code, settings and options, it already knows the findings that run kept (recorded in the temporary folder) and formats no copy. Plain dotnet format --verify-no-changes can't tell the two apart and fails on kept findings.

Excluding vendored code

Code you don't own (a copied library, generated sources checked in, a ThirdParty folder) shouldn't be reformatted. Mark it as generated code in the root .editorconfig:

[ThirdParty/**]
generated_code = true

# Entity Framework Core migrations: generated by 'dotnet ef', regenerated rather than edited
[**/Migrations/**]
generated_code = true

EF Core's migration files (Migrations/*.cs and the model snapshot) are generated but carry no <auto-generated> header, so without this the first run reformats every one of them (291 files in one trial repository).

StyleBro's rules, the built-in rules (IDE0055, IDE0073, ...) and dotnet format's whitespace pass all skip generated code, in the build and in dotnet format. One side effect: the compiler treats generated files as nullable-oblivious unless they say #nullable enable themselves.

dotnet_analyzer_diagnostic.severity = none in that folder isn't enough: a rule turned on by its id (dotnet_diagnostic.BRO1001.severity = warning in the root) wins over it. stylebro-migrate --write therefore writes every replacing rule as none into such a folder's .editorconfig, and it never edits files inside git submodules (folders with their own .git).

Files linked from a submodule. A project can compile files from another folder (<Compile Include="..\..\ext\Lib\Code.cs" />, or a shared .projitems). stylebro-migrate format never formats anything inside a git submodule (it passes --exclude with every submodule folder), so StyleBro doesn't report findings there either: the package's build targets read the submodule paths from the .gitmodules above the project and hide StyleBro's findings in those folders. A folder that is not a submodule (a copied library) still needs the generated_code = true section above.

Multi-targeted repositories

If any project sets several <TargetFrameworks>, plain dotnet format doesn't work at all; stylebro-migrate format does. It takes the same arguments and options:

stylebro-migrate format
stylebro-migrate format MySolution.sln --verify-no-changes --severity warn

Plain dotnet format crashes there on the SDK's formatting fix (IDE0055) without writing anything (reported upstream as dotnet/roslyn#86000 and dotnet/roslyn#86001). The command runs dotnet format once per target framework instead; without multi-targeted projects it's one run. A diagnostic every framework sees is printed once, and --report writes one format-report.json covering all runs. Spellings of one framework share a run (net6 and net6.0, NuGet's short names). A run loads only the projects that target its framework, so the renaming fixes also check the names in every other C# file of the repository: a field the tests read with GetField("count") keeps its name in the library's run too, and a name a project outside the run uses (UsedInProjectNotLoaded) is left to a run that loads both, or kept. Files a project compiles from outside its own folder (<Compile Include="..\Shared\*.cs" />) are formatted in the runs of the projects that compile them, and --files finds such a file's project too. Projects that compile the same files with other settings (a library and its AOT variant compiling ..\Lib\*.cs with other DefineConstants, language version or nullable context for that framework) run in separate parts of a framework's run (== net8.0, part 1 of 2), each with what it references: loaded together, dotnet format crashes on their different edits of one file (dotnet/roslyn#86001). Projects that compile a shared file with the same settings (a polyfill every library links) share a run: they parse it the same, so its copies get the same edits (<LangVersion>latest</LangVersion> counts as the version it means). This applies without multi-targeted projects too. Why: ci.md. stylebro-migrate init tells you when it finds such projects.

A framework whose code compiles exactly like another's in a project gets no run of its own there: when no #if tests a symbol that tells them apart (say the code tests NET8_0_OR_GREATER but nothing newer), net8.0, net9.0 and net10.0 see the same files, settings and branches, and one run formats them all. Only frameworks of one kind share a run (.NET, .NET Standard or .NET Framework, and the same platform such as -windows), since their APIs differ more. The Multi-targeted: line names the frameworks that ran and those that didn't need to.

After a run that changed files, the next one checks every framework at once first: a run whose check is clean isn't made again, and one whose check found something formats only the files with findings (all of its files when an earlier run of the same round changed a file it formats too).

A multi-line string can hold lines starting with # (#pragma warning disable in a source generator's test input) inside an #if branch some framework leaves out: there those lines read as directives, and the SDK's formatter moves them to column 0, breaking the string where the branch is compiled (CS8999). format names such files (Protected: ...) and, while dotnet format runs, swaps those lines' # for a character the file doesn't have, then puts it back, so the rest of the file is formatted as usual. If format is killed before it puts the # back (a closed terminal, a crash), the next stylebro-migrate command does: it keeps a record of the swap in %TEMP%/stylebro-mask ($TMPDIR on Linux and macOS) and says Restored: <file>. A file changed in a way that moved the swapped characters is left alone and named (Not restored: ...); format and --verify-no-changes then stop with Stopped: ... until the # is back.

A project dotnet format can't load at all (F#, .fsproj) makes it skip the C# projects that reference it, for the frameworks where the reference matters. format names them once, with that cause ("Partly incomplete: ... Likely cause: they reference TestFSharp.fsproj"); a restore doesn't change it, and the rest of the repository is formatted.

Old-style (non-SDK) projects

Projects in the old format (<Project ToolsVersion="15.0" ...> without Sdk=, usually .NET Framework with packages.config) work with stylebro-migrate format, with a few extra steps:

  • Restore with Visual Studio's MSBuild first, from a Developer Command Prompt:
msbuild -t:restore -p:RestorePackagesConfig=true

dotnet restore can't restore packages.config, and for the other old-style projects it writes restore output that Visual Studio's MSBuild then rejects ("Your project file doesn't list 'win' as a RuntimeIdentifier"). So format restores only the SDK-style projects when it finds old-style ones, and says so (the -p:RestorePackagesConfig=true part only matters with packages.config). If a build complains like that anyway (something ran dotnet restore), run the MSBuild restore again. - Reference assemblies. dotnet format loads the projects with the .NET SDK, which needs the .NET Framework reference assemblies: without them every project is skipped ("Required references did not load"). Install the targeting pack (the .NET Framework Developer Pack, or Visual Studio's ".NET Framework 4.x targeting pack"), or use the Microsoft.NETFramework.ReferenceAssemblies.net462 NuGet package (the version your projects target) and point MSBuild at it with an environment variable, which reaches both msbuild and dotnet format: set TargetFrameworkRootPath=%NUGET_PACKAGES%\microsoft.netframework.referenceassemblies.net462\1.0.3\build. - Install StyleBro.Analyzers the packages.config way, like StyleCop was: Visual Studio's package manager (or Install-Package StyleBro.Analyzers in its console) writes all three parts, or add them by hand. In packages.config:

<package id="StyleBro.Analyzers" version="0.7.0-alpha.1" targetFramework="net462" developmentDependency="true" />

In the project (paths relative to it; the packages folder is next to the solution):

<ItemGroup>
  <Analyzer Include="..\packages\StyleBro.Analyzers.0.7.0-alpha.1\analyzers\dotnet\cs\StyleBro.Analyzers.dll" />
  <Analyzer Include="..\packages\StyleBro.Analyzers.0.7.0-alpha.1\analyzers\dotnet\cs\StyleBro.CodeFixes.dll" />
</ItemGroup>
<!-- at the end, after the Microsoft.CSharp.targets import: the preset and the baseline -->
<Import Project="..\packages\StyleBro.Analyzers.0.7.0-alpha.1\build\StyleBro.Analyzers.targets" Condition="Exists('..\packages\StyleBro.Analyzers.0.7.0-alpha.1\build\StyleBro.Analyzers.targets')" />

plus an <Error Condition="!Exists('...StyleBro.Analyzers.targets')" ... /> line in the project's EnsureNuGetPackageBuildImports target, as NuGet adds for every package with build targets. Don't use a PackageReference there: it makes NuGet restore the project the PackageReference way, its packages.config packages are no longer restored, and a fresh clone doesn't build. A PackageReference in a Directory.Build.props that also serves SDK-style projects needs a condition that keeps it away from them:

<PackageReference Include="StyleBro.Analyzers" Version="0.7.0-alpha.1" PrivateAssets="all" Condition="'$(UsingMicrosoftNETSdk)' == 'true' or !Exists('$(MSBuildProjectDirectory)\packages.config')" />

stylebro-migrate names the packages.config projects and says this in its report. - --diff leaves packages.config projects out: the copy it formats has no git-ignored packages folder, and dotnet restore can't restore packages.config. It previews the other projects and names the ones it left out; only when every project uses packages.config does it stop. Other old-style projects preview like format runs: unrestored, so the preview has no StyleBro changes for them, and it says which. To see the changes in the packages.config projects, on a clean working tree: run the command without --diff, restore with MSBuild as above, run stylebro-migrate format, and review git diff (git checkout . undoes it). - If StyleCop came in through packages.config or an <Analyzer Include="..\packages\StyleCop.Analyzers.1.0.2\..."> hint path, stylebro-migrate reads its version there: rules that version doesn't have stay off.

Large codebases

If dotnet format would change too much at once, record today's violations in a baseline instead:

stylebro-migrate baseline                            # in the folder with your solution
stylebro-migrate baseline --project MySolution.sln   # or name it

This writes stylebro.baseline; commit it. The build, the IDE and dotnet format then ignore those violations, and only new code has to follow the rules. A baseline can't hide whitespace formatting. Details: baseline.md.

How long a full run takes. format loads the whole solution once per run, and in a multi-targeted repository once per target framework. On Orleans (about 200 projects, several target frameworks) the first stylebro-migrate format took 57 minutes and about 9 GB of memory, and format --verify-no-changes 11.5 minutes. A repository that size with TreatWarningsAsErrors is best started with a baseline: the build passes right away, and the backlog can be fixed a project at a time (stylebro-migrate format path/to/Project.csproj) or a few files at a time (--files).