Setup CI Build Pipeline in Azure DevOps for ASP.NET Core Web API

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 --all
After 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 artifactPullRequestVerification.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-restoreand--no-buildflags avoid repeating work already done in earlier stepsBuild.ArtifactStagingDirectoryis a temporary holding area on the agent; thePublishBuildArtifactstask 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: true
trigger: 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- Save - Repeat 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 policies
- Under Build validation - Add build policy
- Select the
PullRequestVerification-TaskManagerpipeline - Set 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.yamlis the CI pipeline triggered on main; it builds, tests, publishes, and uploads a deployable artifact.PullRequestVerification.yamlacts as a PR gate; it builds and runs tests before any merge is allowed.trigger: nonecombined with aprblock is the correct way to restrict a pipeline to PR events only.--no-restore/--no-buildflags skip redundant work between pipeline steps.PublishBuildArtifactstransfers 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 covers creating a release pipeline that picks up this artifact and deploys it to an environment. Subscribe so you don't miss it.