.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:
- Source Control Integration: Connection to your repository (Git, Azure DevOps, etc.)
- Build Automation: Compiling your .NET code
- Automated Testing: Running unit, integration, and other tests
- Code Quality Analysis: Static code analysis and code coverage
- Artifacts Generation: Creating deployable packages
- 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
:
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 repositorysetup-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:
- 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:
- Register on SonarCloud
- 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:
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:
- Triggers for both
main
anddevelop
branches - Builds the application in Release mode
- Runs tests with code coverage
- Uploads the coverage report to Codecov
- Publishes the application
- 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:
- Azure DevOps Pipelines: Microsoft's CI/CD service, tightly integrated with .NET
- Jenkins: Open-source automation server with plugins for .NET
- TeamCity: JetBrains' CI server with excellent .NET support
- GitLab CI: GitLab's integrated CI/CD solution
- 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:
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:
- Run Tests Frequently: Include unit, integration, and UI tests in your pipeline
- Keep Builds Fast: Optimize build steps and parallelize tests
- Use Build Caching: Speed up builds by caching NuGet packages
- Standardize Environments: Ensure CI environments match production
- Implement Branch Policies: Prevent code from being merged if CI checks fail
- Monitor Test Coverage: Track code coverage and enforce minimum thresholds
- 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:
-
Long Build Times: Large .NET solutions can take time to build
- Solution: Use incremental builds and optimize test execution
-
Windows-Specific Dependencies: Some .NET Framework projects require Windows
- Solution: Use Windows build agents for .NET Framework projects
-
Database Integration Testing: Tests requiring databases can be tricky
- Solution: Use Docker containers or in-memory databases
-
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:
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
- Microsoft Learn: Implement Continuous Integration
- GitHub Actions Documentation
- Azure DevOps Pipelines Documentation
- .NET Testing Documentation
Exercises
- Set up a basic CI pipeline using GitHub Actions for an existing .NET project
- Add code coverage reporting to your CI pipeline
- Implement a multi-stage pipeline that includes both CI and deployment steps
- Configure branch policies to enforce CI checks before merging pull requests
- 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! :)