When working on larger projects with lots of contributors, you want to make sure everyone is playing by the same rules. One of these aspects are code formatting. There is nothing more annoying than having a pull request which basically changed one line of code, but the whole file appears changed because the IDE formatted the code on the way out and rearrange all the pieces around.
In this article, I would like to share my code style setup for dotnet as well as my experience with this and previous solutions. If you’re interested in the tutorial on how to set this up, check the Table of contents below.
- The initial approach:
dotnet-format - The contender:
csharpier - The guardian:
StyleCop - The tutorial: How to use this all?
- Conclusion
The initial approach: dotnet-format
While developing with other technologies, I came across two setups for code style checking and enforcement: spotless (for Java) or prettier and eslint (for TypeScript). While I mostly have user experience with the former, I used the latter in setting up new projects, which means I spent some time turning dials and switches to get it working. If your IDE (in that case VS Code) is setup with the right extensions, you will get all warnings in it and you can spot any violations before you actually commit them to your repo, risking a Build failure due to style violations. I wanted this for our dotnet projects. So the research began.
Pretty quickly, I came across dotnet-format, a tool which comes with your SDK since .NET 6 and it is configured via the standard .editorconfig file. So all good, right?
NOPE!
While the initial setup was good, the pains started very quickly. Almost instantly, I get failures on the automated pull request checks because my developer machine is a Windows computer and the build agent is running on Linux. So we get complaints about file encoding, line endings and all that good stuff. I had to chuck out quite a few rules to get everything into a usable state. And to this day, running the check locally and on the agent gives very different results, to a point where you’d have to be afraid of the result every time you push new code to your pull request.
On top of that, some IDEs don’t fully respect the settings provided by .editorconfig (namely Rider), for which the solution is to provide every team member with the correct IDE settings and hope they never need to be touched (Spoiler: they will), resulting in style violations due to the IDEs configuration.
So a better solution is required.
The contender: csharpier
While running through the forums of the internet, I came across a different tool called csharpier. Their proposal is that the tool is very fast while being quite opinionated and thus not providing to many knobs to adjust its behavior. This might sound bad at first, but since this is the same philosophy as prettier follows - and I did like prettier quite a bit - this should be fine, right?
If you have your own opinions about how your code should be formatted, then csharpier can be a bitter pill to swallow. As mentioned, it doesn’t provide a whole lot of configuration options, so if you dislike any of it’s behavior: Too bad. I had to get used to its rules a bit, since my personal style varied quite a bit from it. However, after reformatting a whole project and looking through the changes, I was surprised how nice everything looked. There are a few things I personally would do differently. For an example, this is a piece of startup code in one of my personal projects. It uses extension methods to extend the functionality of IHost to run some code before the application starts up. This is how I initially wrote it:
IHost host = hostBuilder
.ConfigureServices(services => {
// note: obviously these are just dummies for the sake of demonstration
services
.AddThis()
.AddThatOtherThing()
.AddSingleton<IThis, This>()
.AddHostedService<SomeHost>();
});
await host
.BootCronJobs()
.ArmKillSwitch()
.LogAndRun();
This is after csharpier formatted my code:
IHost host = hostBuilder.ConfigureServices(services =>
{
// note: obviously these are just dummies for the sake of demonstration
services
.AddThis()
.AddThatOtherThing()
.AddSingleton<IThis, This>()
.AddHostedService<SomeHost>();
});
await host.BootCronJobs().ArmKillSwitch().LogAndRun();
As you can see, the last bit has been compressed to one line. While I personally don’t fully agree with that, since I interpret all these calls as a pipeline to be run from top to bottom, I can see why csharpier does that (in this case to keep everything on one line because it does not exceed the configurable line limit). csharpier in general seems to have a tendency to flatten as much as possible onto one line, unless you introduce lambda functions into the mix.
However once I overcame my initial “pride” I started to like what csharpier did for me. And the benefit of having almost no configuration options means, it’s pretty much plug-and-play. However, it only covers formatting. What about linting though?
The guardian: StyleCop
StyleCop is a static code analyzer developed by Microsoft. The story goes that at one point, an employee at Microsoft wrote a tool to scrape through all C# projects to find common practises and the outcome was StyleCop. However in contrast to csharpiers opinionated nature, StyleCop allows for more configuration, which is probably also needed as some of the warnings generated by the tool can mess up your code quite a bit, if you let it reign freely.
The best part about it though is, that it integrates well into the IDE. All objections generated by StyleCop are provided during the build as part of all other compiler output, so the IDE of choice will probably pick this up without a problem. And integration into an automated pipeline is also easy, since it’s just invoked by a call to the dotnet format command. But be warned if you want to integrate it into your existing project: You will spend a few hours, going through all its remarks, learning why it is there and how to address them. Parts can be fixed automatically through a command, parts have to be done manually. However the reward outweighs the cost in my opinion which is why I tried this on one of my bigger personal projects. An hour or two of tinkering was enough to get it “up to code”.
The tutorial: How to use this all?
So after the lot of introduction, you might want to know, how you can get your own project setup. So there is the game plan:
- Add
csharpierto the project - Setup an automated formatting check (and see it fail)
- Reformat the code base
- Setup your project so it automatically gets formatted correctly in the future
- Add
StyleCopto the project - Setup an automated linting check (and see it fail)
- Resolve all linting violations
Formatting
Add csharpier
First, add csharpier to your project tools. To do so create the following file in your project folder under .config/dotnet-tools.json:
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "0.23.0",
"commands": [
"dotnet-csharpier"
]
}
}
}
Follow this up by running the following command in your terminal:
$ dotnet tool restore
Tool 'csharpier' (version '0.23.0') was restored. Available commands: dotnet-csharpier
Restore was successful.
Note: You might want to check what the latest version of
csharpieris when copying this into your project. This was the most recent version as of writing this article.
Setup automated checking
This step depends on what platform you use for automated building and testing. In my example, I’m using GitHub Actions, but this can be easily reproduced on any other platform, like Azure DevOps, GitLab, etc.
Add the following steps to your existing pipeline:
name: Build & Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
# [...] section for building and testing omitted
code-style:
name: Check Code Style
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: |
dotnet tool restore
- name: Check Code Format
run: |
dotnet csharpier --check src/
As you can see, I added a new code-style job which only tests code-style. It doesn’t build the project, it doesn’t run unit tests or anything else. However you can obviously integrate this into an existing job as the core commands you will have to execute are:
dotnet tool restore
dotnet csharpier --check src/
If any violations were detected, csharpier will exit with a non-0 status code, which should make your pipeline fail. You can also test this locally to see the effect. Once committed and properly registered, run your pipeline and observe it (probably) fail at the last step.
Important: Make sure that the failure is due to invalid formatting, not because some dependency setup failed or the path to the source code wasn’t set correctly.
Reformat your code
This is the easiest one. Run this command, then commit your changes to your repository without changing anything else:
$ dotnet csharpier src/
Total time: 185ms
Total files: 563
Setup automated formatting
This step is optional, but I recommend not skipping it as it makes future setup easier.
With proper formatting in place, let’s ensure that setting it up is as easy as possible for all team members (ideally they won’t even notice). We can achieve this using the following tools:
- csharpier IDE integration (available for Visual Studio, VS Code, Rider) to automatically format your code in your IDE
- git hooks to automatically format your code before committing
For the later, we can use another dotnet tool called husky to make sure everyone has git hooks configured properly. Since git hooks are locally configured, husky allows you to set them up for others so when they checkout your project, hooks should be configured automatically and without too much hassle.
This part is partially taken from the official documentation website of
csharpierabout how to configure pre-commit hooks. See here: https://csharpier.com/docs/Pre-commit
To do so, add husky to your .config/dotnet-tools.json like so:
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": { /* ... */ },
"husky": {
"version": "0.5.4",
"commands": [
"husky"
]
}
}
}
Next, we add the following section to one of our .csproj files to make husky automatically setup on build:
<Project>
<Target Name="husky" BeforeTargets="Restore;CollectPackageReferences" Condition="'$(HUSKY)' != 0">
<Exec Command="dotnet tool restore" StandardOutputImportance="Low" StandardErrorImportance="High" />
<Exec Command="dotnet husky install" StandardOutputImportance="Low" StandardErrorImportance="High" WorkingDirectory="../../" />
</Target>
</Project>
Be sure to adjust WorkingDirectory so that it points to the root of your repository. In my case, the .csproj was nested under src/SomeProject/SomeProject.csproj, therefore ../../.
Pro-tip: You can also add this section to a Directory.Build.props file, so it will be automatically picked up for any project within this directory. You can even place it on the root of your Git repository, however you have to ensure that all your .csproj files are on the same level or you might run into some strange behavior. Alternatively, pick a .csproj that is very central to your project (i.e. one that hosts an executable you’re using to run the application).
Finally run a build in your IDE or through a dotnet build command and the setup should be done for you. If you now commit some new code, it will automatically be formatted for you, without you needing to do anything, even if you don’t have an IDE extension installed. Again, make sure to commit all your changes.
Linting
Add StyleCop
Again,
csharpieralready provided some of this information for me, so I reused some of their documentation. However adjustments were made because that was not enough for me. Check here: https://csharpier.com/docs/IntegratingWithLinters
Now we repeat the same basic steps again, but with StyleCop. However StyleCop is not installed as a dotnet tool, but as a NuGet package. Instead of adjusting every single .csproj, I again used the power of my Directory.Build.props file to install the package to all projects by adding this section:
<Project>
<!-- ... -->
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Note that I installed the latest Beta version, since the current release version as problems with newer C# features, like records.
Next, add two files to your project (ideally the root of the repository): stylecop.json and StyleCop.ruleset. The first is a basic behavior configuration for StyleCop, which is documented here: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/Configuration.md
For a start, fill stylecop.json with this stub, then start digging through the documentation to find whatever settings you want to adjust:
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json"
}
For StyleCop.ruleset, you can either take either of the following files as a starting point and customize to your liking:
- “Use rule sets to group code analysis rules” on learn.microsoft.com
- StyleCop Default Rule Set on GitHub (contains all default rules set by StyleCop so you can adjust them easily)
Lastly, again in your Directory.Build.props, add another bit:
<Project>
<PropertyGroup>
<!-- Tells StyleCop what rule set to use -->
<CodeAnalysisRuleSet>../../StyleCop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<!-- Attaches the StyleCop configuration file to all projects so it is used -->
<AdditionalFiles Include="../../stylecop.json" Link="stylecop.json" />
</ItemGroup>
</Project>
With these changes done, you should now be able to rebuild all your projects and potentially get a bunch of new warnings. Don’t worry, we’ll address that later.
Setup automated linting
Same as before, this depends on your environment. For me, I amended my pipeline from above with the following two commands:
# GitHub pipeline
# [...]
jobs:
# [...]
code-style:
name: Check Code Style
runs-on: ubuntu-latest
steps:
- # [...]
- name: Check Code Style
run: |
dotnet format style --verify-no-changes src/
- name: Run Format Analyzer
run: |
dotnet format analyzers --verify-no-changes src/
dotnet format style --verify-no-changes src/
dotnet format analyzers --verify-no-changes src/
Committed and executed, you should now get the same warnings as in your IDE, only this time it should make your build fail.
Resolve linting violations
Congratulations, you’ve reached the last step of this exercise. The bad news: This is going to be the one, that probably takes the longest, depending on the amount of code you have to go through. Parts of this can be automatically fixed, others require your own investigation. The best advise I can give you for this step:
- Run the automated fixes first
- Then group the remaining issues by their code and start working through them one-by-one
You might want to refer back to csharpiers documentation for an initial setup for your rules (https://csharpier.com/docs/IntegratingWithLinters). Below you can find a rule set I have setup for myself using the .editorconfig file.
[*.cs]
# csharpier exceptions
# [Section omitted for brevity, check link above]
# Documentation
dotnet_diagnostic.SA1614.severity = none
dotnet_diagnostic.SA1616.severity = none
dotnet_diagnostic.SA1622.severity = none
dotnet_diagnostic.SA1623.severity = none
dotnet_diagnostic.SA1629.severity = none
# Custom Adjustments
dotnet_diagnostic.SA1101.severity = none # prefix local calls with this
dotnet_diagnostic.SA1309.severity = none # allow _prefix for class members
dotnet_diagnostic.SA1633.severity = none # file header
dotnet_diagnostic.SA1636.severity = none # file header
dotnet_diagnostic.SA1310.severity = none # UPPER_CASE_NAME
dotnet_diagnostic.SA1515.severity = none # no empty line before comment
dotnet_diagnostic.SA1124.severity = none # allow regions
dotnet_diagnostic.SA1649.severity = none # file name don't have to match if not needed
dotnet_diagnostic.SA1201.severity = none # ordering
dotnet_diagnostic.SA1312.severity = none # parameter can also begin with _
dotnet_diagnostic.SA1313.severity = none # parameter can also begin with _
dotnet_diagnostic.SA1402.severity = none # file should be able to contain multiple types
As you can see, I documented all custom adjustments with a short reason, why I set it this way. Most of these are very subjective, so pick whichever you want to keep and through out any you don’t want. An excerpt of what rules I use that deviate from the standard:
- Constants should be
ALL_UPPER_CASE(violatesSA1310) - Don’t use
thisfor class members, if not needed (violatesSA1101) - Use
_prefixfor class member variables (violatesSA1309) - Allow discarding parameters in lambdas (violates
SA1312,SA1313) - Group multiple classes into one file, if they aren’t too long (violates
SA1402) - Use
#regionto group larger blocks of code (violatesSA1124) - and many more
All these rules are documented well in the StyleCop GitHub repository so I highly suggest looking these rules up. It also helps to google/bing/research these rules and look into some Stack Overflow or other forum discussions before committing to one way or another. This can be legitimately insightful.
At the end of your work should lie a pull request that passes all your newly added checks without any warnings or remarks.
Conclusion
In this article, I showed you a study of different code style tools for your dotnet tool kit and how to integrate it into any of your current projects.