1. Introduction
Continuous Integration(CI) is a development practice that allows teams to detect problems early by automatically building and testing code every time changes are pushed to the repository. In this post, I'll walk through how I set up CI for my own .NET9 project using GitHub Actions.
We'll start by briefly introducing what CI is and why it matters. Then, I'll explain how to implement CI on GitHub-from initial setup to writing the actual workflow file. Finally, I'll share some common pitfalls and tips I learned along the way.
2. Why CI Matters
Continuous Integration(CI) ensures that code changes are regularly merged and automatically verified through builds and tests. This practice is essential for maintaining code quality, catching bugs early, and enabling smooth collaboration-especially on larger or fast-moving projects.
- Catch Errors Early
- Keep the main Branch stable
- Speed Up Development
- Improve Team Collaboration
- Gain Confidence in Your Codebase
3. GitHub Actions Workflow: Configuration Walkthrough
The workflow configuration file is named .github/workflows/dotnet-mysql.yml. A complete version of this file will be provided in the final section.
This file should be placed under the .github/workflows/ directory at the root of your project, which is the standard location for GitHub Actions to detect and run workflows.
In this section, we’ll walk through the GitHub Actions YAML configuration used to implement CI for a .NET 9 Web API project with MySQL and Entity Framework Core. The workflow covers building the app, applying EF Core migrations, and running automated tests — all triggered on pull requests to main
.
Trigger Conditions
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
The workflow is triggered when:
- A commit is pushed directly to the
main
branch
Best practice: you can disable direct pushes to
main
by protecting the branch. This forces developers to use pull requests, ensuring CI checks are always run before merging.
- A pull request is opened targeting
main
This ensures CI runs before merging new code into production, helping catch issues early.
Job Setup: build-test
jobs:
build-test:
runs-on: ubuntu-latest
This job runs in a Linux-based container using GitHub’s latest Ubuntu runner.
Here, build-test is the name of the job — you can change it to any valid identifier, such as my-custom-job-name, as long as it contains no spaces or special characters.
Spinning Up MySQL as a Service
services:
mysql:
image: mysql:8.0
ports:
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: testdb
MYSQL_USER: testuser
MYSQL_PASSWORD: testpass
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -u root -proot123"
--health-interval=10s
--health-timeout=5s
--health-retries=5
We use a Dockerized MySQL service for integration testing:
- Credentials and default database (
testdb
) are set via environment variables. - A health check ensures the DB is ready before proceeding.
Environment Variables
env:
ConnectionStrings__DefaultConnection: "server=127.0.0.1;port=3306;database=testdb;uid=testuser;pwd=testpass"
ASPNETCORE_ENVIRONMENT: CI
JWT_JSON: '{"Issuer":"test-issuer","Audience":"test-audience","IssuerSigningKey":"fake-secret-key12345678!@#$%^&*90()-=_+qwert","AccessTokenExpiresMinutes":30,"RefreshTokenExpiresDays":7}'
These are injected into the test environment to:
- Provide EF Core a working connection string.
- Set the environment profile to
CI
. - Mock JWT-related secrets using a hardcoded JSON string.
Step-by-Step Execution
1. Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
This step uses the official GitHub Action actions/checkout to clone your repository’s code into the GitHub Actions runner. It ensures that the workflow has access to the latest code from the branch that triggered the workflow (e.g., main, dev, or a feature branch).
Without this step, the subsequent build, test, or deployment steps would not have access to your project's source files.
2. Setup .NET SDK
- name: Setup .NET 9 Preview
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0.300'
This step installs the specified version of the .NET SDK—in this case, .NET 9.0.300—on the GitHub Actions runner. It uses the official actions/setup-dotnet action to ensure the correct .NET environment is available for building and testing your project.
By specifying the version, you ensure consistency across local development, CI, and production environments. This is especially important when using preview or non-default SDK versions.
3. Install EF Core CLI
- name: Install EF Core CLI tools
run: dotnet tool install --global dotnet-ef
4. Add tools to PATH
- name: Add dotnet tools to PATH
run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
Restore, Build, and Migrate
- name: Restore dependencies
run: dotnet restore To-Do-List/To-Do-List.csproj
This step ensures that all required NuGet packages are available before building the project.
The dotnet restore
command reads the dependencies listed in the .csproj
file and downloads any missing packages from the NuGet repository.
If the packages are already cached locally, it skips re-downloading them.
In short: It prepares the environment by making sure all external libraries your project depends on are present.
This is a standard and essential step in any .NET CI workflow to ensure the build process won’t fail due to missing dependencies.
- name: Build solution
run: dotnet build To-Do-List/To-Do-List.csproj --configuration Release --no-restore
Then apply EF Core migrations for each DB context:
- name: "Apply EF migration: MyIdentityDbContext"
run: dotnet ef database update --context To_Do_List.Identity.DbContext.MyIdentityDbContext --project To-Do-List/To-Do-List.csproj --startup-project To-Do-List/To-Do-List.csproj
- name: "Apply EF migration: TaskDbContext"
run: dotnet ef database update --context To_Do_List.Tasks.DbContext.TaskDbContext --project To-Do-List/To-Do-List.csproj --startup-project To-Do-List/To-Do-List.csproj
- name: "Apply EF migration: SystemConfigurationContext"
run: dotnet ef database update --context To_Do_List.Configuration.DbContext.SystemConfigurationContext --project To-Do-List/To-Do-List.csproj --startup-project To-Do-List/To-Do-List.csproj
Although migrations are triggered inside WebApplicationFactory, applying them explicitly in the CI workflow makes the process more declarative and independent of internal test setup. This ensures the database is ready before any test code runs, leading to a more robust and self-documenting CI pipeline.
Run Automated Tests
- name: Run tests
run: dotnet test To-Do-List.Tests/To-Do-List.Tests.csproj --configuration Release --logger "console;verbosity=normal"
This step runs all unit and integration tests, ensuring the pipeline fails fast if any logic breaks.
--logger "console;verbosity=normal"
Use normal as default, and switch to detailed or diagnostic only when you need to troubleshoot failures in the CI logs.
4. Pull Request + Branch Protection Strategy
To configure Pull Request + Branch Protection Strategy on GitHub, go to your repository’s Settings → Branches, then add or edit a rule for the main branch. In the protection settings, check “Require a pull request before merging”, “Require status checks to pass before merging”, and “Require branches to be up to date before merging”. These settings ensure that all code changes go through a Pull Request, pass CI checks, and are tested against the latest main branch before being merged. This helps maintain code quality and prevents direct, unreviewed changes to protected branches.
5. Validating the Workflow
To validate your GitHub Actions workflow, simply create a feature branch (e.g. test-ci), make a small change to trigger the CI, and open a Pull Request to merge it into main. Once the status checks (like build-test) pass successfully, and the workflow runs as expected, you can safely merge the PR. This confirms that your CI/CD pipeline is working properly.
6. yaml file
name: .NET CI with MySQL
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
ports:
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: testdb
MYSQL_USER: testuser
MYSQL_PASSWORD: testpass
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -u root -proot123"
--health-interval=10s
--health-timeout=5s
--health-retries=5
env:
ConnectionStrings__DefaultConnection: "server=127.0.0.1;port=3306;database=testdb;uid=testuser;pwd=testpass"
ASPNETCORE_ENVIRONMENT: CI
JWT_JSON: '{"Issuer":"test-issuer","Audience":"test-audience","IssuerSigningKey":"fake-secret-key12345678!@#$%^&*90()-=_+qwert","AccessTokenExpiresMinutes":30,"RefreshTokenExpiresDays":7}'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup .NET 9 Preview
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0.300'
- name: Install EF Core CLI tools
run: dotnet tool install --global dotnet-ef
- name: Add dotnet tools to PATH
run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
- name: Restore dependencies
run: dotnet restore To-Do-List/To-Do-List.csproj
- name: Build solution
run: dotnet build To-Do-List/To-Do-List.csproj --configuration Release --no-restore
- name: "Apply EF migration: MyIdentityDbContext"
run: dotnet ef database update --context To_Do_List.Identity.DbContext.MyIdentityDbContext --project To-Do-List/To-Do-List.csproj --startup-project To-Do-List/To-Do-List.csproj
- name: "Apply EF migration: TaskDbContext"
run: dotnet ef database update --context To_Do_List.Tasks.DbContext.TaskDbContext --project To-Do-List/To-Do-List.csproj --startup-project To-Do-List/To-Do-List.csproj
- name: "Apply EF migration: SystemConfigurationContext"
run: dotnet ef database update --context To_Do_List.Configuration.DbContext.SystemConfigurationContext --project To-Do-List/To-Do-List.csproj --startup-project To-Do-List/To-Do-List.csproj
- name: Run tests
run: dotnet test To-Do-List.Tests/To-Do-List.Tests.csproj --configuration Release --logger "console;verbosity=normal"
Top comments (0)