In this new post, I explain how deploying Windows Services using pipelines in Azure DevOps. This helps you achieve a proper CD/CI for your Windows Service projects. Some context is provided by the Microsoft documentation.
Here the list of posts related to this one:
The source code of this post is available on GitHub.
Windows Services built on .NET Core and classic .NET Framework can be deployed using Azure DevOps to our target machine(s). They can automatically run on these machines. This process removes the need to copy files manually.
You will have the following ready:
- An Azure DevOps account
- Your working Windows Service code is committed in Azure DevOps Repositories
- A target Windows machine to deploy to with an internet connection that you have access to. The target machine must have access to the URL
dev.azure.com
So, I assume that the code for the Windows Service is in a repository and the pipeline will build the service in this repository and deploy it on the Windows Service machine.
Create a build pipeline
First, I created in my Azure DevOps a project called WindowsServiceTest and here I’m going to create the pipeline. Now, on the menu on the left select Pipelines and Create Pipeline.

After clicking on the create button, I can select where the code is. As I said above, the code is in Azure DevOps. So, I choose Azure Repos Git. In the following screenshot an example where to click.

After that, I have to select the repository. In my case, this is straightforward because I don’t have any other repository apart of the main one.

Now, Azure DevOps asks if I want to start with a new pipeline (Starter pipeline) or using an existing one.

Click on the button Show more to display a list of other pipelines. I can see this screen now.

Now, from the list, select .NET Desktop. This boilerplate offers us some useful settings. This will be relevant for both .NET Core and .NET Framework. This is the YAML out-of-the-box.
# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net
trigger:
- main
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: VSTest@2
inputs:
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'

Customize the pipeline
Before showing the pipeline, a few consideration. I have some NuGet packages stored in the Azure DevOps Artifacts. I want to restore those packages from my artifacts feed. Also, my priority is to run tests and show the Tests result and the Code coverage.
General settings
First, the general settings in the YAML. The trigger is from the main branch. The image I want to use is the windows-latest
. As a variables
, I set the build as a Release
.
trigger:
- main
pool:
vmImage: windows-latest
variables:
buildConfiguration: 'Release'
After that, I will add the steps. Now, I explain step by step the tasks I want to add.
Install .NET
Now, I built the Windows Service with the version 8 of NET Core. So, the first task is to install it.
- task: UseDotNet@2
displayName: 'Use dotnet 8'
inputs:
version: '8.0.x'
Use NuGet
As I said before, some of the packages are in the artifacts, I added a Nuget.config
in the root of the project. The tasks I’m adding are related to install and use the NuGet tool in the pipeline. After that, I list the NuGet sources and then restore the packages for the solution.
- task: NuGetToolInstaller@1
- task: NuGetAuthenticate@1
displayName: 'Authenticate to Azure Artifacts feed'
- script: dotnet nuget list source
displayName: 'List NuGet sources'
- task: NuGetCommand@2
displayName: 'NuGet Restore with custom config'
inputs:
restoreSolution: '**/*.sln'
feedsToUse: config
nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.config'
This is an example of the NuGet.config
I use for the project. This file must be in the root of the project.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="<feedName>" value="https://pkgs.dev.azure.com/<yourfeed>/nuget/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Build the project
Now, the next step is to build the projects. The output of the build will be save/copy in the directory ci-build
in the StagingDirectory
of Azure DevOps. I decided to save the build there. This provides a clear place where I can find all the files. Then, compress them for the next deployment.
- task: DotNetCoreCLI@2
displayName: Build project
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration) -o $(Build.StagingDirectory)/ci-build'
Run the tests
All my projects have tests. So, before continuing with the deployment, I want to be sure that all the tests passed. After that, I want to publish the collect the results of the tests and the code coverage.
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: '**/*[Te]ests/*.csproj'
arguments: '--configuration $(buildConfiguration) --collect "Code Coverage" --collect "XPlat Code Coverage"'
Publish the code coverage
Once, the tests are completed and passed and the code coverage determined, I will publish the result.
- task: PublishCodeCoverageResults@2
displayName: 'Publish code coverage'
inputs:
summaryFileLocation: '$(Agent.BuildDirectory)/**/coverage.cobertura.xml'
pathToSources: '$(Agent.BuildDirectory)/**/coverage'
Therefore, open an executed pipeline, I can see there are some new tabs: Tests and Code Coverage.

