Skip to main content

.NET CI/CD Pipeline

Introduction

Continuous Integration and Continuous Deployment (CI/CD) pipelines have become essential components in modern software development. They automate the process of building, testing, and deploying applications, helping developers deliver high-quality code more efficiently and reliably.

In this tutorial, we'll explore how to set up CI/CD pipelines for .NET applications. We'll look at the core concepts, tools, and practical implementations that will help you automate your .NET application's development lifecycle.

What is a CI/CD Pipeline?

A CI/CD pipeline is an automated workflow that helps developers integrate code changes more frequently and reliably deploy applications to production environments.

  • Continuous Integration (CI) involves automatically building and testing code changes whenever they're committed to version control.
  • Continuous Deployment (CD) extends the CI process by automatically deploying successful builds to various environments (development, staging, production).

CI/CD Pipeline Flow

Benefits of CI/CD for .NET Applications

  1. Faster Development Cycles: Automate repetitive tasks like building, testing, and deployment
  2. Higher Code Quality: Detect bugs early through automated testing
  3. Consistent Deployments: Reduce human error in deployment processes
  4. Quicker Feedback: Get immediate feedback on code changes
  5. Reduced Risk: Deploy smaller changes more frequently

Essential Components of a .NET CI/CD Pipeline

A typical .NET CI/CD pipeline includes these key stages:

  1. Source Control: Code repository (GitHub, Azure DevOps, GitLab)
  2. Build: Compilation and assembly of code
  3. Test: Running unit tests, integration tests, etc.
  4. Package: Creating deployment packages (NuGet, Docker images)
  5. Deploy: Deploying to target environments

Setting Up CI/CD for .NET Applications

Let's explore three popular platforms for setting up CI/CD pipelines for .NET applications:

1. GitHub Actions

GitHub Actions allows you to create workflows directly in your GitHub repository. Here's how to create a basic CI/CD pipeline for a .NET application:

  1. Create a .github/workflows directory in your repository
  2. Add a YAML file (e.g., dotnet.yml) with your workflow configuration
yaml
name: .NET CI/CD Pipeline

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: 7.0.x

- name: Restore dependencies
run: dotnet restore

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

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

- name: Publish
run: dotnet publish -c Release -o ./publish

- name: Deploy to Azure Web App
if: github.ref == 'refs/heads/main'
uses: azure/webapps-deploy@v2
with:
app-name: 'your-app-name'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ./publish

This workflow will:

  • Trigger when code is pushed to the main branch or when a pull request targets the main branch
  • Set up .NET on the runner
  • Restore dependencies, build, and test your application
  • Publish the application if all previous steps succeed
  • Deploy to an Azure Web App if the branch is main

2. Azure DevOps Pipelines

Azure DevOps offers robust CI/CD capabilities that integrate well with .NET applications. Here's a sample YAML pipeline:

yaml
trigger:
- main

pool:
vmImage: 'windows-latest'

variables:
buildConfiguration: 'Release'
dotnetSdkVersion: '7.0.x'

stages:
- stage: 'Build'
displayName: 'Build and Test'
jobs:
- job: 'BuildAndTest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '$(dotnetSdkVersion)'

- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: '**/*.csproj'

- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'

- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
projects: '**/*Tests/*.csproj'
arguments: '--configuration $(buildConfiguration)'

- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'

- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'

- stage: 'Deploy'
displayName: 'Deploy to Azure'
dependsOn: 'Build'
condition: succeeded()
jobs:
- job: 'DeployToAzure'
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'drop'
downloadPath: '$(System.ArtifactsDirectory)'

- task: AzureRmWebAppDeployment@4
inputs:
ConnectionType: 'AzureRM'
azureSubscription: 'Your-Azure-Subscription'
appType: 'webApp'
WebAppName: 'your-app-name'
packageForLinux: '$(System.ArtifactsDirectory)/**/*.zip'

This pipeline organizes the workflow into stages for build and deploy, making it clear which steps belong to each phase of the process.

3. Jenkins Pipeline for .NET

If you're using Jenkins for CI/CD, you can define a pipeline in a Jenkinsfile:

groovy
pipeline {
agent any

environment {
DOTNET_CLI_HOME = '/tmp/dotnet_cli_home'
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
}

stages {
stage('Checkout') {
steps {
checkout scm
}
}

stage('Restore') {
steps {
sh 'dotnet restore'
}
}

stage('Build') {
steps {
sh 'dotnet build --configuration Release --no-restore'
}
}

stage('Test') {
steps {
sh 'dotnet test --configuration Release --no-build'
}
}

stage('Publish') {
steps {
sh 'dotnet publish --configuration Release --no-build --output ./publish'
}
}

stage('Deploy') {
when {
branch 'main'
}
steps {
// Example deployment command (adjust as needed)
sh 'echo "Deploying application..."'
// Add your deployment steps here
}
}
}

post {
always {
// Clean up workspace
cleanWs()
}
success {
echo 'CI/CD pipeline completed successfully!'
}
failure {
echo 'CI/CD pipeline failed!'
}
}
}

