Setting up pre-commit hooks for a .NET project
📝
Setting up pre-commit hooks for a .NET project
Tags
project-architecture
C#
Published
Nov 3, 2021

Formatter: dotnet-format

At the moment, dotnet-format is going to be a part of the next .NET SDK 6. The README suggests installing dotnet-format as a global tool, but I wanted it to be install as a dependecies like node_modules. Therefore, I had to create a manifest for toolings first (think of it as package.json) before installing dotnet-format.
# Create a maninfest for local tools
dotnet new tool-manifest

# Install dotnet-format for this project only
dotnet tool install dotnet-format
package.json
dotnet-format is also customizable via .editorconfig, with a few options available. I set it up to match my current Visual Studio Code format
root = true

[*.{cs,vb}]
indent_style = space
indent_size = 2
end_of_line = crlf
.editorconfig
Then, I add a command to rebuild local tools if someone else want to contribute
"setup": "dotnet tool restore",
package.json
Finally, the script for formatting. Note that I also set the verbosity level (-v) to quite (q).
"format": "dotnet format -v q",
package.json

Linter: StyleCop Analyzers

StyleCop Analyzers is a really popular tool for linting C# project. It’s installed as a nuget and run as the same time as the build command. Remember to install the correct version for your C# (below table is from their repo).
Compatibility
C# version
StyleCop.Analyzers version
Visual Studio version
v1.0.2 or higher
VS2015+
v1.1.0-beta or higher
VS2017+
v1.2.0-beta or higher
VS2019
Because StyleCop Analyzers run on build command, the build script can be used for linting
"lint": "dotnet build"
package.json
One final touch, StyleCop Analyzers treats lints as warnings. It is recommended to set these lints as error
<PropertyGroup>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <!-- Other properties... -->
</PropertyGroup>
.csproj
Seem like StyleCop Analyzers doesn’t like my half C#, half JavaScript conventions.
# More errors hidden

\paper-csharp\modules\file_parser\TextFile.cs(15,12): error SA1206: The 'public' modifier should appear before 'static' [\paper-csharp\paper-csharp.csproj]
\paper-csharp\modules\file_parser\TextFile.cs(31,10): error SA1513: Closing brace should be followed by blank line [C:\Users\fearn\Despaper-csharp\paper-csharp.csproj]
\paper-csharp\Program.cs(1,1): error SA1633: The file header is missing or not located at the top of the file. [\paper-csharp\paper-csharp.csproj]
\paper-csharp\modules\cli\Generator.cs(115,74): error SA1101: Prefix local calls with this [\paper-csharp\paper-csharp.csproj]
\paper-csharp\modules\cli\Generator.cs(117,34): error SA1101: Prefix local calls with this [\paper-csharp\paper-csharp.csproj]

0 Warning(s)
136 Error(s)

Editor integration

Visual Studio Code has some really nice feature to sync your workspace everywhere. The first one is launch.json use to run with build command. I set up some default arguments to pass in the ssg for testing (the option configurations.args).
{
  "version": "0.2.0",
  "configurations": [
    {
      // Use IntelliSense to find out which attributes exist for C# debugging
      // Use hover for the description of the existing attributes
      // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
      "name": ".NET Core Launch (console)",
      "type": "coreclr",
      "request": "launch",
      "preLaunchTask": "build",
      // If you have changed target frameworks, make sure to update the program path.
      "program": "${workspaceFolder}/bin/Debug/net5.0/paper-csharp.dll",
      "args": ["-i", "sample"],
      "cwd": "${workspaceFolder}",
      // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
      "console": "internalConsole",
      "stopAtEntry": false
    },
    {
      "name": ".NET Core Attach",
      "type": "coreclr",
      "request": "attach"
    }
  ]
}
launch.json
The second file is settings.json. This acts as a editor settings. Settings from Preferences: User Settings (search using Ctrl + Shift + P) can be defined here. In my case, I want the formatter to automatically run on save
{  "editor.formatOnSave": true,}
settings.json
Last one is extension.json. I didn’t do it here, but I set it up on another web project. It’s a way to hint other developers about extensions that make development faster. Just need to put in the IDs of those extensions.
{
  "recommendations": [
    "eamodio.gitlens",
    "dsznajder.es7-react-js-snippets",
    "esbenp.prettier-vscode"
  ]
}
extension.json

Pre-commit hook: husky & lint-staged

Husky is a tool to set up scripts to run before changes are commited.
lint-staged runs your script on all staged files and re-stage those changes. For example, lint-staged re-stage your changes after your formatters make additional formatting.
# Install husky as a dependency
$ npm install husky -D

# Add an additional script "prepare" inside package.json
$ npm set-script prepare "husky install"

# Create a husky config file
$ npx husky add .husky/pre-commit "npm test"
A new directory .husky should appear in your project. Inside, there’s a pre-commit file. This is basically a bash file, bash commands can be added in here. Be creative.
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo '🐶 Checking your commit...'

# echo an error message on fail
npm run pre-commit || echo '❌ Build fail. Fix errors showed above'
pre-commit
Now add a script pre-commit inside package.json. What my pre-commit do is running both the formatter and the linter. If there’s an error (not warnings) in either script, husky’ll exit and the code need to be fixed before commiting again.
"pre-commit": "npx lint-staged --relative && npm run lint"
package.json
The final step is specifying a script to run on staged C# files only. A flag --include is passed because lint-staged pass a list of staged files to the script, and we only want to format those staged files, not all our files.
"lint-staged": {  "*.cs": "dotnet format --include"},
package.json
The final package.json will look like this
{
	"scripts": {
	    "setup": "dotnet tool restore",
	    "predev": "dotnet run -- -i sample -o dist",
	    "dev": "vite dist",
	    "build": "dotnet run -- -i sample -o build",
	    "format": "dotnet format -v q",
	    "lint": "dotnet build",
	    "prepare": "husky install",
	    "pre-commit": "npx lint-staged --relative && npm run lint"
	  },
	  "lint-staged": {
	    "*.cs": "dotnet format --include"
	  },
}

Ending note

Finally I can check off learning all these pre-commit setup. The other is running tests.
These setup was interesting but never enough for me to dig in. But after learning about them, it’s really easy to set up another one, and save so much time doing code review. Also a really great way to learn about conventions and best practices. Now, I’m off to fix those 136 linting errors. PR welcome…

Additional Resources