I work as C# programmer and I sometimes need to deal with assemblies created by my colleagues. They are a separate team and they provide us with some assemblies that act as a middle layer between us and a windows service.

They don't sign their assemblies (but they will eventually, one day), so their assemblies are not so vulnerable to problems with assembly versions. That means that at runtime if assembly A references assembly B version 1.0.0.1 and there is only assembly B version 1.0.0.2, that doesn't cause them any problem.

However, we need to sign their assemblies via Signer. That is still no problem. But when the application is starting and assemblies are being loaded then when the assembly A tries to load assembly B version 1.0.0.1 and there is only version 1.0.0.2 an exception is thrown. The versions simply have to match.

This problems are caused by the fact that they compile their projects in an incorrect (incorrect for us ;)) order and some of their projects reference dlls in a common bin instead of related project.
The rest of the post is about how to go through the csproj files and explore the right build order so that the projects can be built with respect to their project and assembly dependencies (= references to common bin folder).

If you are only interested in a code and know where the problem is, just go to the code or download.

What causes multiple version dependencies

Look at the picture:

dependencies of csprojs

The build steps are named A, B, C, D.
Assemblies (projects) are named 1, 2, 3.
To simplify things assume that all the projects reference only assemblies from bin folder. Assembly 1 is independent, assembly 2 and 3 depend on 1 (through assembly reference). So, the steps in detail are:

  1. Assembly 1 is built and copied (in postbuild event) to bin. Its version is 1.0.0.1
  2. Assembly 2 is built. It creates a dependency on 1:1.0.0.1 and is copied to bin
  3. Assembly 1 is built and copied (in postbuild event) to bin. Its version is 1.0.0.2
  4. Assembly 3 is built. It creates a dependency on 1:1.0.0.2 and is copied to bin

If there is any other project that references 1,2,3 and you have to sign the assemblies, then you have a problem.

If you think about "why don't you avoid referencing the assemblies in bin folder?" then my answer is that in real life scenarie when there are many projects, then it's much more convenient to have multiple solutions with only subset of projects.
So Solution X might contain projects 1 and 2 and Solution Y projects 1 and 3.

How to find the dependencies

We know there are two types of dependencies:

  • Dependency on project. This is identified by ProjectReference element in csproj file.
    <ItemGroup>
     <ProjectReference Include="..\..\MyProj.csproj">
      <Project>{3B347485-93A1-436F-96DF-7C382A9E7304}</Project>
      <Name>MyProj</Name>
     </ProjectReference>
     ...
    </ItemGroup>
  • Dependency on assembly (dll). The element name is simly Reference. (our enemy)
    <ItemGroup>
     <Reference Include="Castle.Core, Version=1.0.3.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, ...">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\..\..\..\Lib\NHibernate\Castle.Core.dll</HintPath>
     </Reference>
     ...
    </ItemGroup>

So the solution here is just 'convert' assembly dependencies to project dependencies.

  1. We will look at Reference/HintPath in the csproj file
  2. extract the assembly name (it is possible from Reference/@Include as well)
  3. find a csproj file B in our directory that produces assembly with name from (2)
  4. and note down that the csproj from (1) is dependent on B project (from (3))

Code, finally

[CmdletBinding()]
param(
    [string]$dir='c:\dev\trunk\',
    [string]$skip #regex that filters some projects out
)

<# removes relative parts:  a\b\c\..\..\1\2 -> \a\1\2
   if you know a better approch, let me know ;) http://twitter.com/stejcz
#>
function RemoveRelativePaths($str) {
    $counter = 0
    do {
        if (++$counter -gt 20) { throw 'possibly unknown pattern: '+$str }
        $len = $str.Length
        $str = $str -replace '\\([\w\d]+\.)*[\w\d]+\\\.\.',''
    }while($str.Length -ne $len)
    $str
}

The function RemoveRelativePaths is just a helper to shorten paths like 'a\b\c\..\..\1\2' to '\a\1\2'. As I'm looking at it now, the regex might be simpler I think, like '\\[\w\d]+\\\.\.' but I'll leave it as it is.

In part #1 we get all the csproj files in specified directory and parse them – we read project referencies (XPath '//e:ProjectReference'), assembly name (XPath '//e:AssemblyName') to convert assembly dependency to project dependency, and parse assembly references (XPath '//e:Reference').

#part #1
Write-Host "Reading csprojs.."
$binReference = @{}
$skipProjs = @{}
$dependencies = Get-ChildItem $dir *.csproj -Recurse | % { 
    $content = [xml](gc $_.FullName)
    if (!($content.Project)) {
        Write-Warning "Project $($_.FullName) skipped. Does not contain root tag with name Project"
        return
    }
    
    Write-Debug "Reading $($_.FullName)"
    $ret = '' | select FullName, References, AssemblyName, Skipped
    $ret.FullName = $_.fullname    
    $ns = @{'e'="http://schemas.microsoft.com/developer/msbuild/2003" }
    $ret.References = @(
        @(Select-Xml -Xml $content -XPath '//e:ProjectReference' -Namespace $ns) |
        select -ExpandProperty Node | 
        select -ExpandProperty Include | 
        % { RemoveRelativePaths (Join-Path (Split-Path $ret.FullName -Parent) $_ ) })
    $ret.AssemblyName = Select-Xml -Xml $content -XPath '//e:AssemblyName' -Namespace $ns |
        select -ExpandProperty Node -First 1 | 
        select -Expand    Property '#text'
        
    $skipProjs[$ret.FullName] = $skip -and $ret.FullName -match $skip
    
    # processing references to bin
    @(Select-Xml -Xml $content -XPath '//e:Reference' -Namespace $ns) |
        select -ExpandProperty Node | 
        ? { $_.HintPath} |
        select -ExpandProperty HintPath | 
        % {
            $assemblyName = [IO.Path]::GetFileNameWithoutExtension($_) # napr. Gmc.System
            if (!$binReference.ContainsKey($assemblyName)) { 
                #Write-Host "New bin reference for $assemblyName"
                $binReference[$assemblyName] = @()
            }
            $binReference[$assemblyName] += $ret
        }
    
    $ret
    
    Write-Debug "Count of referencies: $($ret.References.Count)"
    if ($skipProjs[$ret.FullName]) { Write-Verbose "Skipped $($ret.Fullname)" }
}

Now we create the project dependencies from assembly dependencies. If you reference some 3rd party assemblies and there is no source csproj for them, they will just be written to the console saying something like 'there is no related project'.

#part #2
#resolve dependencies by assembly
$binReference.Keys | % {
    $assemblyName = $_
    $assemblyCsproj = $dependencies | ? { $_.AssemblyName -eq $assemblyName }
    if (!$assemblyCsproj) { 
        return $assemblyName
    } else {
        $binReference[$assemblyName] | % { $_.References += $assemblyCsproj.FullName }
    }
} | sort | % `
    -Begin { Write-Verbose "These assemblies are referenced but don't have related projects" } `
    -Process { Write-Verbose " $_" }

If you specified something to $skip parameter, then right the projects will be removed from list.

#part #3
<#
 there is an array in $dependencies, e.g.
FullName                         References                               AssemblyName
----                             ----------                               ----------
C:\dev\trunk\Base.csproj         {}                                       Base
C:\dev\trunk\Proj1.csproj        {C:\dev\trunk\Base.csproj}               Proj1
C:\dev\trunk\Proj2.csproj        {C:\dev\trunk\Base.csproj}               Proj2
C:\dev\trunk\ProjXYZ.csproj      {C:\dev\trunk\Base.csproj, C:\dev\t...}  ProjXYZ
#>

# removes all the projects that should be skipped; they are removed from references as well
# this could be done in csproj reading phase as well
Write-Host "Removing skipped projects.."
Write-Debug "Count before: $($dependencies.Count)"
$dependencies = $dependencies | 
    ? { !$skipProjs[$_.FullName] }  |
    % { $_.References = @($_.References | ? { !$skipProjs[$_]});
            $_ }
Write-Debug "Count after: $($dependencies.Count)"

Last part. Now we have all the data available so we can go through them and order the projects by referencies. The first set of projects will be the ones that don't depend on any other projects. The second set will be projects that depend only on first set and so on.
The output of the script is array of these sets.

#part #4
$steps, $stepNum = @(), 0
while($dependencies.Count -gt 0) {
    $indep = @($dependencies | ? { $_.References.Count -eq 0} | select -ExpandProperty FullName)
    if ($indep.Count -eq 0) {
        $global:RemainingDependencies = $dependencies
        throw "There is no independent project. Check `$global:RemainingDependencies variable `
        for remaining csproj dependencies"
    }
    Write-Verbose ("Count of independent projs: $($indep.Count), " +
        "step: $stepNum, dependencies count: $($dependencies.count)")
    $toAdd = ''|select Order,Projects
    $toAdd.Order,$toAdd.Projects = $stepNum++, @($indep)
    $steps += $toAdd 
    
    # Remove projects that are independent on any other project
    $dependencies = @($dependencies | ? { $indep -notcontains $_.FullName })
    
    # Remove the independent projects from referencies of other projects
    $dependencies | % { 
        Write-Debug "Checking project $($_.FullName), referencies count: $($_.References.Count)"
        $_.References = @($_.References | ? { $indep -notcontains $_ })
        Write-Debug "  new count of referencies: $($_.References.Count)"
    }
}
$steps

