Setup CI Build Pipeline in Azure DevOps for ASP.NET Core Web API
From zero to a working CI pipeline that builds your .NET app, runs unit tests, and produces a deployable artifact - step by step.
Connect with me:
Want to sponsor this newsletter? Let’s work together →
Introduction
Let’s say you have a .NET application on your local PC and you’d like to start using Azure DevOps - to store your code, automate builds, and run unit tests on every change.
Where do you even start?
In this article we’ll go from point zero - Visual Studio and your .NET code on your machine - to a fully working CI (Continuous Integration) pipeline in Azure DevOps that:
Pushes your existing code to Azure Repos
Builds your application on every merge to main
Runs your unit tests automatically
Produces a deployable build artifact (a ZIP of your published app)
Validates every pull request before it can be merged - separated PR verification pipeline
🎬 Watch the full video here:
Setting Up Azure DevOps
If you’re starting from scratch, follow these steps:
Create a Microsoft account if you don’t have one
Go to Azure DevOps and select “Get started for free”
Create an organization
Create a new project inside that organization
Once inside your project you’ll see the main sections: Boards, Repos, Pipelines, Test Plans, and Artifacts. We’ll be working mostly with Repos and Pipelines.
Pushing Your Code to Azure Repos
Navigate to Repos → Files. Azure DevOps gives you a few options to get your code in:
Push an existing local repository - use the git commands shown on the page to add Azure Repos as a remote and push your full commit history
Import from GitHub - paste your GitHub repo URL and provide credentials if needed
Initialize a new empty repository - useful if you’re starting fresh; clone it locally, copy your code in, then commit and push
In the tutorial I created a new repository called TaskManager and used the “push an existing repository” command since the project was already using Git locally:
git remote add origin <azure-repos-url>
git push -u origin --allAfter refreshing the page, all code and full commit history appeared in Azure Repos.
If you’re not using Git yet, the recommended path is: create the repo with the Visual Studio .gitignore template → clone it locally → copy your code in → git add, commit, and push.
The Two YAML Pipeline Files
All pipeline logic lives in YAML files that you version alongside your code. We’ll create two:
Build.yaml - triggers on merges to main, builds the app, runs tests, publishes the app, and uploads the artifact
PullRequestVerification.yaml - triggers on pull requests targeting main, builds the app and runs tests as a quality gate
Create both files by right-clicking the solution in Visual Studio → Add → New Item.
Build.yaml - CI Build Pipeline
This pipeline runs every time a commit lands on main (e.g. after a PR is merged). It builds the app, runs tests, publishes, and uploads the artifact.
trigger:
branches:
include:
- main
paths:
exclude:
- "*.md"
variables:
buildConfiguration: "Release"
dotnetVersion: "10.0.x"
solutionPath: "TaskManager.slnx"
unitTestsPath: "TaskManager.Application.Tests/TaskManager.Application.Tests.csproj"
pool:
vmImage: "ubuntu-latest"
steps:
- task: UseDotNet@2
displayName: "Install .NET $(dotnetVersion)"
inputs:
packageType: "sdk"
version: $(dotnetVersion)
- task: DotNetCoreCLI@2
displayName: "Restore"
inputs:
command: "restore"
projects: $(solutionPath)
- task: DotNetCoreCLI@2
displayName: "Build"
inputs:
command: "build"
projects: $(solutionPath)
arguments: "--configuration $(buildConfiguration) --no-restore"
- task: DotNetCoreCLI@2
displayName: "Run unit tests"
inputs:
command: "test"
projects: $(unitTestsPath)
arguments: "--configuration $(buildConfiguration) --no-build"
publishTestResults: true
- task: DotNetCoreCLI@2
displayName: "dotnet publish"
inputs:
command: "publish"
projects: "TaskManager.API/TaskManager.API.csproj"
arguments: "--configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)/drop"
- task: PublishBuildArtifacts@1
displayName: "Upload artifact"
inputs:
PathtoPublish: "$(Build.ArtifactStagingDirectory)/drop"
ArtifactName: "drop"
publishLocation: "Container"A few things worth noting:
Markdown files are excluded from the trigger - changes to docs don’t cause a rebuild
--no-restore and --no-build flags avoid repeating work already done in earlier steps
Build.ArtifactStagingDirectory is a temporary holding area on the agent; the PublishBuildArtifacts task is what actually ships those files to Azure DevOps storage so they’re accessible from the pipeline UI and can be picked up by a release pipeline for deployment
PullRequestVerification.yaml - PR Quality Gate
This pipeline is the gatekeeper for the main branch. It runs on every pull request targeting main - not on direct commits.
trigger: none
pr:
branches:
include:
- main
paths:
exclude:
- "*.md"
variables:
buildConfiguration: "Release"
dotnetVersion: "10.0.x"
solutionPath: "TaskManager.slnx"
unitTestsPath: "TaskManager.Application.Tests/TaskManager.Application.Tests.csproj"
pool:
vmImage: "ubuntu-latest"
steps:
- task: UseDotNet@2
displayName: "Install .NET $(dotnetVersion)"
inputs:
packageType: "sdk"
version: $(dotnetVersion)
- task: DotNetCoreCLI@2
displayName: "Restore"
inputs:
command: "restore"
projects: $(solutionPath)
- task: DotNetCoreCLI@2
displayName: "Build"
inputs:
command: "build"
projects: $(solutionPath)
arguments: "--configuration $(buildConfiguration) --no-restore"
- task: DotNetCoreCLI@2
displayName: "Run unit tests"
inputs:
command: "test"
projects: $(unitTestsPath)
arguments: "--configuration $(buildConfiguration) --no-build"
publishTestResults: truetrigger: none explicitly disables push-based triggers. The pr block is what activates this pipeline - whenever someone opens or updates a pull request targeting main, this kicks off automatically.
Creating the Pipelines in Azure DevOps
Push both YAML files to main, then:
Go to Pipelines → Pipelines → New Pipeline
Select Azure Repos Git → choose your repository
Select Existing Azure Pipelines YAML file
Pick
/build.yaml→ SaveRepeat for
/pull-request-verification.yaml
Rename them for clarity via the three-dot menu:
CI-Build–Task ManagerPullRequestVerification–TaskManager
Enabling Branch Policy on Main
To enforce the PR pipeline as a required check before merging:
Go to Repos → Branches
Click the three dots next to
main→ Branch policiesUnder Build validation → Add build policy
Select the
PullRequestVerification–TaskManagerpipelineSet trigger to Automatic, policy to Required
Save
Now no pull request can be merged to main unless the build and tests pass.
End-to-End Test
To verify everything works:
Create a feature branch
Make a small change and push
Open a pull request targeting main
You’ll see the PR verification pipeline queue automatically. Once it passes (build succeeded, all unit tests green), you can complete the merge.
After merging, the CI Build pipeline kicks off on main - builds, tests, publishes, and uploads the artifact. The result is a TaskManager.API.zip in the Artifacts tab, ready for deployment.
Key Takeaways
Azure Repos stores your code and full Git history - push your existing repo with a single command
Build.yaml - CI pipeline triggered on main; builds, tests, publishes, and uploads a deployable artifact
PullRequestVerification.yaml - PR gate; builds and runs tests before any merge is allowed
trigger: none + pr block - the correct way to restrict a pipeline to PR events only
--no-restore / --no-build flags - skip redundant work between pipeline steps
PublishBuildArtifacts - transfers files from the build agent to Azure DevOps storage; without this step, the artifact doesn’t exist outside the agent
Branch policies - enforce the PR pipeline as a required check on main so broken code can never be merged
What’s Next?
The next tutorial will cover creating a release pipeline that picks up this artifact and deploys it to an environment. Make sure to subscribe so you don’t miss it.
Resources
Azure DevOps Documentation: https://learn.microsoft.com/en-us/azure/devops/
Azure Pipelines YAML reference: https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/
Connect with me:
Follow me on LinkedIn
Subscribe on YouTube
Want to sponsor this newsletter? Let’s work together →