Under the tab Tests, I see all the tests and if they passed or not.

The Code Coverage tab shown how much code is covered by tests. Generally speaking, the code coverage should be around 77% to get a good coverage of the projects.

Zip the files
Now that the build is done, I want to zip the files in the ci-build
folder. The resulted zip file will be publish in the artifacts to be deploy later in the machine.
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(Build.StagingDirectory)/ci-build'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
So, the configuration I use create a zip file and the name is the BuildId
(a number). I don’t want to include the root of the folder but only the content of the folder. The file must be a zip file format. If the zip file exists, it can be replaced.
Publish artifacts
Finally, I want to publish the zip file in the artifacts. With that, the release will be use this artifact to deploy the Windows Service into the Windows Server machine or any other target machines. This will allow us for deploying Windows Services using Azure DevOps.
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
Full YAML
trigger:
- main
pool:
vmImage: windows-latest
variables:
buildConfiguration: 'Release'
steps:
- task: UseDotNet@2
displayName: 'Use dotnet 8'
inputs:
version: '8.0.x'
- task: NuGetToolInstaller@1
- task: NuGetAuthenticate@1
displayName: 'Authenticate to Azure Artifacts feed'
- script: dotnet nuget list source
displayName: 'List NuGet sources'
- task: NuGetCommand@2
displayName: 'NuGet Restore with custom config'
inputs:
restoreSolution: '**/*.sln'
feedsToUse: config
nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.config'
- task: DotNetCoreCLI@2
displayName: Build project
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration) -o $(Build.StagingDirectory)/ci-build'
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: '**/*[Te]ests/*.csproj'
arguments: '--configuration $(buildConfiguration) --collect "Code Coverage" --collect "XPlat Code Coverage"'
- task: PublishCodeCoverageResults@2
displayName: 'Publish code coverage'
inputs:
summaryFileLocation: '$(Agent.BuildDirectory)/**/coverage.cobertura.xml'
pathToSources: '$(Agent.BuildDirectory)/**/coverage'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(Build.StagingDirectory)/ci-build'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
Set the Deployment group
Now, to deploy the services on the Windows Servers or any other machine, I need to specify our deployment target. Go to Deployment Groups and then New or Add a deployment group.

Give your Deployment group a Name and description and click Create:

On the next screen, Azure DevOps gives me the PowerShell script I have to execute on the target machines. This will associate the machine with this group and I can use them later in the Releases. Click on Use a Personal access token, then Click Copy to clipboard:

Now go to your target machine, Open an Administrator-privileged Powershell command prompt, paste your script and then execute. This will take a while, maybe 2 to 5 minutes. You should see something like the following if all is well, indicating that the Azure Pipelines agent is installed on the machine correctly:

During the configuration, there are some questions. I use the default values.
- Enter deployment group tags for agent? (Y/N) (press enter for N): I pressed Enter
- Enter enable SERVICE_SID_TYPE_UNRESTRICTED for agent service (Y/N) (press enter for N): I pressed Y
- Enter User account to use for the service (press enter for NT AUTHORITY/SYSTEM): I pressed Enter
- Enter whether to prevent service starting immediately after configuration is finished? (Y/N) (press enter for N): I pressed Enter
So, now the configuration is completed. This is a example of what I can see after that.

Now, in the Deployment groups in Azure DevOps, I see the Target machine group. It has 1 machine. The machine is online.

Wrap up
In this first post about deploying Windows Services using Azure DevOps, I showed how to create the pipeline. This pipeline builds the solution of a Windows Server. Additionally, I demonstrated how to publish the artifact. In the next post, I will show how to create the release to deploy and run the Windows Server.
Happy coding!