NuGet package customizations and optional references

By jay at November 25, 2011 20:55 Tags: , , , ,

This article describes a bit of what NuGet does and why you should take a look at it, but also a package installation customization to a work around a problem with packages that contain optional assemblies.

 

NuGet is a fantastic tool. Its ability to ease package discovery, installation and update is a big time saver. As a solution maintainer, you can spend less time deploying and updating your external dependencies, particularly when they’re often updated.

 

Private NuGet Repositories

It can be used to easily install public packages exposed via Microsoft’s servers, but it can also be used to create private package repositories.

I've been using it to publish internal framework that is often updated and used in many projects. The automatic creation of packages in a build script and the ease of deployment using the NuGet OData server is, again, a big time saver.

Each time a check-in is performed to update the internal framework, a new package is automatically created and it appears as a package update for the projects that use it.

 

Existing libraries and NuGet

The package model is built around some basic rules:

  • Each package is installed using its version as the directory name
    The NuGet engine will update all the references “HintPath” nodes of projects files for them to point to the updated package location.
  • By default, the install action of a package adds references to all the available assemblies in the target project.
    There’s a pretty good reason for that; you don’t want to download and install assemblies that you don’t need. There is no support for “sub-packages”, even with reference exclusions. Big frameworks, like Enterprise Library, get chunked into small dependent packages, and you can install only those needed by your projects.
  • A project that installed a package that has references to package-excluded references will not have these manual references updated to the latest package.
    That is a side effect of the second rule. If you add reference-excluded assemblies to a package, the action of updating that package will not update manual references you created by referencing these assemblies.

     

    For existing libraries that may not easily be split into small pieces, primarily for time constraints, moving to NuGet can be a tough job. If you make a package with all your assemblies and only reference a few of them by default in there, then updating the package can quickly become an annoying “Find and Replace” job to manually change references that did not get updated automatically by the NuGet engine.

     

    Using the Install.ps1 script

    Fortunately, there is a file that can be included in the package, which is by convention named and located in tools\install.ps1.

    It is a PowerShell script that gets called automatically when the package is installed. But the interesting part is that this installer file gets called with a DTE.Project instance that can be used to manipulate the actual project in VS2010 !

    This means that when the package is installed, it is possible to update the references that were manually created to the previous package assemblies.

     

    Updating the manual references

    This is not a straightforward as it may seem, but after working around a HintPath update issue, here is a small helper to do the job:

    param($installPath, $toolsPath, $package, $project)
        
        $packageName = $package.Id + '.' + $package.Version;
        $packageId = $package.Id;
    
        # Full assembly name is required
        Add-Type -AssemblyName 'Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
    
        $projectCollection = [Microsoft.Build.Evaluation.ProjectCollection]::GlobalProjectCollection
        
        # There is no indexer on ICollection<T> and we cannot call
        # Enumerable.First<T> because Powershell does not support it easily and
        # we do not want to end up MethodInfo.MakeGenericMethod.
        $allProjects = $projectCollection.GetLoadedProjects($project.Object.Project.FullName).GetEnumerator(); 
    
        if($allProjects.MoveNext())
        {
            foreach($item in $allProjects.Current.GetItems('Reference'))
            {
                $hintPath = $item.GetMetadataValue("HintPath")
                
                $newHintPath = $hintPath -replace $packageId + ".*?\\", "$packageName\\"
            
                if ($hintPath -ne $newHintPath)
                {
                    Write-Host "Updating $hintPath to $newHintPath"
                    $item.SetMetadataValue("HintPath", $newHintPath);
                }
            }
        }

    This script is called for each project the package is installed onto, and scans all the references of the project that match the current package to update them.

    You’ll notice that the ICollection<T> interface is not particularly PowerShell friendly. Too bad the Powershell syntax does not allow the use of generic methods, otherwise that nasty GetEnumerator / MoveNext could have gone away. Still, Powershell is dynamically typed so using IEnumerable.Current is fine.

blog comments powered by Disqus

About me

My name is Jerome Laban, I am a Software Architect, C# MVP and .NET enthustiast from Montréal, QC. You will find my blog on this site, where I'm adding my thoughts on current events, or the things I'm working on, such as the Remote Control for Windows Phone.