Using NuGet packages in web solutions

Adding a NuGet package to a project is simple enough, but integrating them into an existing build strategy can be problematic. Examples are based on Visual Studio 2010, NuGet 1.7 and NAnt 0.91.

Reason

When adding a NuGet package for one or more projects in a solution, the package is initially unpacked to “[solution root]/packages”. References are then added to the projects in question which point to the specific package’s subfolder.
Due to the folder layout of web applications additional processing can be required to move package contents post installation, depending on the prevalent solution architecture in the company.

As an example, consider the following folder structure for a web application containing multiple projects. These could be data access layer, business logic, presentation and so forth:

Solution folder structure

When adding a NuGet package to projects “ComponentA” and “ComponentB”, the package and its dependencies are unpacked to the root of the solution:

Managing solution NuGet packages

NuGet package location

References to the installed packages can now be found when inspecting the project files as shown in the following excerpt:

<Reference Include="Reason.Code.Example.NuGetPackage">
  <HintPath>..\..\..\packages\Reason.Code.Example.NuGetPackage.1.0.0\lib\net40\Reason.Code.Example.NuGetPackage.dll</HintPath>
</Reference>
<Reference Include="Reason.Code.Example.OtherNuGetPackage">
  <HintPath>..\..\..\packages\Reason.Code.Example.OtherNuGetPackage.2.0.1\lib\net40\Reason.Code.Example.OtherNuGetPackage.dll</HintPath>
</Reference>

As per usual the referenced binaries are copied to the output folder of each project upon building the solution (unless “copy local” is set to false for the references in question) — but when dealing with a web solution it’s necessary to copy these binaries into the binary folder of the web root (i.e. “[…]/ExampleSolution/www/bin” in the example above), which can be achieved in several ways.

Code

Post-build event

Using a post-build event which copies all binaries in the standard project output folder to the solution binary folder is very simple to set up, as shown below.

Editing post-build event for a project

This method has the following drawbacks:

  • Must be set up separately for every project.
  • Only works if every project has “copy local” enabled for NuGet references.

Custom build task

Using a custom NAnt or MSBuild task initially requires more effort than using the inherent post-build event functionality, but quickly becomes a time saver when used across many solutions. An example of such a custom task for NAnt is shown below:

[TaskName("nugetpackagecopy")]
public class NuGetPackageCopyTask : Task
{
  [TaskAttribute("projectfile", Required = true)]
  [StringValidator(AllowEmpty = false)]
  public string ProjectFile
  {
    get;
    set;
  }

  [TaskAttribute("todir", Required = true)]
  [StringValidator(AllowEmpty = false)]
  public string TargetDirectory
  {
    get;
    set;
  }

  protected override void ExecuteTask()
  {
    Log(Level.Verbose, string.Format("Copying NuGet packages for \"{0}\".", Path.GetFileName(ProjectFile)));
    XDocument document = XDocument.Load(ProjectFile);
    XName nodeName = XName.Get("HintPath", "http://schemas.microsoft.com/developer/msbuild/2003");
    foreach (XElement element in document.Root.Descendants(nodeName))
    {
      string path = element.Value;
      if (IsNuGetPackageReference(path))
        CopyFileToTargetDirectory(path);
    }
  }

  private bool IsNuGetPackageReference(string path)
  {
    return path.ToLower().Replace("\\", "/").Contains("/packages/");
  }

  private void CopyFileToTargetDirectory(string relativeNuGetPackagePath)
  {
    string projectDirectory = Path.GetDirectoryName(ProjectFile);
    string absoluteNuGetPackagePath = Path.Combine(projectDirectory, relativeNuGetPackagePath);
    string fileName = Path.GetFileName(relativeNuGetPackagePath);
    string targetFileName = Path.Combine(TargetDirectory, fileName);
    Log(Level.Debug, string.Format("Copying \"{0}\" to \"{1}\".", absoluteNuGetPackagePath, targetFileName));
    File.Copy(absoluteNuGetPackagePath, targetFileName, true);
  }
}

A custom build tasks provides the following benefits:

  • Only requires to be set up once per solution (as opposed to once per project).
  • Works whether “copy local” is enabled or not.

Example

Below is an example of  a NAnt build file using the custom task described earlier. The custom code can simply be copied into the “code”-node, or compiled to a separate assembly and placed in the NAnt install root.

<?xmlversion="1.0"?>
<!-- Custom task -->
<script language="C#">
  <references>
    <include name="System.Xml.dll"/>
    <include name="System.Xml.Linq.dll"/>
  </references>
  <imports>
    <import namespace="System.Xml.Linq"/>
  </imports>
  <code>
    <![CDATA[
      C# code goes here.
    ]]>
  </code>
</script>

<!-- Build targets -->
<project name="NuGetExample" default="build" basedir=".">
  <property name="debug" value="true"overwrite="false"/>
  <property name="solution.root" value="."/>
  <property name="solution.web.root" value="${solution.root}/www"/>

  <target name="build" depends="copy.nuget.packages">
    <!-- Standard build steps omitted -->
  </target>

  <target name="copy.nuget.packages">
    <foreach item="File" property="filename">
      <in>
        <items>
          <include name="${solution.web.root}/**/*.csproj"/>
        </items>
      </in>
      <do>
        <nugetpackagecopy projectfile="${filename}" todir="${solution.web.root}/bin"/>
      </do>
    </foreach>
  </target>
</project>

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s