Introduction: Why is Basic CI/CD Not Enough?
The initial level of CI/CD for C# projects is usually limited to simply running dotnet build and dotnet test. However, when a project grows into a microservice architecture, a monorepository, or requires support for multiple .NET versions, the standard approach becomes insufficient. Build times increase, tests are duplicated, and the NuGet package deployment process becomes routine.
In this article, we will cover advanced techniques for configuring GitHub Actions for C# (.NET) projects. You will learn how to speed up pipelines using caching, organize parallel tests, implement matrix builds, and set up automatic publishing to NuGet. All code examples are ready to use in your projects.
1. Matrix Build and Testing Multiple .NET Versions
A matrix is a powerful GitHub Actions tool that allows you to run the same workflow with different parameters. For C#, this is indispensable when you need to check code compatibility with .NET 6, .NET 8, and .NET 9.
Example Matrix Configuration
Create the file .github/workflows/ci-matrix.yml:
name: Matrix Build and Test
on: push: branches: [ main, develop ] pull_request: branches: [ main ]
jobs: build: runs-on: ubuntu-latest strategy: matrix: dotnet-version: ['6.0.x', '8.0.x', '9.0.x'] configuration: ['Release', 'Debug'] fail-fast: false # Continue the build even if one version fails
steps: - uses: actions/checkout@v4 - name: Setup .NET ${{ matrix.dotnet-version }} uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ matrix.dotnet-version }} - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --configuration ${{ matrix.configuration }} --no-restore - name: Run tests run: dotnet test --configuration ${{ matrix.configuration }} --no-build --verbosity normalKey Points:
fail-fast: false— allows you to see results for all versions, even if one of them fails.- The matrix can include not only .NET versions but also OS (e.g.,
os: [ubuntu-latest, windows-latest]). - For a monorepository, you can add a
project-pathparameter and test different modules.
2. Dependency Caching: Speed Up the Build by 3x
Each GitHub Actions run starts with a clean environment. Without caching, dotnet restore will download all NuGet packages from scratch every time. For large projects, this can take 2-5 minutes. The solution is caching the ~/.nuget/packages folder.
Advanced Cache Configuration
Use the actions/cache action with a proper key that takes the lock file into account:
steps: - uses: actions/checkout@v4
- name: Cache NuGet packages uses: actions/cache@v4 with: path: ~/.nuget/packages # Key: hash of the lock file + OS name key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} # Fallback to the previous key if the lock file hasn't changed restore-keys: | ${{ runner.os }}-nuget-
- name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x'
- name: Restore dependencies run: dotnet restoreTip: If the project has multiple .csproj files, use hashFiles('**/*.csproj', '**/packages.lock.json') for a more precise hash.
3. Parallel Test Execution and Report Generation
When a project has hundreds of tests, sequential execution can take tens of minutes. GitHub Actions supports parallelization at the job level. But for