Real-World Example: Building a Complete CI/CD Pipeline for a .NET Core Web API

Let's build a practical example of a CI/CD pipeline for a .NET Core Web API that includes:

  • Building and testing the application
  • Creating a Docker image
  • Deploying to Kubernetes

Step 1: Create a Basic .NET Core Web API

First, we'll need a simple Web API to work with:

csharp
// Controllers/WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;

namespace MyApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
}

Step 2: Create a Dockerfile

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["MyApi.csproj", "./"]
RUN dotnet restore "MyApi.csproj"
COPY . .
RUN dotnet build "MyApi.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
EXPOSE 80
ENTRYPOINT ["dotnet", "MyApi.dll"]

Step 3: Create a GitHub Actions Workflow for Complete CI/CD

yaml
name: .NET API CI/CD Pipeline

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

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-test-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

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

- name: Restore dependencies
run: dotnet restore

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

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

# Only proceed with Docker build and deployment on main branch
- name: Login to Container Registry
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata for Docker
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

# Deploy to Kubernetes
- name: Set up kubectl
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: azure/setup-kubectl@v3

- name: Set Kubernetes context
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBECONFIG }}

- name: Deploy to Kubernetes
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
# Update the k8s deployment manifest with the new image
sed -i 's|IMAGE_TO_REPLACE|${{ steps.meta.outputs.tags }}|g' k8s/deployment.yaml

# Apply the Kubernetes manifests
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml

Step 4: Create Kubernetes Manifests

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dotnet-api
labels:
app: dotnet-api
spec:
replicas: 3
selector:
matchLabels:
app: dotnet-api
template:
metadata:
labels:
app: dotnet-api
spec:
containers:
- name: dotnet-api
image: IMAGE_TO_REPLACE # This will be replaced during CI/CD
ports:
- containerPort: 80
resources:
limits:
cpu: "500m"
memory: "256Mi"
requests:
cpu: "200m"
memory: "128Mi"
yaml
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: dotnet-api-service
spec:
selector:
app: dotnet-api
ports:
- port: 80
targetPort: 80
type: ClusterIP

Advanced CI/CD Pipeline Techniques

1. Multi-Environment Deployments

For enterprise applications, you might need to deploy to multiple environments:

yaml
name: Multi-Environment Deployment

on:
push:
branches: [ main ]

jobs:
build:
# Build and test steps here

deploy-dev:
needs: build
environment: Development
runs-on: ubuntu-latest
steps:
# Deploy to development environment

deploy-staging:
needs: deploy-dev
environment: Staging
runs-on: ubuntu-latest
steps:
# Deploy to staging environment

deploy-production:
needs: deploy-staging
environment: Production
# Requires manual approval
runs-on: ubuntu-latest
steps:
# Deploy to production environment

2. Database Migrations

Including database migrations in your CI/CD pipeline:

yaml
- name: Apply Database Migrations
run: |
dotnet tool install --global dotnet-ef
dotnet ef database update --project src/MyApp.Data
env:
ConnectionStrings__DefaultConnection: ${{ secrets.DB_CONNECTION }}

3. Security Scanning

Adding security scanning to your pipeline:

yaml
- name: Security scan
uses: snyk/actions/dotnet@master
with:
args: --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

Best Practices for .NET CI/CD Pipelines

  1. Keep builds fast: Optimize build scripts and use build caching
  2. Separate unit and integration tests: Run unit tests early and frequently
  3. Use environment variables for sensitive information
  4. Version your artifacts: Tag Docker images, NuGet packages with appropriate versions
  5. Implement proper branch policies: Protect important branches
  6. Use Infrastructure as Code (IaC): Define infrastructure with tools like Terraform or ARM templates
  7. Monitor your pipelines: Set up alerts for failed builds/deployments
  8. Include quality gates: Code coverage, static code analysis

Troubleshooting Common Issues

Here are some common issues you might encounter with .NET CI/CD pipelines and how to solve them:

Build Failures

  • Issue: Build fails due to missing dependencies
  • Solution: Ensure your pipeline has proper restore steps before build

Test Failures

  • Issue: Tests pass locally but fail in CI
  • Solution: Check for environment-specific dependencies or race conditions

Deployment Failures

  • Issue: Deployment fails with permission errors
  • Solution: Verify service principal or account permissions

Summary

In this tutorial, we've covered:

  • The fundamentals of CI/CD pipelines for .NET applications
  • Setting up CI/CD with GitHub Actions, Azure DevOps, and Jenkins
  • A complete real-world example with Docker and Kubernetes deployment
  • Advanced techniques and best practices for effective CI/CD

By implementing CI/CD pipelines, you can significantly improve your .NET application development workflow, making it more efficient, reliable, and maintainable.

Additional Resources

Practice Exercises

  1. Set up a basic CI pipeline for a simple .NET console application
  2. Extend your pipeline to include unit testing with xUnit or NUnit
  3. Create a CD pipeline that deploys your application to Azure App Service
  4. Implement a multi-stage pipeline that deploys to staging first, then production
  5. Add code quality checks using SonarQube or other static analysis tools


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