Example

I created a very tiny project to test it (you can download it). The output looks like this:

[1]: $dep =.\resolve-CsprojDependencies.ps1 -dir m:\temp\blog\117TestProj -verbose
Reading csprojs..
VERBOSE: These assemblies are referenced but don't have related projects
VERBOSE:  log4net
Removing skipped projects..
VERBOSE: Count of independent projs: 1, step: 0, dependencies count: 4
VERBOSE: Count of independent projs: 2, step: 1, dependencies count: 3
VERBOSE: Count of independent projs: 1, step: 2, dependencies count: 1
[2]: $dep | fl

Order    : 0
Projects : {M:\temp\blog\117TestProj\Base1\Base-1.csproj}

Order    : 1
Projects : {M:\temp\blog\117TestProj\Dep-2-1bin\Dep-2-1bin.csproj, 
            M:\temp\blog\117TestProj\Dep-3-1bin\Dep-3-1bin.csproj}

Order    : 2
Projects : {M:\temp\blog\117TestProj\DepAll\DepAll.csproj}

After you build the projects in this order, you can check that all the assemblies are dependent only on one version of Base-1.dll.

Download

Meta: 2010-01-28, Pepa

Sometimes when you work with .NET assemblies, you can see a message telling you something about dependencies and referencies. Something like this:

Error when binding assemblies

It probably means that some assemblies reference an assembly that is not contained in the /bin folder. How can it happen? Have project A, B, C. A is basic project, B depends on A, C depends on A,B. Compile A, then compile B. Recompile A and compile C. C will reference fresh assemblies from A, but it will reference old assemblies from A through the B project as well. You can either go for Reflector and check manually the references or take PowerShell and do it in a much funier way.

The PowerShell approach

I won't waste your time, here is the script:

$references=Get-ChildItem c:\temp\bin -rec | % {
    $loaded  = [reflection.assembly]::LoadFile($_.FullName)
    $name    =$loaded.ManifestModule
    $loaded.GetReferencedAssemblies() | % {
        $toAdd='' | select Who,FullName,Name,Version
        $toAdd.Who,$toAdd.FullName,$toAdd.Name,$toAdd.Version = `
            $loaded,$_.FullName,$_.Name,$_.Version
        $toAdd
    }
}

$references | 
    Group-Object FullName,Version | 
    Select-Object -expand Name | 
    Sort-Object

Just change the directory name with the assemblies and run it. After I did it, I saw this output:

antlr.runtime, Version=2.7.6.2, Culture=neutral, P...
Common.Logging, Version=1.2.0.0, Culture=neutral, ...
...
Gmc.System, Version=0.2.3491.12201, Culture=neutra...
Gmc.System, Version=0.2.3491.12554, Culture=neutra...
...
System.Xml, Version=1.0.3300.0, Culture=neutral, P...
System.Xml, Version=1.0.5000.0, Culture=neutral, P...
System.Xml, Version=2.0.0.0, Culture=neutral, Publ...
System.Xml.Linq, Version=3.5.0.0, Culture=neutral,...

The key problem are the two rows about Gmc.System – two different versions are used. Now, just go and look for the referencing assemblies (some names were 'obfuscated'):

[3] $references | Where-Object { $_.Name -eq 'Gmc.System' } | Select-Object Who,Version

Who                                                Version
---                                                -------
Gmc.Dam.D.Api, Version=1.0.0.0, Culture=neutral,.. 0.2.3491.12554
Gmc.Dam.D.Fcd, Version=1.0.0.0, Culture=neutral... 0.2.3491.12554
Gmc.Dam.Z, Version=1.0.0.0, Culture=neutral, Pu... 0.2.3491.12554
Gmc.Dam.Web, Version=1.0.0.0, Culture=neutral, ... 0.2.3491.12554
Gmc.Dam.Web, Version=1.0.0.0, Culture=neutral, ... 0.2.3491.12201
Gmc.Ginger.A, Version=0.1.3491.12219, Culture=n... 0.2.3491.12201
Gmc.Ginger.D.Api, Version=0.1.3491.12222, Cultu... 0.2.3491.12201
Gmc.Ginger.D.Facade, Version=0.1.3491.12223, Cu... 0.2.3491.12201
Gmc.Ginger.Z, Version=0.1.3491.12217, Culture=n... 0.2.3491.12201
Gmc.Ginger.Web.Mvc, Version=0.1.3491.12232, Cul... 0.2.3491.12201

It's quite obvious that assemblies beginning with Gmc.Ginger use the old version. Problem solved.

Meta: 2009-07-23, Pepa