Pragmatically upgrading .net framework version for all projects with PowerShell

Do feel free to provide any comments/feedback to @TheRichCarey on Twitter

We had a situation where we needed to upgrade all the CSPROJ files in a solution to 4.8. The issue is that some of our solutions contain almost a hundred projects so a manual intervention would be prone to error. (plus we have multiple solutions to apply this against!)

Whilst there are a number of extensions that do this on the VS Marketplace, they seemed a little overkill for something that can surely be achieved in PowerShell? At the end of the day its a simple find-and-replace, right?

This will load the solution file, iterate over each CSPROJ referenced and then replace the current framework version with the one specified in $versionToUse. It will then overwrite the file.

Potential improvements

This met my needs fine, but could be improved for sure! Things that it could do better are:

  • Auto scan for .sln files.
  • Provide some form of report at the end
  • Auto-checkout for TFS or Git (Using git CLI or TFS CLI)

Ensuring “dotnet test” TRX & Coverage files end up in SonarQube

Do feel free to provide any comments/feedback to @TheRichCarey on Twitter

I have written before about using SonarQube to do static analysis, but one issue I never came back to was ensuring that code coverage files generated via a build pipeline end up being picked up by the Sonar Scanner to assess code coverage.

Note that the following I am actually using the ‘dotnet test’ build step, rather than the ‘Vs Test’ one. Do let me know if you find a nice work around for the VS Test variant, as I couldn’t get it to drop coverage files!

The issue

The issue is that:

  • When using VSTest, TRX files are deleted automatically if using version 2+ of the VS Test task as per this stack overflow post.
  • When I switched back to ‘dotnet test’ the same thing appeared to be happening.
  • .coverage files are not output by default
  • TRX and Coverage files are placed in a temporary folder of the build agent rather than the executing agents working directory.
  • Even though SonarQube could detect the tests, it would still register as 0.0% code coverage!

Getting ‘dotnet test’ to collect coverage

The first step was to get the ‘dotnet test’ build step to collect the code coverage, and not just dump TRX files.

To do this, go to the “Arguments” field of the dotnet test build step and append --collect "Code Coverage", as well as ensuring that “Publish test results and code coverage” is enabled.

Ensure generated files are copied to the working directory

As the coverage files will end up in the /tmp folder of the build agent, SonarQube will not be able to scan them.

We will need to add a new build step of “copy files” with the correct filter set to get the .trxand .coverage files from the default temporary directory on the build agent, to the test results folder of the workspace. To do this we need to add the “Copy Files” task into the build and place it after the “VS Test” task. The source folder for the copy will be $(Agent.HomeDirectory)\_work\_temp and the target folder will be $(Common.TestResultsDirectory) – The contents can remain as ** but feel free to filter if required. Example below.

If we run a build now, we should now see files in the TestResults folder of the build agent’s working directory.

I didn’t have to make any changes to the configuration within SonarQube as it should just pick up the coverage files. If I follow the above I get the following (Lets just ignore the fact the number is low ๐Ÿ˜‰)

CSPROJ Changes to test projects

One thing I did notice in the console when attempting to fix this code coverage issue was that I got a lot of warnings like:

SonarQube.Integration.targets: warning : The project does not have a valid ProjectGuid. Analysis results for this project will not be uploaded to SonarQube. 

As all my projects were .net core or .net standard the CSPROJ files do not contain a <ProjectGuid> tag by default. As also suggested in this stack overflow answer , I added a GUID to my test project file. I am not 100% if this is required, but it stopped warnings appearing in my console and does no harm.


If you have multiple builds to update and you are using Azure Devops, you can take advantage of “Task Groups”. This allows you to create a single build step which in turn executes a series of other build steps. Using the steps above, you can create a new Task Group to create a single build step to run the test script and make sure the files are copied to the correct location for analysis. For example I have the single build step below:

Which means I can then just call this single build step in all my builds

Setting up a self-hosted build agent for Azure DevOps

Azure DevOps has brilliant build pipeline options and as easy as it is to get set up with their hosted build agents, it can get quite costly rather quick. In this post I cover off setting up a self-hosted build agent for use with Azure.

This post won’t cover setting up the build box, but can be covered in a later guide if required. I actually have my build box scripted out using Choco commands to allow building of .NET projects to make this step easier.


  • Pro: Full control over the build
  • Pro: Can have your builds build items or run services which simply aren’t available in the Hosted agents.
  • Pro: Low cost. If you already have the hardware, why pay for Azure VMs?
  • Con: Maintenance and redundancy. If the machine goes down or breaks it blocks your pipeline.
  • Con: Extra setup steps.


Before starting you will need to make sure:

  • You are a collection/build admin
  • You have a server configured to build the appropriate software (i.e. Correct SDKs etc which won’t be covered in this post)

Personal Access Tokens

First of all, you will need a personal access token for your account. This is used to allow your build agent access to Azure without hard-coding your credentials into your build scripts. You can use your own account for this, or a specially created service account – Just note it will need permissions to access the collections it will be building.

To get this, log in to your Azure Devops portal, and navigate to your security page.

In here, select “Personal Access Tokens” and then “New”. A panel will be displayed to configure this PAT. Specify a friendly and unique name, select the organisation you are using this token for, and then set its security access.

For the security access, I recommend selecting Full Access under “Scopes” so you can use this PAT for general Dev Ops activities. You can fine-tune the control, but you must ensure it has read/execute on the build scope as an absolute minimum. For expiry I typically select the longest period which is 1 year.

Agent download and configuration

Next up you will need to navigate to the project settings > Pipelines > Agent Pools.

Create a new Agent Pool with an appropriate name (You don’t *have* to do this and can just use the default pool if you wish, but I like the separation). When your pool is created you will see the option to add a new agent to it.

Clicking “New Agent” will give you the instructions for the OS of your choice. As per the instructions, download the agent (A ~130 ZIP file) and then place somewhere sensible on the machine that will be acting as a build server. When extracted, run config.cmd in an elevated command window

When running the config.cmd command you will require the following information:

  • Server URL
    • This will be{organisation name}
  • What type of authentication you will use (Just press return as it will default to PAT)
  • Your PAT to access the server, as set up in the first step.
  • The Pool to connect to. This will be the name of the agent pool created above.
  • The working folder. The folder to use for storing workspaces being built.
  • A name for this agent. Call it whatever you want, but I would personally always include the machine name as it makes it easier to work out which agents are running.

Providing all the above settings are specified correctly and there are no authentication issues, it should now attempt to start.

Confirming the agent is active

Going back to the Agent Pools configuration screen you should now see the agent listed in the appropriate agent pool.

If the agent is not displaying after a few minutes, something went wrong in setup.

If the agent is displaying offline, try running the “run.cmd” command in an elevated command window on your build server.

Now all you have to do is select your new agent pool when creating your next build!

Recursive folder comparison with PowerShell

The Issue

This post definitely isn’t “new” or revolutionary, but I was quite surprised to find the Compare-object helper in PS, and I’m bound to forget in the future…

As part of some recent roadmap work, we moved over to a new installer technology for some of our tooling. This came with some minor headaches such as validating we have harvested all the correct files. The first iteration of this was a manual check which obviously is prone is human error – aside from being mind numbing!

I didn’t really want to use a third party tool. WinMerge can perform fantastic comparisons, but I wanted something quick and custom. Ideally also not spending longer than 10 minutes creating any code!

The first iteration was to do a recursive loop, pull out all the file names (Note: not the path) into 2 separate text files. The only “nicety” was I wrapped directory names in square brackets to give it some organisation.

The downside of this is that it only really worked for my sample folder with a few items. In production with thousands of files and nested folders this was plain chaos. Also I had to compare these files in a third party tool like WinMerge anyway – taking away the point of doing this!

The final version of my script aimed to only show the difference (avoid noise), ideally show which direction the change occurred using Compare-Object in PowerShell.

The Result

  • Do a recursive loop through the directory structure
  • Output Folder names as [Folder], and recursively dive-down. This is a bit dirty as I didn’t want the full path (harder to compare) but wanted to differentiate when I dug down. YMMV.
  • Output File names, excluding some files I didn’t care about (Like .tmp & .XML files)
  • Do this for folder A and folder B, storing the result to a variable
  • Using Compare-Object on these variables and outputting the result.
