Software developmentVisual Studio

Making mistakes: Part 2

Delete all your binaries before a release build.

An incredibly easy way to mess up your project is forgetting to delete all your own binaries before building and testing a release candidate. It’s so easy to miss that most companies run dedicated build and test systems that enforce a clean environment before any meaningful action is taken. Not all developers can enjoy that kind of safety at work, and even less of us at home. But first, let’s go ahead and dig a little deeper into how Visual Studio treats assembly references, and why this is so important.

There are three kinds of assembly references that I am aware of and each one has its specific behaviour. There is the project reference that we get a lot in every solution and that targets a project file. The next common reference type is the one that includes an assembly by its path. A few years ago, the package reference was introduced, which targets NuGet packages by name and version.

<Project Sdk="Microsoft.NET.Sdk">

  ...

  <ItemGroup>
    <ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Reference Include="ClassLibrary2">
      <HintPath>..\some\third\party\path\ClassLibrary2.dll</HintPath>
    </Reference>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="ClassLibrary3" Version="1.2.3" />
  </ItemGroup>

</Project>

They all share one common problem, which is introduced by Visual Studio and the way it handles references. When Visual Studio compiles a project, it will copy all required reference assemblies into the output directory. On the other hand, when Visual Studio cleans a project, it will delete the project output (e.g. assembly, documentation, debug symbols, resources) and all assemblies that are listed as reference in the project file. It isn’t immediately obvious, but there are a million things that could happen between compiling and cleaning, and some of them could break the cleaning process.

What exactly is the problem?

A common action when working on software is refactoring. It’s also quite common that, during a big refactoring process, a single project is split into two or more projects. This means adding new references to and removing old references from one or more projects. When a reference is removed after the project was compiled but before it is cleaned, that’s when trouble finds us. A library that was referenced previously will still be present in the output directory as a dead copy. There is nothing that takes care of a dead library copy, so it is neither deleted on cleaning nor updated by recompiling. And since today’s coding practices progressively shift towards the usage of dependency injection and dependency inversion principles, this issue is becoming more and more of a problem. Especially plugin type projects can suffer from this when libraries are resolved at runtime to inject concrete implementations that weren’t present at compile time.

In this scenario, a library that used to be referenced directly at compile time, is now being loaded as a plugin at runtime. But an older version of the library is still present in the bin directory, which isn’t being cleaned up by the IDE. So, when the program executes and the runtime starts to look for the plugin assembly, it will first look for it next to the currently running executable. There it will find the older version and load that instead, since the requested assembly matches the located assembly almost perfectly. As a result, the program works correctly on the local machine and all tests are green. Including the new plugin into the deployment mechanism is easily missed now, which will eventually break the program for everyone else, as the library can’t be found anywhere.

I’ve had this happen to me twice so far, once at work and once on a private project. At work, the error was mitigated before release through the use of a proper development process that includes gated check-ins and a clean build environment for the build pipelines. The private project was another story, however. Without the luxury of continuous integration at home, I accidentally released a version that was pretty much unusable. With only so much spare time available to me at the time, getting around the issue took more than half a year. Thankfully, no one else is using my software, so the damage wasn’t that big.

Mending the problem

Luckily, there is an easy way to get around this issue. While a build server mitigates the problem of dead copies by always running in a well-defined and clean state, a build environment on a developer machine requires manual removal of old binaries. By default, all the output is compiled into a directory within each project, but I’d rather have just one common directory for all the binaries of a solution. Also, I don’t want to have to edit the output directory of every project manually, as configuration should be done in one place only. I use targets files as solution items, that are included in every project by a single line of XML code: <Import Project="..\Configuration.targets" />

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- Common -->
  <PropertyGroup>
    <OutputPath>..\..\bin\$(RootNamespace)\$(Platform)\$(Configuration)\</OutputPath>
  </PropertyGroup>

</Project>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- Common -->
  <PropertyGroup>
    <OutputPath>..\..\bin\$(RootNamespace)\$(Platform)\$(Configuration)\</OutputPath>
  </PropertyGroup>

</Project>

This results in an output directory on the same level as the solution directory. Wiping all the binaries in a solution now requires just a single action, no matter how many projects there are. When a release candidate is nearing completion, I simply delete all the binaries and rebuild everything from source. All the tests are then executed using the latest code and without the risk of having any unwanted artefacts that aren’t part of the release. The targets file is also a great place to handle any other solution wide configuration, but that is a topic for another article. To increase ease-of-use, I’ve created a PowerShell script that does the job nicely as an external tool in Visual Studio.

param (
    [string]$target = $null
)

if(!$target) {

Write-Host @"
Usage as external tool in Visual Studio

Title: Remove Bin
Command: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Arguments: -ExecutionPolicy RemoteSigned -File "D:\Dev\GitHub\Misc\Scripts\RemoveDir.ps1" "..\bin"
Initial directory: `$(SolutionDir)
Use Output window: True
Treat output as Unicode: False
Prompt for arguments: False
Close on exit: True
"@

exit

}

# setup
$CurrentDir = Get-Item $pwd
$TargetDir = [System.IO.DirectoryInfo] [System.IO.Path]::Combine($CurrentDir.FullName, $target)

# remove
"Removing: $TargetDir"
Remove-Item -Path $TargetDir.FullName -Force -Recurse

Leave a Reply

Your email address will not be published.