Skip to main content

.NET Continuous Integration

Introduction

Continuous Integration (CI) is a development practice where developers integrate code changes into a shared repository frequently, typically multiple times a day. Each integration is automatically verified by building the application and running automated tests. This approach helps detect errors quickly and locate them more easily.

In .NET development, CI has become essential for maintaining high-quality software and accelerating delivery. Whether you're working with .NET Framework or .NET Core/.NET 5+, implementing CI can significantly improve your development workflow.

Why Continuous Integration Matters

Before diving into the technical details, let's understand why CI is crucial for .NET projects:

  • Early Bug Detection: Catches integration issues as soon as they occur
  • Code Quality Assurance: Enforces coding standards and practices
  • Reduced Integration Problems: Minimizes "integration hell" by merging small changes frequently
  • Improved Team Collaboration: Provides transparency about build status and failures
  • Consistent Builds: Creates reproducible build environments
  • Increased Development Speed: Automates repetitive tasks

CI Components for .NET Projects

A typical .NET CI pipeline includes the following components:

  1. Source Control Integration: Connection to your repository (Git, Azure DevOps, etc.)
  2. Build Automation: Compiling your .NET code
  3. Automated Testing: Running unit, integration, and other tests
  4. Code Quality Analysis: Static code analysis and code coverage
  5. Artifacts Generation: Creating deployable packages
  6. Notifications: Alerting the team about build status

Setting Up a Basic CI Pipeline for a .NET Project

Let's set up a basic CI pipeline using GitHub Actions, a popular CI/CD service. We'll create a workflow for a sample .NET application.

1. Create a GitHub Actions Workflow File

In your .NET project repository, create a new file at .github/workflows/dotnet-ci.yml:

yaml
name: .NET CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Test
run: dotnet test --no-build --verbosity normal

This workflow will:

  • Trigger on pushes to the main branch or pull requests
  • Set up the .NET SDK
  • Restore project dependencies
  • Build the project
  • Run tests

2. Understanding the Workflow

Let's break down what each section does:

  • Trigger Events: The on section specifies when the workflow runs
  • Jobs: The build job defines what actions to take
  • Steps: Individual actions performed sequentially
    • checkout: Gets your code from the repository
    • setup-dotnet: Installs .NET SDK
    • Subsequent steps run .NET CLI commands

3. Adding Code Quality Checks

Let's enhance our CI pipeline with code quality checks using a popular .NET tool called SonarCloud:

yaml
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=my-project
-Dsonar.organization=my-organization

To use this, you'd need to:

  1. Register on SonarCloud
  2. Create a token and add it as a secret in your GitHub repository

Real-World Example: CI Pipeline for a .NET Web API

Let's create a more comprehensive CI pipeline for a .NET Web API project:

yaml
name: .NET Web API CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Test
run: dotnet test --no-build --verbosity normal --configuration Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover

- name: Code Coverage Report
uses: codecov/codecov-action@v3

- name: Publish
run: dotnet publish --no-build --configuration Release --output ./publish

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: api-build
path: ./publish

This enhanced workflow:

  1. Triggers for both main and develop branches
  2. Builds the application in Release mode
  3. Runs tests with code coverage
  4. Uploads the coverage report to Codecov
  5. Publishes the application
  6. Uploads build artifacts for potential deployment

CI Tools for .NET Development

While we've focused on GitHub Actions, there are several CI tools compatible with .NET:

  1. Azure DevOps Pipelines: Microsoft's CI/CD service, tightly integrated with .NET
  2. Jenkins: Open-source automation server with plugins for .NET
  3. TeamCity: JetBrains' CI server with excellent .NET support
  4. GitLab CI: GitLab's integrated CI/CD solution
  5. CircleCI: Cloud-based CI service supporting .NET

Setting Up CI with Azure Pipelines

For Microsoft-centric environments, Azure Pipelines is often the preferred choice. Here's a basic Azure Pipelines configuration for a .NET project:

yaml
trigger:
- main

pool:
vmImage: 'windows-latest'

variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'

- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'

- task: VSTest@2
inputs:
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'

Save this as azure-pipelines.yml in your repository root.

Best Practices for .NET Continuous Integration

To get the most benefit from your CI setup:

  1. Run Tests Frequently: Include unit, integration, and UI tests in your pipeline
  2. Keep Builds Fast: Optimize build steps and parallelize tests
  3. Use Build Caching: Speed up builds by caching NuGet packages
  4. Standardize Environments: Ensure CI environments match production
  5. Implement Branch Policies: Prevent code from being merged if CI checks fail
  6. Monitor Test Coverage: Track code coverage and enforce minimum thresholds
  7. Implement Security Scanning: Use tools like NuGet Package Security Analysis

Common CI Challenges in .NET Projects

When implementing CI for .NET projects, watch out for these common issues:

  1. Long Build Times: Large .NET solutions can take time to build

    • Solution: Use incremental builds and optimize test execution
  2. Windows-Specific Dependencies: Some .NET Framework projects require Windows

    • Solution: Use Windows build agents for .NET Framework projects
  3. Database Integration Testing: Tests requiring databases can be tricky

    • Solution: Use Docker containers or in-memory databases
  4. UI Testing Challenges: UI tests can be brittle and slow

    • Solution: Run UI tests separately or less frequently

Example: Using Test Parameterization in CI

Here's how to make your tests more flexible in CI environments using parameterization:

csharp
public class DatabaseTests
{
private readonly string _connectionString;

public DatabaseTests()
{
// Use environment variable in CI, local connection otherwise
_connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING")
?? "Server=localhost;Database=TestDb;Trusted_Connection=True;";
}

[Fact]
public async Task CanConnectToDatabase()
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
Assert.True(connection.State == System.Data.ConnectionState.Open);
}
}

In your CI pipeline, you would set the TEST_CONNECTION_STRING environment variable to point to your CI test database.

Summary

Continuous Integration is a critical practice for modern .NET development that helps teams deliver high-quality software faster. By automating the build and test processes, CI ensures that code changes are verified early and often, reducing integration problems and improving software quality.

We've covered:

  • The fundamentals of CI for .NET projects
  • Setting up CI pipelines with GitHub Actions and Azure Pipelines
  • Best practices for effective .NET CI
  • Common challenges and solutions
  • Real-world examples of CI configurations

By implementing CI in your .NET projects, you'll improve code quality, increase developer productivity, and create a more collaborative development environment.

Additional Resources

Exercises

  1. Set up a basic CI pipeline using GitHub Actions for an existing .NET project
  2. Add code coverage reporting to your CI pipeline
  3. Implement a multi-stage pipeline that includes both CI and deployment steps
  4. Configure branch policies to enforce CI checks before merging pull requests
  5. Extend your CI pipeline to include static code analysis using a tool like SonarCloud


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)