
Learn how to build Azure DevOps pipelines for .NET CI/CD. Step-by-step YAML tutorial to deploy your C# app to Azure automatically. Start now!
If you are still building and deploying your C# applications by hand, you are losing hours every week and inviting "it works on my machine" bugs into production. Azure DevOps pipelines solve this by automating the entire journey from code commit to cloud deployment. In this hands-on tutorial, you will learn how to build a complete CI/CD pipeline for a .NET application using Azure DevOps — covering build, test, artifact publishing, and automatic deployment to Azure. Whether you are a beginner searching for "how to set up Azure DevOps CI/CD" or a senior engineer looking for advanced YAML best practices, this guide has you covered.
What Are Azure DevOps Pipelines and Why They Matter
Azure DevOps pipelines are a cloud-based service that automatically builds, tests, and deploys your code every time you push a change. They are the engine behind continuous integration (CI) and continuous delivery (CD) — two practices that separate high-performing engineering teams from the rest.
Here is the core idea: CI means every commit is automatically compiled and tested, so integration bugs are caught within minutes instead of days. CD means that once code passes those checks, it is automatically packaged and shipped to your cloud environment. For .NET teams, Azure DevOps is a natural fit because it integrates seamlessly with Azure App Service, Azure Functions, and Azure Container Apps.
Why does this matter? Manual deployments are slow, error-prone, and don't scale. Automating your Azure DevOps CI/CD pipeline gives you faster feedback, repeatable releases, and the confidence to deploy multiple times a day.
Pipelines as Code: The YAML Advantage
Azure DevOps supports two pipeline styles: the classic visual editor and YAML pipelines. Modern best practice is pipelines as code using a YAML file checked into your repository. This means your build process is version-controlled, reviewable in pull requests, and reproducible across branches. Throughout this YAML pipeline tutorial, we'll use that approach.
Prerequisites for Your .NET CI/CD Pipeline
Before you build your first pipeline, make sure you have the following:
- An Azure DevOps organization and project (free tier works perfectly).
- A .NET application (this tutorial uses an ASP.NET Core web app targeting .NET 8).
- Your code in a Git repository — Azure Repos or GitHub both work.
- An Azure subscription with an App Service created for deployment.
- A service connection linking Azure DevOps to your Azure subscription.
Step 1: Create the azure-pipelines.yml File
Azure DevOps looks for a file named azure-pipelines.yml in the root of your repository. This single file defines your entire build and deploy workflow. Let's start with a minimal pipeline that restores, builds, and tests a .NET project.
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- script: dotnet restore
displayName: 'Restore NuGet packages'
- script: dotnet build --configuration $(buildConfiguration) --no-restore
displayName: 'Build the solution'
- script: dotnet test --configuration $(buildConfiguration) --no-build --logger trx
displayName: 'Run unit tests'
Let's unpack what each section does, because understanding the why is what turns a copied snippet into a skill you own:
- trigger — tells the pipeline to run automatically whenever code is pushed to the
mainbranch. This is the heart of continuous integration. - pool — selects a Microsoft-hosted agent.
ubuntu-latestis fast and free, but you can usewindows-latestif your app depends on Windows-specific libraries. - variables — centralizes values like build configuration so you change them in one place.
- steps — the ordered tasks. Note the
--no-restoreand--no-buildflags: they avoid redundant work and speed up your pipeline significantly.
Step 2: Publish a Deployable Artifact
Building and testing is the CI half. To get to deployment, you need to package your app into an artifact — a versioned, self-contained output that the deploy stage can consume. Add these steps to publish your ASP.NET Core app as a zip and upload it.
- script: dotnet publish --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)
displayName: 'Publish application'
- task: PublishBuildArtifacts@1
displayName: 'Publish artifact to Azure DevOps'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
The $(Build.ArtifactStagingDirectory) is a built-in variable pointing to a temporary folder for build output. Publishing the artifact decouples your build from your deployment — a key principle of reliable CI/CD. You build once and deploy the exact same binary to every environment, eliminating the "but it built fine yesterday" class of bugs.
Step 3: Deploy to Azure App Service Automatically
Now for the part everyone searches for: automatic .NET deployment to the cloud. The cleanest way to structure this is with multi-stage pipelines — a Build stage and a Deploy stage. This separation makes your pipeline readable and lets you add approval gates between stages.
trigger:
branches:
include:
- main
variables:
buildConfiguration: 'Release'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '8.0.x'
- script: dotnet restore
- script: dotnet build --configuration $(buildConfiguration) --no-restore
- script: dotnet test --configuration $(buildConfiguration) --no-build
- script: dotnet publish --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
- stage: Deploy
displayName: 'Deploy to Azure'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployWeb
environment: 'production'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy to Azure App Service'
inputs:
azureSubscription: 'MyAzureServiceConnection'
appName: 'my-dotnet-webapp'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
A few important details here that separate a working pipeline from a fragile one:
- dependsOn and condition: succeeded() ensure the deploy stage only runs if the build and tests pass. Never deploy broken code.
- deployment job (instead of a regular
job) unlocks deployment strategies and ties the run to an environment, giving you deployment history and traceability. - azureSubscription references the service connection you created. Azure DevOps handles authentication securely behind the scenes — never put credentials in YAML.
Setting Up the Azure Service Connection
In your Azure DevOps project, go to Project Settings → Service connections → New service connection → Azure Resource Manager. Use the automated (workload identity federation) option — it's the most secure and avoids long-lived secrets. Name it exactly as referenced in your YAML, for example MyAzureServiceConnection.
Best Practices for Azure DevOps Pipelines for .NET
Once your basic pipeline works, these best practices will keep it fast, secure, and maintainable as your project grows.
1. Cache NuGet Packages to Speed Up Builds
Restoring packages on every run wastes time. Use the Cache task to persist your NuGet cache between runs, often cutting restore time from minutes to seconds.
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
restoreKeys: 'nuget | "$(Agent.OS)"'
path: '$(NUGET_PACKAGES)'
displayName: 'Cache NuGet packages'
2. Add Approval Gates for Production
For production environments, configure approvals and checks on the environment in Azure DevOps. This requires a human to click "Approve" before the deploy stage runs — a critical safety net for high-stakes releases.
3. Use Variable Groups and Azure Key Vault for Secrets
Never hardcode connection strings or API keys. Store them in a variable group linked to Azure Key Vault, then reference them in your pipeline. This keeps secrets out of source control and rotates them centrally.
4. Publish Test Results and Code Coverage
Make test outcomes visible directly in the pipeline summary. Collect coverage with --collect "Code Coverage" and publish results so failing tests are obvious at a glance.
- task: PublishTestResults@2
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
condition: succeededOrFailed()
Common Pitfalls to Avoid
Even experienced developers hit these snags when building their first Azure DevOps CI/CD pipeline. Watch out for them:
- Wrong artifact path on deploy. The
$(Pipeline.Workspace)/droppath must match your artifact name. A mismatch is the number one cause of "package not found" errors. - Forgetting the SDK version task. Hosted agents may default to a different .NET version. Always pin your SDK with
UseDotNet@2to avoid surprise build failures. - Deploying without testing. Skipping
condition: succeeded()means a failed build can still ship. Always gate your deploy stage. - Running everything on every push. Use
triggerpath filters and branch filters so unrelated changes (like README edits) don't trigger full deployments. - Storing secrets in YAML. Anything in your repo is visible to everyone with read access. Use Key Vault or secret variables instead.
Putting It All Together
With the multi-stage pipeline above, every push to main now automatically restores, builds, tests, packages, and deploys your .NET app to Azure — no manual steps, no missed commands, no human error. That is the full promise of CI/CD delivered through Azure DevOps pipelines: code to cloud, automatically.
As your needs grow, you can layer in container builds with Docker, deploy to Azure Kubernetes Service, run integration tests against staging slots, and use deployment slots for zero-downtime releases with the AzureWebApp@1 task's slot settings.
Conclusion and Key Takeaways
You now have a complete, production-ready blueprint for building Azure DevOps pipelines for .NET. Automating your CI/CD pipeline is one of the highest-leverage investments you can make — it pays back every single day in faster releases and fewer bugs. Here are the key takeaways to remember:
- Pipelines as code: Define everything in
azure-pipelines.ymlso your process is versioned and reviewable. - Separate build and deploy stages: Build once, deploy the same artifact, and gate deployment behind passing tests.
- Secure your secrets: Use service connections, variable groups, and Azure Key Vault — never hardcode credentials.
- Optimize for speed: Cache NuGet packages and use
--no-restore/--no-buildflags to keep runs fast. - Add safety nets: Use environment approvals for production deployments.
Start small with a single build-and-test pipeline, get it green, then add deployment. Within an afternoon you can have your C# application flowing automatically from code to cloud. Ready to ship faster? Open Azure DevOps, create your azure-pipelines.yml, and push your first commit today.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment