Daniel Fisher (lennybacon.com)

SOA, DATA & THE WEB

Could not load file or assembly exception and how to fix your builds when using nuget packages

Once in a while it happens that an exception is thrown like the following:

Could not load file or assembly 'devcoach.Core, Version=1.0.11308.1, Culture=neutral, PublicKeyToken=0313e76cb5077f22' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)

What did cause this exception to be fired?

Lets have a look at the following picture which illustrates the scenario.

image

The build order is defined by the dependencies. As we have separate modular component oriented development each project resides it its own repository and has its own nuget package.

How is Visual Studio handling references?

image

If we do not reference from GAC or another Project the target is always (if the assembly is strong names – and yes it should!) the Assembly.FullName as seen here:

<?xml version="1.0" encoding="utf-8"?>
<Project
ToolsVersion="4.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <ItemGroup>
      <Reference
Include="devcoach.Core, Version=1.0.11308.1, …">
      <HintPath>..\packages\…\devcoach.Core.dll</HintPath>
  </Reference>

We change the “Version Specific” setting in Visual Studio’s property windows manually.

image

And manipulate the result to be:

<?xml version="1.0" encoding="utf-8"?>
<Project
ToolsVersion="4.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <ItemGroup>
      <Reference Include="devcoach.Core, Version=1.0.11308.1,…">
      <VersionSpecific>False</VersionSpecific>
<HintPath>..\packages\…\devcoach.Core.dll</HintPath>
  </Reference>

Why does this matter?

1) Sometimes during development – especially when hunting a bug – it happens that I create a solution that crosses repository frontiers. This requires to switch from a nuget reference to a project reference. After work is done it is important that the project reference is rolled back to a nuget package reference. I created a small helper for that job called Reference Switcher. The rolled back reference now points to a newer version as functionality was added or a bug was removed.

2) Almost every time before I start working on a project I update the nuget package references. The updated reference may points to a newer version if functionality was added or a bug was removed.

3) In a web application project while updating a package visual studio realizes that the file is not there or not accessable (while beeing written) and falls back to the assembly that was copied to the bin directory during the last build – the prior version of course.

So each and every time a new version is introduced (while Visual Studio is running) it means to be clever and behaves as described. When the solution is commited to the source control server, pulled by the build server, packages get updated, build fails – NO nuget package is published. we have the issue in place.

A build task to the rescue

I wrote a msbuild task to solve the problems by modifying the project file. It switches references with version information to the form:

<?xml version="1.0" encoding="utf-8"?>
<Project
ToolsVersion="4.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <ItemGroup>
      <Reference Include="devcoach.Core">
      <HintPath>..\packages\…\devcoach.Core.dll</HintPath>
  </Reference>

Additionally, if the HintPath’s innerText points to an assembly placed inside the bin directory it tries to find the corresponding package and references the assembly from there – of course without version information.

Usually I prefer XmlReader and Writer but as we are in a non-performance-critical area I just dumped the memory-eating XmlDocument in.

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace devcoach.Tools.BuildTasks
{
	public class ModifyProjectFile : Task
	{
		[Required]
		public string[] ProjectFile { get; set; }

		public bool SetVersionSpecificationToFalse { get; set; }
		public bool PreferPackageReferences { get; set; }

		public override bool Execute()
		{
			var listener = new MsBuildLogTraceListener(Log);
			Trace.Listeners.Add(listener);

			try
			{
				foreach (var projectFile in ProjectFile)
				{
					var projectFilePath = Path.GetFullPath(projectFile);
					Trace.TraceInformation(
						string.Concat(
							"Processing file '",
							projectFilePath,
							"'..."));
					if (SetVersionSpecificationToFalse)
					{
						Trace.TraceInformation(
							"Set version specification to false.");
						RemoveVersionSpecification(projectFilePath);
					}
					if (PreferPackageReferences)
					{
						Trace.TraceInformation(
							"Prefer package references.");
						ReplaceBinReferences(projectFilePath);
					}
				}

				return true;
			}
			catch (Exception ex)
			{
				Log.LogErrorFromException(ex);
				return false;
			}
			finally
			{
				Trace.Listeners.Remove(listener);

			}
		}

		private void ReplaceBinReferences(string projectFile)
		{
			var projectDirectory = Path.GetDirectoryName(projectFile);
			var pdInfo = new DirectoryInfo(projectDirectory);
			var solutionDirInfo = pdInfo.Parent;

			var packagesDir = 
				Path.Combine(
					solutionDirInfo.FullName,
					"packages");

			if (!Directory.Exists(packagesDir))
			{
				Trace.TraceInformation("No packages directory found...");
				return;
			}

			var projectDoc = new XmlDocument();
			projectDoc.Load(projectFile);

			var xmlnsmgr = new XmlNamespaceManager(projectDoc.NameTable);
			xmlnsmgr.AddNamespace(
				"msbuild",
				"http://schemas.microsoft.com/developer/msbuild/2003");

			var hintPathNodes=
				projectDoc.SelectNodes(
					"/msbuild:Project/msbuild:ItemGroup/msbuild:Reference/msbuild:HintPath",
					xmlnsmgr);

			if (hintPathNodes == null)
			{
				Trace.TraceInformation("No references found...");
				return;
			}
			foreach (XmlNode hintPathNode in hintPathNodes)
			{
				var hintPath = hintPathNode.InnerText;

				var binIndex = hintPath.IndexOf("\\bin\\");
				var startsWithBin = 
					hintPath.StartsWith(".\\bin\\") || 
					hintPath.StartsWith("bin\\");
				if (binIndex > -1 || startsWithBin)
				{
					var referenceFile = Path.GetFileName(hintPath);

					var newReference = 
						Directory.EnumerateFiles(
							packagesDir,
							referenceFile,
							SearchOption.AllDirectories).FirstOrDefault();

					if (newReference == null)
					{
						Trace.TraceInformation(
							string.Concat(
								"No package found for '",
								hintPath,
								"'..."));
						return;
					}

					Trace.TraceInformation(
						string.Concat(
							"Patching hintpath to '",
							newReference,
							"'..."));

					hintPathNode.InnerText = newReference;

				}
			}
			projectDoc.Save(projectFile);
		}

		public void RemoveVersionSpecification(string projectFile)
		{
			var projectDoc = new XmlDocument();
			projectDoc.Load(projectFile);

			var xmlnsmgr = new XmlNamespaceManager(projectDoc.NameTable);
			xmlnsmgr.AddNamespace(
				"msbuild",
				"http://schemas.microsoft.com/developer/msbuild/2003");

			var referenceNodes =
				projectDoc.SelectNodes(
					"/msbuild:Project/msbuild:ItemGroup/msbuild:Reference",
					xmlnsmgr);
			if (referenceNodes == null)
			{
				Trace.TraceInformation("No references found...");
				return;
			}
			foreach (XmlNode referenceNode in referenceNodes)
			{
				var include = referenceNode.Attributes["Include"].Value;

				var commaIndex = include.IndexOf(",");
				if (commaIndex > -1)
				{
					var reference = include.Substring(0, commaIndex);
					
					Trace.TraceInformation(
						string.Concat(
							"Unversion reference to '",
							reference,
							"'..."));
					referenceNode.Attributes["Include"].Value = reference;

				}
			}
			projectDoc.Save(projectFile);
		}

	}
}

The resulting manifest shows, that the Assembly is nonetheless references with a specific version:

image

Comments

Write a comment