Analyze C# code with SonarCloud & GitHub Actions
Table of contents
Static code analysis
Static code analysis is the process of examining the source code of a software without running it, with the purpose of detecting bugs, finding code smells, improving the code, finding security issues, helping with reviewing pull requests, and more.
Popular code analysis tools include ESLint for JavaScript and SonarQube for many programming languages. SonarCloud, for example, can do a review on your pull requests every time it is updated, and reports bugs, vulnerabilities, security hotspots, code smells found, as well as % of test coverage and code duplication.
SonarCloud
SonarCloud is a cloud-based code analysis service designed to detect coding issues in 26 different programming languages. By integrating directly with your CI pipeline or one of our supported DevOps platforms, your code is checked against an extensive set of rules that cover many attributes of code, such as maintainability, reliability, and security issues on each merge/pull request. As a core element of our Sonar solution, SonarCloud completes the analysis loop to help you deliver clean code that meets high-quality standards.
The paragraph above was copied from SonarCloud Docs. SonarCloud is, in my words and in a short sentence, a managed service that does code analysis on demand for your repositories through CI pipelines.
Also, it’s free for open source repositories. If your repo is private, you must pay to use it.
GitHub Actions
GitHub Actions is a CI/CD service provided by GitHub to automate parts of the software development lifecycle, such as building, testing and doing code analysis configured mostly with YAML files under ./.github/workflows
folder of your repository.
I particularly find it amazing: it is on GitHub that you store your code, and on GitHub that you set up your CI/CD workflows. 🐐
The SonarCloud GitHub Action
For some programming languages, there is a ready-to-use SonarCloud GitHub Action that you can use in your workflows. Pretty easy! Just like it’s explained in their docs, you add the step to the action with some parameters and everything will work.
You can’t use it with .NET projects 🫠
However, this is not so simple with .NET projects, though it is not complicated. In the action docs, there’s a section Do not use this GitHub action if you are in the following situations that says that we can’t use the action when:
Your code is built with Maven: run ‘org.sonarsource.scanner.maven:sonar’ during the build
Your code is built with Gradle: use the SonarQube plugin for Gradle during the build
➡️ You want to analyze a .NET solution: Follow our interactive tutorial for Github Actions after importing your project directly in SonarCloud
You want to analyze C and C++ code: rely on our SonarCloud Scan for C and C++ and look at our sample C and C++ project
Instead, you need to:
Install the
sonarscanner
dotnet toolBegin the analysis with the tool, passing all parameters
Build the project
Run the tests
End the analysis
This entire process is simplified with two scripts that we’ll see below.
Let’s do it
Setup SonarCloud
First, sign in into SonarCloud with the account you want. I suggest using GitHub.
As soon as you are in, click the plus (+) sign in the right upper corner and click on Analyze new project.
Select a project, then click Set Up.
In the next part, Set up project for Clean as You Code, just go with Previous version for now, and every change to your code will be considered new code to be analyzed. Then, click on Create project.
It will create the project for you, and in the next step, it asks what is your analysis method (basically your CI tool). Select GitHub Actions.
And this step is done. It asks you to create a new secret in your repository, so let’s go into the next step. Also, if you click the .NET button below, it gives a quick start on how to do it, but you can just ignore.
Please note that I used
async-review-poc
as the repository for this specific step, only to get the screenshots. Everywhere else I use thewhoof-aspnetcore
repository instead.
Set up the GitHub repository
All you have to do in your repository is to create that secret mentioned above, to authenticate into SonarCloud when running their scanner.
In your repository settings, go to Secrets and variables, Actions, then click on New repository secret:
Set the name to SONAR_TOKEN
, paste the value provided by SonarCloud and click Add secret.
runsettings.xml
At the repository root, create a file named runsettings.xml
, it will contain the configuration to run the project’s tests.
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<!-- https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/VSTestIntegration.md -->
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Format>json,cobertura,lcov,teamcity,opencover</Format>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
Create the scripts
Finally, let’s start by creating the scripts that will be used by the actions that will be created later.
Again, at the repository root, create a scripts
folder, and the following scripts:
start-tests.sh
#!/usr/bin/env bash
set -eu -o pipefail
REPORTS_FOLDER_PATH=test-reports
dotnet test \
--logger trx \
--logger "console;verbosity=detailed" \
--settings "runsettings.xml" \
--results-directory $REPORTS_FOLDER_PATH
This script is responsible for running your application’s tests, passing some important parameters like using the trx
(Visual Studio test results file), the settings file to use and where to store the test results.
start-sonarcloud.sh
#!/usr/bin/env bash
set -eu -o pipefail
if [ -z "$1" ]; then
echo "Please provide the sonar token 👀"
exit 0
fi
if [ -z "$2" ]; then
echo "Please provide the project version 👀"
exit 0
fi
echo "### Reading variables..."
SONAR_TOKEN=$1
PROJECT_VERSION=$2
echo "### Beginning sonarscanner..."
.sonar/scanner/dotnet-sonarscanner begin \
/k:"graduenz_whoof-aspnetcore" \
/o:"graduenz" \
/d:sonar.token="$SONAR_TOKEN" \
/v:"$PROJECT_VERSION" \
/d:sonar.host.url="https://sonarcloud.io" \
/d:sonar.cs.opencover.reportsPaths="**/*/coverage.opencover.xml" \
/d:sonar.cs.vstest.reportsPaths="**/*/*.trx" \
/d:sonar.exclusions="samples/**/*.cs,src/common/Whoof.Migrations/**/*.*"
echo "### Building project..."
dotnet build
./scripts/start-tests.sh
.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN"
This script makes sure the sonar token and the project version to use are passed as parameters, then begins the scan with sonarscanner
passing all parameters configuring which project to analyze, where to read test results, what files and folders should be ignored in code analysis, etc., then builds and tests the project (using our script created previously), and ends the scan.
Create the action
Everything but the action is now set up in the repository. All actions should be stored under the .github/workflows
folder, and we are going to create one for evaluating pull requests and main branch merges.
Under that folder, create a file name named evaluate-pr.yml
with the following content:
name: Evaluate pull request
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Setup Java 21
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '21'
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Cache SonarCloud packages
uses: actions/cache@v4
with:
path: ~/sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
uses: actions/cache@v4
with:
path: .sonar/scanner
key: ${{ runner.os }}-sonar-scanner
restore-keys: ${{ runner.os }}-sonar-scanner
- name: Install SonarCloud scanner
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
run: |
mkdir -p .sonar/scanner
dotnet tool update dotnet-sonarscanner --tool-path .sonar/scanner
- name: SonarCloud scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker compose up -d
./scripts/start-sonarcloud.sh ${{ secrets.SONAR_TOKEN }} ${{ github.sha }}
This action is divided in three parts: a name
, when it’s triggered (on
), and what it does (jobs
). As you can see, it’s triggered on every push on the main branch (including merge commits), and on every pull request (and every commit on it).
It has a single job named build
that uses latest Ubuntu available to run it, checks out the code, configures it to use Java 17 (Temurin, but I’m pretty sure you can use whatever you want), installs the sonarscanner dotnet tool globally (very important!), then finally runs the start-sonarcloud.sh
script.
You will notice it also does
docker-compose up -d
before running the script, that is because the application integration tests’ uses dependencies like PostgreSQL that runs on Docker. Check asp.net-core-integration-tests.md for more information about this.
Open a pull request
First, commit and push everything you did to main branch before!
Then, let’s say you made some changes in a new branch in your repository. You will raise a pull request, and you want to get it analyzed by the new action you just set up. Go ahead!
As soon as you raise the pull request, give it a few seconds and the action will start to run, as shown in the image above. If everything goes well, it will end, and then you will get a comment on your PR, like this:
If any bug, vulnerability, security issue or code smell is detected, it will be indicated, and you can handle it properly, as well as see coverage reports to see where to improve it, just click the anchors in the comment. SonarCloud helps a lot with keeping up the good code!
Also, if you go to the Checks tab, you will get the complete SonarCloud report:
And that’s all! Look further if you want to go a bit deeper, that section below is likely to be updated often.
Extra
Until this point, you’ve seen how to set up the whole process of static code analysis on your pull requests and main branch commits. However, there are some extra lessons that I learned, and I’m sharing below with you.
References
There are two former coworkers that indirectly contributed, with their projects, to build this whole mechanism that I just showed you.
Rafael Miranda: he is the author of Ziggurat, a .NET library to create message consumers that makes easy to implement idempotency, with built-in support to SQL Server and MongoDB, and the DotNetCore.CAP library. His repository has helped a lot because it’s where I got the scripts from.
Willian Antunes: if you noticed the
set -eu -o pipefail
in the scripts, I got that from a post in his blog: Production-ready shell startup scripts: The Set Builtin. It modifies the shell behavior on how to deal with errors.
Branch protection rules
A common practice is to protect your branch(es) from direct commits, and requiring pull requests to merge them into the branch, as well as adding some requirements to the pull requests before merging.
In the example below, it has been set up to:
Require a pull request before merging.
- And must have, at least, 2 approvals.
Require status checks.
- You must select the status checks, just type “sonar” in the text field below and select SonarCloud Code Analysis.
Require conversation resolutions before merging.
- If someone comments in your PR, the comment must be resolved before merging.
This will protect and improve the whole software development process, making sure nothing broken gets pushed to main branch without prior review and agreement.
Disabling a rule in a class
In my project, something that I needed was to suppress some code smells that were detected by the code analysis. In a regular coding task that is not ideal; however, the example below is a class that has many generic arguments, much more than the two authorized, but is part of the abstractions, a reason why I considered it acceptable.
namespace Whoof.Api.Common.Controllers;
[SuppressMessage("SonarLint", "S2436", Justification = "Abstraction tradeoffs")]
public abstract class BaseCrudController
<TDto, TEntity, TCreateCommand, TUpdateCommand, TDeleteCommand, TGetByIdQuery, TGetListQuery, TSearch>
: ControllerBase
where TDto : BaseDto
where TEntity : class
where TCreateCommand : BaseCreateCommand<TDto>, new()
where TUpdateCommand : BaseUpdateCommand<TDto>, new()
where TDeleteCommand : BaseDeleteCommand<TDto>, new()
where TGetByIdQuery : BaseGetByIdQuery<TDto>, new()
where TGetListQuery : BaseGetListQuery<TDto, TEntity>, new()
where TSearch : BaseSearch<TEntity>
The SuppressMessage
attribute can suppress the accusation of an issue, in this case, the S2436 rule, with proper justification.
Subscribe to my newsletter
Read articles from Guilherme Raduenz directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Guilherme Raduenz
Guilherme Raduenz
I've been coding for 10+ years ― more specifically since 2011, using mostly the .NET stack, and sometimes I write about coding. I live in Blumenau (Brazil), speak Portuguese natively, English every day at work, and also a little bit of German (heritage language). My personal interests are to get in touch with nature by going camping, hiking, doing things outdoor, and traveling by car in the countryside. I'm also into cars, airplanes and general engineering, so I enjoy going to some events and visiting museums. Other than that, creating utilities, improving and automating things on my routine, my house, etc., and that sometimes become projects under my GitHub, though not always public.