Skip to main content

Azure DevOps Pipelines for .NET: CI/CD Tutorial

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 main branch. This is the heart of continuous integration.
  • pool — selects a Microsoft-hosted agent. ubuntu-latest is fast and free, but you can use windows-latest if 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-restore and --no-build flags: 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)/drop path 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@2 to 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 trigger path 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.yml so 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-build flags 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.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...