An acquaintance of a friend tweeted about a problem he's having with MSBuild: it often fails to resolve nth-tier binary dependencies of a project:

The application references a binary directly, which makes the class library a first-tier dependency of the application.  The class library references another binary assembly, which becomes a second-tier dependency of the application.  

Here's a Visual Studio solution with the same dependency configuration: (31.73 kb)

Build the application (use the Debug configuration) and check out the application's output path.  You would expect to find application.exe, classlibrary.dll, and binaryassembly.dll, but binaryassembly.dll is missing.  However, if you scrutinize the MSBuild output during the application.csproj build (use the /v:d switch), you can see that it's trying like mad to find binaryassembly.dll:

Project "D:\Project\Dump\msbuldreferencedemo\application\application.csproj" on node 0 (Rebuild target(s)).
Dependency "BinaryAssembly, Version=, Culture=neutral, PublicKeyToken=null".
  Could not resolve this reference. Could not locate the assembly "BinaryAssembly, Version=, Culture=neutral, PublicKeyToken=null". Check to make sure the assembly exists on disk. If this reference is required by your code, you may get compilation errors.
  For SearchPath "D:\Project\Dump\msbuldreferencedemo\classlibrary\bin\Debug".
  Considered "D:\Project\Dump\msbuldreferencedemo\classlibrary\bin\Debug\BinaryAssembly.exe", but it didn't exist.
  Considered "D:\Project\Dump\msbuldreferencedemo\classlibrary\bin\Debug\BinaryAssembly.dll", but it didn't exist.
  For SearchPath "C:\Project\Dump\msbuldreferencedemo\BinaryAssembly\bin\Debug\".
  Considered "C:\Project\Dump\msbuldreferencedemo\BinaryAssembly\bin\Debug\BinaryAssembly.exe", but it didn't exist.
  Considered "C:\Program Files\Microsoft SQL Server\90\DTS\ForEachEnumerators\BinaryAssembly.exe", but it didn't exist.
  Considered "C:\Program Files\Microsoft SQL Server\90\DTS\ForEachEnumerators\BinaryAssembly.dll", but it didn't exist.
  For SearchPath "{GAC}".
  Considered "BinaryAssembly, Version=, Culture=neutral, PublicKeyToken=null", which was not found in the GAC.
  For SearchPath "bin\Debug\".
  Considered "bin\Debug\BinaryAssembly.exe", but it didn't exist.
  Considered "bin\Debug\BinaryAssembly.dll", but it didn't exist.
  Required by "D:\Project\Dump\msbuldreferencedemo\classlibrary\bin\Debug\ClassLibrary.dll".

MSBuild doesn't know where to look for the file.  My team hit this problem after we started replacing projects in our source hive with prebuilt binaries; the builds would succeed, but the applications failed to run because none of the second-tier dependencies of a first-tier binary reference would show up in the output folder.  I did some digging and found one simple way and one complex way to resolve the issue.

Easy: Put All Binaries in One Place

The easiest way to address this problem is to place all binary dependencies into a single folder.  If you do reference a binary from this folder, and the folder contains the binary's dependencies, those second-tier dependencies will be found during the build.  In the project file, the first-tier binary includes a hint path so the compiler knows where to look for the file.  For example, open application.csproj and change the hint path for the classlibrary reference to point to the \lib folder (line 5):  

  <Reference Include="ClassLibrary, Version=, Culture=neutral, processorArchitecture=MSIL">
  <Reference Include="System" />

Rebuild the project and you'll find both classlibrary.dll and binaryassembly.dll in the output folder.  This hint path is used as one of the search paths when MSBuild attempts to resolve a dependency during the build, and since the hint path also contains binaryassembly.dll, MSBuild is able to find that second-tier dependency as well.

Hard: Modify the Assembly Search Path List

This is the solution we had to use on my team.  The choice was made to keep each binary in a unique folder, so interdependencies between the binaries could not be managed with just the hint paths. 

The magic starts in Microsoft.Common.targets, which you will find under %FRAMEWORKDIR%\v2.0.50727\.  In that file there is a series of targets aimed at resolving different kinds of references in your project, from project references to COM references to assembly references.  The target that holds the key is ResolveAssemblyReferences, which is nothing more than a wrapper around the ResolveAssemblyReference task.  This task accepts a parameter named SearchPaths that determines where looks for assemblies.  The value of this parameter is determined by the AssemblySearchPaths property, which is created earlier in Microsoft.Common.targets:

The SearchPaths property is set to find assemblies in the following order:

    (1) Files from current project - indicated by {CandidateAssemblyFiles}
    (2) $(ReferencePath) - the reference path property, which comes from the .USER file.
    (3) The hintpath from the referenced item itself, indicated by {HintPathFromItem}.
    (4) The directory of MSBuild's "target" runtime from GetFrameworkPath.
        The "target" runtime folder is the folder of the runtime that MSBuild is a part of.
    (5) Registered assembly folders, indicated by {Registry:*,*,*}
    (6) Legacy registered assembly folders, indicated by {AssemblyFolders}
    (7) Look in the application's output folder (like bin\debug)
    (8) Resolve to the GAC.
    (9) Treat the reference's Include as if it were a real file name.
  <AssemblySearchPaths Condition=" '$(AssemblySearchPaths)' == '' ">

The value of this property is a semicolon-delimited list of paths and search targets.  This list can be easily altered by redefining the property.  If we know the path where the binary reference can be found, we can add it explicitly to the search path by overriding the BeforeResolveReferences target.  Add this target to the end of application.csproj, adjusting the path in line 3 to your local machine (be sure to revert it if you tested the simple solution above):

<Target Name="BeforeResolveReferences">
    <Output TaskParameter="Value"
        PropertyName="AssemblySearchPaths" />

Rebuild application.csproj and you'll find both classlibrary.dll and binaryassembly.dll in the output folder.

Of course, hard-coding paths into the build is a Bad Thing, so choose a more general solution if you have to tread this path too.  I ended up using this little diddy of a target to determine the directories where all assemblies are located, and then prepending that list to the AssemblySearchPaths property:

  <BRSearchPathFiles Include="$(SolutionDir)..\**\*.dll" />

<Target Name="BeforeResolveReferences">
  <RemoveDuplicates Inputs="@(BRSearchPathFiles->'%(RootDir)%(Directory)')">    
    <Output TaskParameter="Filtered" ItemName="BRSearchPath" />

  <CreateProperty Value="@(BRSearchPath);$(AssemblySearchPaths)">
    <Output TaskParameter="Value"
        PropertyName="AssemblySearchPaths" />