function GetFiles($path, [string[]]$excludedFiles)
    foreach ($item in Get-ChildItem $path)
        if ($excludedFiles | Where {$item -like $_}) { continue }

        if( (Get-Item $item.FullName) -is [System.IO.DirectoryInfo]){
        if (Test-Path $item.FullName -PathType Container)
            GetFiles $item.FullName $excludedFiles
$env1 = GetFiles -path "C:\folderA\" -excludedFiles "*.xml",".tmp"
$env2 = GetFiles -path "C:\folderB\"  -excludedFiles "*.xml",".tmp"

Compare-Object -DifferenceObject $env1 -ReferenceObject $env2

Which provides output like:

This could definitely be optimized and cleaned up for sure, and YMMV massively.

Overall, a few minutes in PowerShell and I managed to save substantial time – and that was my only real goal!

My attempt at using SonarQube for static code analysis

This post covers my attempts to use SonarQube as a stand-alone install to perform static code analysis on a regular basis. This will cover purely getting the tool working, Maybe I will pick up how I can use the data in a later post?

I will be doing this in a very narrow focus which is for the project I am currently working on which is .NET stack, with builds running in VSTS using MSBUILD.

SonarQube runs code analysis as solutions are being built and provides a web dashboard of code smells, security vulnerabilities, duplication and more. My aim is to use it to identify technical debt, as well as track debt is reducing over time.

Note you can hook this into Azure Dev-ops fairly easily too with a few clicks and less setup, but I wanted to host the tool on our own infrastructure for zero-cost. I also believe you can use their cloud version free if you are open source.

My aims are:

  • To get the self-hosted version of SQ installed/setup
  • Get it running against a local solution
  • Work out how to hook this into our VSTS build process (If possible)

Getting started

First of all, I downloaded and extracted the free self-hosted version of SQ (Community edition) and placed it on one of our build servers. This package is essentially a self-hosting application, and following the 2-min getting started guide here , it’s genuinely quite easy to get the dashboard running within that 2 minutes (Providing the system requirements are met – which looks like you just need a recent Java JRE/JDK installed)

Following the above guide, and launching the shell/batch script of your choice, you can then navigate to http://localhost:9000 and see the SonarQube dashboard asking you to create a new project.

When creating a new project you are prompted for a project key and display name. The key will be used for the integration, and the display name will be the name displayed on the dashboard.

Next up is the token. The token is used for authentication purposes when uploading analysis files and can be changed and revoked later. I just used the word “sausages” as an example, but when you click “generate” it will provide your token.

Next it will tell you how to configure your project for SQ. I am doing this against a .NET project (C#, JS, etc) so will continue with this example.

For a C# project which will be built using MSBUILD, you first need the “SonarScanner for MSBUILD“. The SonarScanner is the tool that performs the analysis by starting before MSBUILD kicks in, and then ending, and collating the results to send to the server when it ends.

This tool can be placed anywhere, but the folder will need adding to the PATH on your windows environment.

At this point, we can do some powershell to test that it’s all hooked up correctly! (You can use pre/post build events too, but this is much simpler for testing) In this example we will CD into the directory the SLN is in, start the scanner tool using our project name and key from earlier and build our solution file (in rebuild). Finally we will end the scanning.

cd 'C:\path to your SLN'
SonarScanner.MSBuild.exe begin /k:"My-Project" /"http://localhost:9000" /d:sonar.login="Your Key"
msbuild MySolution.sln /t:Rebuild
SonarScanner.MSBuild.exe end /d:sonar.login="Your Key"

Note that this assumes you have the scanner and MSBUILD in your PATH variable. If you do not, you can simply call the exe directly. Note that the MSBUILD exe is located at C:\Program Files (x86)\Microsoft Visual Studio\<version>\<edition>\MSBuild\<version>\Bin\msbuild.exe. I believe that SQ requires MSBUILD of 12 and above – I am currently using 15.0.

If we run this, it will take a few moments to start and a variable amount of time to complete (Depends heavily on the size of your solution). At the very end you will see a line of “Execution Success” and if you still have your dash open you may have seen it update.

If you navigate back to http://localhost:9000 you should now see your project. Note that if your sln was particularly large you may just see a “background processing” message whilst it imports the analysis file.

This is good news! However, there is a bit of an ominous warning in the footer of the dashboard which reads

Embedded database should be used for evaluation purposes only. The embedded database will not scale, it will not support upgrading to newer versions of SonarQube, and there is no support for migrating your data out of it into a different database engine.

This is easily solved by simply having a backing database for SQ. You will however lose all your progress so far.

Moving beyond proof of concept.

So we can use this in our build/deploy pipeline, I want to have it hook into a database, and installed onto one of our servers and finally have one of our CI builds run it!

The process is the same as above in terms of placing the extracted files onto the server apart from we also have to punch a hole in Windows Firewall for TCP port 9000 so it can be accessed remotely. Running the start batch script now will bring you to the same dashboard and warning as before which is what we want to avoid, so we will need to hook up a database.

For the database I will be using MS SQL hosted on 2016. You can find documentation for other database types (MySql, Oracle etc.) on the SQ documentation.

First I created an empty database on the SQL 2016 server named “SonarQube”, and also a new SQL user named “Sonar” who is a dbo on the SonarQube database.

Back in the SQ install, in the \sonarqube\conf folder is a file. In here we need to add the following:


Note: If, like me, your SQL instance is named like “server\instancename” you will need to escape the slash before the instance so it is like “server\\instancename”. The error that is generated does not lead to this being the cause of SQ not launching which was a pain!

Next up, I wanted to have my VSTS build automatically run the analysis and process it. I wanted this to be part of the departments CI builds, but due to the size of the project it was taking upwards of 30 minutes to complete so we moved it to the nightly builds.

Now you should be able to launch SQ again and not see the banner in the footer. A new project should be created like before.

As we use TFS/VSTS in house, this guide will show working with self-hosted TFS. There is a lot more in-depth (and more useful!) guides on the SQ docs here. The steps to take before progressing are:

  • Download the SQ VSTS extension appropriate for your version of TFS/VSTS
  • Install extension to TFS
  • Add the extension to the collection.

Once the extension is installed, you should see 2 new build steps of:

  • SonarQube Scanner for MSBUILD – Begin Analysis
  • SonarQube Scanner for MSBUILD – End Analysis

Before you can use these, you will need to configure the SQ endpoint. To do this, you can either goto the collections administration page, then the “service” tab, or add the “SonarQube Scanner For MSBUILD – Begin analysis” build step to your VSTS build and then click “Manage” the server panel. In here, you can click “New Service Endpoint” and then “SonarQube”

When you add a new SQ endpoint, you configure it using a friendly name, the URL of the dashboard, and the token you set up for your project/user (much like the earlier powershell script had)

Now if you go back to edit your build definition. You will be able to use Sonar Qube with the Begin and End tasks either side of your build actions (again, much like the earlier powershell)

To set it up, select the SQ end point as configured earlier, and then under Project Settings, set the Project Key and Project name appropriately (based on however the project in SQ was set up) and finally, under “Advanced”, check “Include full analysis report in the build summary”. There is no configuration required for the end analysis build step.

Running it now will most likely result in a failure unless the machine which is hosting the build agent has the correct software installed.

Much like we did for the local version, we need to download and setup the SonarScanner and add it to the build path. The server which hosts the agents will also need to be running MSBUILD v14 or v15 (at time of writing). You can get a standalone version of MSBUILD v15 direct from Microsofts download pages. It took me a while to find it, so this is the direct link to v14. (Which I had to use due to a separate issue) . Hopefully this is the correct one for 14.0.25420.1

Now if you run your build it should (hopefully) produce some output. I did run into a few errors in this stage which may be just me being a bit uninformed (and not really reading docs…)

Some of the “Gotcha’s” I ran into

  • As noted earlier, MS SQL server names which contain slashed must be escaped, and the error that it thrown does not indicate this is the case!
  • Weird, ambiguous messages. (An instance of analyzer SonarAnalyzer.Rules.CSharp.ThreadStaticWithInitializer cannot be created from SonarAnalyzer.CSharp.dll ) which actually had nothing to do with that, and was actually related to MSBUILD versions. I was pulling my hair out until I saw this post which states you need MSBUILD v14+ from update 3 (14.0.25420.1) where I was using MSBUILD v14 but 14.0.23107.10).
  • MSBUILD Version issues. The build would work but the SQ analysis would fail with an error about supported MSBUILD versions. To get around this I made sure that the MSBUILD step in the VSTS build was using an argument of /tv:14.0 to ensure it would use a specific version.
  • Static code analysis could not be completed on CSS files due to Node JS version (ERROR: Only Node.js v6 or later is supported, got <ver>. No CSS files will be analyzed.). Simply install latest nodeJS to the server hosting the agents.
  • Timeouts! This was only an issue for me as the projects LOC is in the hundreds of thousands so it generates a large log. (##[error]The analysis did not complete in the allotted time of 300 seconds. Consider setting the build variable SonarQubeAnalysisTimeoutInSeconds to a higher value.) To get around this, you add a build variable to the VSTS build named “SonarQubeAnalysisTimeoutInSeconds”. I tried setting it to zero (which is usually ‘infinite’) but then I got “##[error]The analysis did not complete in the allotted time of 0 seconds“. I couldn’t find any reliable info about max values so set mine to 20 minutes to be safe.


Finally, (for me) after some light tinkering it all worked. When I was scanning google/stackoverflow it seems a few people had the same issues as me, so I don’t feel too bad about it for a first try!

Our nightly builds now pump out some lovely code analysis to the dashboard which I have shared with the team and the results are feeding into our technical backlog to be resolved. I’m going to leave it running for at least a month, and see if we get any real usage out of it.

More on results later, perhaps?

This wasn’t really written as a complete step-by-step, more of a stream of consciousness as I tried to learn about something new, but hopefully it helps someone, even if that is future me when I come back to re-remember how it works.

Update: I realised this morning that the project version on the SonarQube dash never increased or changed which meant it was hard to check when issues were implemented without cross-referencing the date and the checkin history. If you set the “Project Version” on the Begin Analysis step the in-built variable of $(Build.SourceVersion) you will get the changeset the build was ran off as the version.