Manage Your MEF Parts with NuGet

Why?

With MEF you have the flexibility to compose applications without taking a reference to every required assembly. MEF’s flexibility is one of its most seductive features. But I see a dark side. Maybe I’m paranoid, but I miss the security of taking references. Before MEF, I never worried about getting my assemblies into the bin directory because MSBuild took care of that for me. Now I do worry about it, a little, and this is one of the reasons I’ve written about a method to test MEF composition.

I also really like NuGet. Most people compare NuGet to apt, but to me it’s always been “CPAN for .NET”. I guess that tells you a little bit about where I’m coming form. But whatever your background, you have to love a system that lets you say “Here is the thing I’m interested in using, you go get the rest.” But at first glance, MEF and NuGet don’t play well together (even though NuGet uses MEF!) NuGet makes the fundamental assumption that when a library exists in a NuGet package, then the intent when installing that package is to reference that library. I’m not saying that’s a bad assumption in general, it’s just not a very good assumption to make when dealing with libraries of MEF parts.

The good news is that NuGet can be used to create MEF friendly packages that don’t create references when installed, but do ensure that the parts are copied to the bin directory. You get runtime-only references, with all the NuGet package management goodness. I’ll be the first to admit that some of the techniques used to make this happen are hacky, but I’ve tested it and it works for me. So strap yourself in, and read on.

A Little Background

I’m going to start by showing the structure of a nuspec file which NuGet can use to make a MEF friendly package.

After showing the nuspec, I’ll show how to automate spec and package creation using MSBuild.

  • It will be helpful to know a little MSBuild. I wish I had a good resource, but I don’t. Somehow, I’ve just learned it though osmosis. This one looks nice. I mostly look at the reference when I have questions.
  • To create the MEF-friendly package, I’ll extend the recipe I wrote for creating a “Standard” NuGet package with MSBuild.
  • You will need the nightly build of the MSBuild Community Tasks.
  • There will be some XSLT. I can vouch for w3schools, since I used it as my primary reference for creating the XSLT used in this post.

Anatomy of a MEF Friendly Package

Here is the nuspec which builds a MEF friendly package for the CheesePizzaMaker assembly from my MEF talk.

<?xml version="1.0" encoding="utf-8"?>
<package>
  <metadata>
    <id>CheesePizzaMaker.MEF</id>
    <version>1.0.4441.28470</version>
    <authors>Jim Counts</authors>
    <owners>Jim Counts</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Makes Cheese Pizza</description>
    <copyright>Copyright 2012</copyright>
    <title>CheesePizzaMaker (MEF)</title>
    <dependencies>
      <dependency id="PizzaStore" version="" />
    </dependencies>
    <frameworkAssemblies>
      <frameworkAssembly assemblyName="System" targetFramework="" />
      <frameworkAssembly assemblyName="System.ComponentModel.Composition" targetFramework="" />
    </frameworkAssemblies>
  </metadata>
  <files>
    <file src="bin\x86\Release\CheesePizzaMaker.dll" target="content\Parts" />
    <file src="bin\x86\Release\CodeContracts\CheesePizzaMaker.Contracts.dll" target="content\Parts" />
    <file src="bin\x86\Release\CheesePizzaMaker.pdb" target="content\Parts" />
    <file src="NuGet\install.ps1" target="tools" />
  </files>
</package>

This nuspec file is meant to be the target of the “nuget pack” command. Since the target will not be a project file, NuGet will not fill any of the fields for us at runtime, so there are no replacement tokens like there were in the “standard” nuspec I wrote about before. But, you can avoid filling out most of the data by hand if you give NuGet an assembly to examine when it runs the “nuget spec” command. I’ll show you how to do that later in this post, or you can consult the NuGet docs. After generating the spec, I customized some fields:

  • Id – to distinguish the MEF friendly package, I added .MEF to the Id.
  • Title – I added the title field, and used a title that made it easy to distinguish in the Package Manager
  • Dependencies – I added the PizzaStore package as a dependency. This package contains the interface definitions shared between parts and their host. When the version field is blank, NuGet will look for the highest available version, and that’s the behavior I want, but you can specify a version if you like. Also, note that the PizzaStore package is a standard package; it will create a reference in the project where it is installed. Since it only contains interfaces, and not parts this is what we want. However, you could (in theory, I haven’t tried this yet) have MEF packages that depend on other MEF packages.
  • Framework Assemblies – Since this is a MEF friendly package, we know that the target project will need MEF. So, System.ComponentModel.Composition and System are both listed as dependencies. NuGet will add them to the project if they aren’t already there. You could leave this off, but then you would have to add the references by hand. Automation is smoother, take advantage of it.
  • Files – Finally we have the files section. We have the same three files that I included in the standard package, and one more file, which is the install.ps1 script which runs when the package is installed. Other than the PowerShell script, which we will look at in a moment, the big difference is where the files are located in the package. Instead of placing the files in the lib folder, as is standard, I’ve put them into the content folder. Furthermore, I’ve placed them in a subdirectory called “Parts”.

Because the libraries are in the content folder, NuGet will not add a reference to the files when the package is installed. However, it won’t copy them to the bin directory either, so placing the files into the content folder only solves half the problem. Install.ps1 solves the other half:

param($installPath, $toolsPath, $package, $project)

foreach ($runtimelib in $project.ProjectItems.Item("parts").ProjectItems)
{
    if(-not $runtimeLib.Name.EndsWith(".Contracts.dll"))
    {
        # Set to "Copy Always"
        $runtimelib.Properties.Item("CopyToOutputDirectory").Value = 1
    }
    else
    {
        # Change to "None"
        $runtimelib.Properties.Item("BuildAction").Value = 0;
    }
}

To make the MEF friendly package work, I’ve adopted the convention that the Parts folder contains MEF parts which should be copied to the bin directory. The only exception is the Contracts library, which also contains the MEF attributes for the assemblies they describe, and can cause cardinality mismatches if it is deployed to bin. By default, content files are placed directly in the project folder, the build action is set to “Content”, and the copy to output directory property is set to “Do not copy”. Because we specified a subfolder in the nuspec file, NuGet will create a “Parts” directory and place the files in there. To get the files copied to the bin directory, the PowerShell script enumerates through the contents of the Parts folder (which is a “ProjectItem” from MSBuild’s point of view) and change the value of the “CopyToOutputDirectory” property to 1 as long as the file doesn’t end with “.Contracts.dll.” In the enumeration for “CopyToOutputDirectory”, 1 means “Copy Always”, good luck on finding that documented anywhere (send me a link if you find it.)

In theory, the top half of the if-statement is all you need to make the package MEF friendly. But, I discovered that MSI files will include the Contracts library if you leave the Build Action equal to “Content.” Again, this will cause cardinality mismatch exceptions if you then use the MSI to install your application. What’s worse is that you will only see the problem in production. So, to save us from this headache, the PowerShell script will set the Build Action equal to “None” when it finds the Contracts library.

So that’s a MEF friendly NuGet package. When you install the package, your libraries will appear in the Parts folder. On build, your assembly and its pdb will be copied to a “Parts” folder in the bin directory. The only adjustment you need to make in your application is that you must include a DirectoryCatalog for this new subfolder in your composition. You might also have noted the hardcoded value for the package version. In the next section I’ll show how this number can be updated at runtime, but if you don’t want to read that, just know that this value can be overridden from the command line when calling “nuget pack”.

Automating with MSBuild

I’m sensitive to the needs of those who like to cut and paste, so here is a link to the NuGet folder for the CheesePizzaMaker on bitbucket.org. Do come back though, because automating the MEF friendly package is a little more complex than the standard package.

Consuming the Build Script in the Project

This step is just like consuming the standard build script.

  1. Unload your project.
  2. Edit your project.
  3. Update your project file’s “AfterBuild” target.

If you are unfamiliar with editing a project file, visit my previous article on creating NuGet packages with MSBuild for step-by-step instructions. If you are planning to build both types of packages (standard and MEF-friendly) then your AfterBuild target should look like this when you are done:

  <Target Name="AfterBuild">
    <MSBuild Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"
       Projects="NuGet\NuGet.msbuild" />
    <MSBuild Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"
       Projects="NuGet\NuGet.MEF.msbuild" />
  </Target>

Setup Support Files

We need to create several supporting files, two that enable the build script to customize the nuspec file for us, and one which we include in the nuget package. Finally, we include NuGet itself, for convenience (as discussed previously, this is my personal preference, but the build script will need to be adjusted if your preference differs.) You don’t need to add these files to the solution, but if you do, make sure that you set the Build Action to “None”.

  • NuGet.exe – Go ahead and create a folder called NuGet in your project, and save a copy of NuGet.exe in this folder.
  • Install.ps1 – This file is included in the MEF-friendly NuGet package tools folder. The generated nuspec will expect to find this file in the NuGet subfolder. So create a text file called install.ps1 and copy the contents of the PowerShell script into it (shown above and available on bitbucket.org).
  • Mefdata.xml – This file is a source file for an XmlMassUpdate task in the MEF-friendly build script. It contains all the custom elements except the dependencies. Some of the data is dummy data, which the script will update in a later step. Create this file in the NuGet folder.
<?xml version="1.0" encoding="utf-8"?>
<package xmlns:xmu="urn:msbuildcommunitytasks-xmlmassupdate">
  <metadata>
    <title>title</title>
    <frameworkAssemblies>
      <frameworkAssembly xmu:key="assemblyName"
         assemblyName="System" targetFramework="" />
      <frameworkAssembly xmu:key="assemblyName"
         assemblyName="System.ComponentModel.Composition" targetFramework="" />
    </frameworkAssemblies>
  </metadata>
  <files>
    <file xmu:key="src" src="AssemblyPath" target="content\Parts" />
    <file xmu:key="src" src="ContractPath" target="content\Parts" />
    <file xmu:key="src" src="SymbolPath" target="content\Parts" />
    <file xmu:key="src" src="NuGet\install.ps1" target="tools" />
  </files>
</package>
  • dependencydata.xslt – If the project has a “packages.config” file then it means that there are some NuGet packages installed in the project. The script uses this transform file to convert packages.config into an XmlMassUpdate source which contains the contents of the <dependencies /> node in the nuspec file. Create this file in the NuGet folder.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                exclude-result-prefixes="msxsl"
>
  <xsl:output indent="yes"/>
  <xsl:template match="/">
    <package xmlns:xmu="urn:msbuildcommunitytasks-xmlmassupdate">
      <metadata>
        <dependencies>
          <xsl:for-each select="packages/package">
            <dependency xmu:key="id">
              <xsl:attribute name="id">
                <xsl:value-of select="@id"/>
              </xsl:attribute>
              <xsl:attribute name="version"/>
            </dependency>
          </xsl:for-each>
        </dependencies>
      </metadata>
    </package>
  </xsl:template>
</xsl:stylesheet>

Now we’re ready to create the NuGet.MEF.msbuild script.

Create the Build Script

Create an XML file in the NuGet folder and call it NuGet.MEF.msbuild. You can download the complete file from bitbucket.org, but I will go through each section step by step here.

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="4.0"
         DefaultTargets="default">
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
  <PropertyGroup>
    <NuGetApp>NuGet.exe</NuGetApp>
    <ProjectDir>$(MSBuildProjectDirectory)\..</ProjectDir>
    <PackageDir>$(ProjectDir)\..\..\..\PackageSource</PackageDir>
    <ReleasePath>bin\x86\Release</ReleasePath>
    <ProjectName>CheesePizzaMaker</ProjectName>
    <PackagesFile>$(ProjectDir)\packages.config</PackagesFile>
    <DependencyFile>$(MSBuildProjectDirectory)\dependencydata.xml</DependencyFile>
    <MefDataFile>$(MSBuildProjectDirectory)\mefdata.xml</MefDataFile>
    <XsltFile>$(MSBuildProjectDirectory)\dependencydata.xslt</XsltFile>
    <PackageId>$(ProjectName).MEF</PackageId>
    <MefNuSpecFile>$(ProjectDir)\$(PackageId).nuspec</MefNuSpecFile>
    <ReferenceLib>$(ReleasePath)\$(ProjectName).dll</ReferenceLib>
    <ContractLib>$(ReleasePath)\CodeContracts\$(ProjectName).Contracts.dll</ContractLib>
    <SymbolLib>$(ReleasePath)\$(ProjectName).pdb</SymbolLib>
  </PropertyGroup>

We start off by creating our project and importing the MSBuild Community Tasks. Next we setup some data used throughout the script. Some of these properties aren’t as clean as I’d like them to be, so there might be some refactoring in my future. Here are the highlights:

  • PackageDir – the script will move completed packages to this folder at the end of the process.
  • ReleasePath – relative path to the bin directory to pull files from.
  • ProjectName – if you don’t make any customizations to this process, then this is the only field you need to worry about updating.
  • PackageId – my convention is to suffix “.MEF” to the project name. You can setup your own convention here.
  • MefNuSpecFile – this is where the build script will put the customized nuspec file when it is done writing it.
  • ReferenceLib – this is the part library you want to package.
  • ContractLib – the Code Contracts reference assembly is optional, but you need to make some adjustments to this recipe if you don’t want to include it.
  • SymbolLib – the debugging symbols, also optional, but it’s useful to me in my packages.

The first targets follow the property section:

  <Target Name="clean">
    <Delete Files="$(PackageId).nuspec; ..\$(PackageId).nuspec; $(ProjectName).nuspec; $(DependencyFile)" />
  </Target>
  <Target Name="default" DependsOnTargets="MefSpec; MefPackage; MovePackages"/>

During script development, it’s nice to be able to reset everything, so the clean target will delete intermediate and output files. You can invoke it using the /t switch on the command line.

C:\PizzaStore\CheesePizzaMaker\NuGet>c:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe NuGet.MEF.msbuild /t:clean

The default target specifies which targets must be executed to build and deploy the package. There is nothing special about the name “default”. It is only the default target because it is specified in the DefaultTargets attribute in the project element. We could specify “clean” or “MefSpec” as the default, but that wouldn’t make much sense.

  <Target Name="MefSpec"
          Condition="!Exists($(MefNuSpecFile))"
          DependsOnTargets="CreateDependencyFile; CreateMefSpec; CleanMefSpec; MergeMefSpec; UpdateMefSpec">
  </Target>

Like “default”, the MefSpec target is a meta-target which specifies other targets to run. It also includes a condition which will skip the spec creation if a spec already exists. This means that you can further customize the generated spec by hand without worrying about overwriting your changes on the next build.

  <Target Name="CreateDependencyFile" Condition="Exists($(PackagesFile))">
    <XslTransformation
      OutputPaths="$(DependencyFile)"
      XmlInputPaths="$(PackagesFile)"
      XslInputPath="$(XsltFile)"
      />
  </Target>

CreateDependencyFile executes an XslTransformation task when “packages.config” exists. It applies dependencydata.xslt to packages.config and creates dependencydata.xml, which is an XmlMassUpdate source file.

  <Target Name="CreateMefSpec">
    <Exec WorkingDirectory="$(MSBuildProjectDirectory)"
          Command="$(NuGetApp) spec -AssemblyPath &quot;$(ProjectDir)\$(ReferenceLib)&quot;" />
    <Move SourceFiles="$(ProjectName).nuspec"
          DestinationFiles="$(MefNuSpecFile)" />
  </Target>

CreateMefSpec executes “nuget spec” to create a default spec file. We give NuGet the path to the parts assembly so that it can fill in some of the standard data for us. Note that this command is executed from the “NuGet” subfolder for two reasons. First, we want to avoid overwriting the standard nuspec file if it exists, I couldn’t see a way to specify the output file name from the NuGet command line. Second, NuGet will automatically use the csproj file to generate a tokenized spec file for us if we run the spec command from a directory which contains a csproj (or any other project) file. We don’t want a tokenized spec file, so we need to run from a folder where NuGet can’t see the project file. Ultimately, we want the MEF-friendly spec file in the project folder, so we move it there before finishing CreateMefSpec.

  <Target Name="CleanMefSpec">
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/licenseUrl"
               Delete="true" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/projectUrl"
               Delete="true" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/iconUrl"
               Delete="true" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/releaseNotes"
               Delete="true" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/tags"
               Delete="true" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/dependencies"
               Delete="true" />
  </Target>

In CleanMefSpec, I delete fields I don’t care about, and one field I intend to replace with XmlMassUpdate. If you have use of the different URLs, notes, etc., then you should not delete these fields, but update them in the UpdateMefSpec target.

  <Target Name="MergeMefSpec">
    <XmlMassUpdate Condition="Exists($(DependencyFile))"
       ContentFile="$(MefNuSpecFile)"
                   ContentRoot="/"
                   SubstitutionsFile="$(DependencyFile)"
                   SubstitutionsRoot="/" />
    <XmlMassUpdate Condition="Exists($(MefDataFile))"
                   ContentFile="$(MefNuSpecFile)"
                   ContentRoot="/"
                   SubstitutionsFile="$(MefDataFile)"
                   SubstitutionsRoot="/" />
  </Target>

MergeMefSpec performs one or two XmlMassUpdate tasks. If there was a packages.config file, then there should now be a dependencydata.xml file created by the “CreateDependencyFile” target. If so, MergeMefSpec will update the MEF-friendly nuspec with the dependencies. You might want to review the dependencies after the spec is written. This technique does no interdependency analysis of the packages in packages.config. It just takes a dependency on all of them. So if you are writing package A, and A depends on B, and B depends on C, then your packages.config will list B and C as installed packages. However, package A may not need a direct dependency on C. You can imagine a scenario where B is updated and no longer requires C, in which case package A doesn’t need to include it any more either, but your spec doesn’t know that. If you are worried about this scenario, your current option is to edit the spec by hand to remove the indirect dependencies. This is one of the hacky areas I would like to revisit in the future, but for now just be aware of it.

Next, MergeMefSpec uses XmlMassUpdate to merge in the “static” customizations from mefdata.xml that don’t depend on whether or not there are other NuGet packages installed in the project. At this point we have a template nuspec which needs to be updated with relevant data from the project.

  <Target Name="UpdateMefSpec">
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/id"
               Value="$(PackageId)" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/metadata/title"
               Value="$(ProjectName) (MEF)" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
              XPath="/package/files/file[@src='AssemblyPath']/@src"
               Value="$(ReferenceLib)" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/files/file[@src='ContractPath']/@src"
               Value="$(ContractLib)" />
    <XmlUpdate XmlFileName="$(MefNuSpecFile)"
               XPath="/package/files/file[@src='SymbolPath']/@src"
               Value="$(SymbolLib)" />
  </Target>

UpdateMefSpec replaces some defaults and dummy data with the real values. Here we update the id, the title, and the paths to the package files. The custom nuspec is now complete.

  <Target Name="MefPackage"
          Condition="Exists($(MefNuSpecFile))">
    <!-- Get the version number of the main assembly to pass to nuget cli -->
    <GetAssemblyIdentity AssemblyFiles="$(ProjectDir)\$(ReferenceLib)">
      <Output TaskParameter="Assemblies"
              ItemName="AsmInfo" />
    </GetAssemblyIdentity>
    <Exec WorkingDirectory="$(ProjectDir)"
          Command="&quot;$(MSBuildProjectDirectory)\$(NuGetApp)&quot; pack &quot;$(MefNuSpecFile)&quot; -Verbose -NoPackageAnalysis -Version %(AsmInfo.Version)" />
  </Target>

Next up is MefPackage, where we build the MEF-friendly package. MefPackage first examines the parts assembly using a GetAssemblyIdentity task. This task places the assembly information into the property specified by the ItemName attribute; in our case it is called “AsmInfo”. Next we execute “nuget pack”. Our target is the nuspec file. I use Verbose because I like it. NuGet will not be happy that we have put dll files in the content folder and issues warnings suggesting that we might want to put them in a lib folder. Since the whole point of this exercise is to distribute libraries as content rather than references, I’ve suppressed the analysis step using NoPackageAnalysis. Finally, we specify the Version flag and give it the version data supplied by the GetAssemblyIdentity task.

  <Target Name="MovePackages"
          Condition="Exists($(PackageDir))">
    <!-- Using command line because I want to be sure to get the most up to date list of *.nupkg -->
    <Exec WorkingDirectory="$(ProjectDir)"
          Command="move /y *.nupkg &quot;$(PackageDir)&quot;" />
  </Target>
</Project>

As with the standard nuget build script, MovePackages checks that the destination directory exists then uses the shell to move the packages to the destination. I found that it was much easier to use the shell because it will get the most recent list of *.nupkg files in the source folder, and will happily overwrite packages in the destination folder. Finally, we close the project element and the MSBuild script is done.

Test the Build

Switch to release mode and build your project. If everything is setup correctly, your packages should appear in the folder specified by the PackageDir property. If not, see my previous post for troubleshooting tips.

Test the Package

In the PizzaStore example, there is a dummy program that does nothing but serves as a place to simulate different composition scenarios. To test the package I removed the project reference to PizzaStore. There was no reference to CheesePizzaMaker since I used .bat files to move the dll files around during the MEF talk.

After removing the PizzaStore reference I setup Visual Studio to use PackageDir as a package source, and then used the NuGet package manager in GUI mode to install the MEF package. As expected, NuGet created the Parts folder, set the Build Action and other properties, and also pulled in the dependency to PizzaStore and re-created the reference (this time as an assembly reference rather than a project reference).

Uninstalling the CheesePizzaMaker.MEF package also removed all the content files and the assembly reference.

Thanks for Reading

There you have it: MEF, now with NuGet. I’m sure there is a candy bar pun in there somewhere.

About these ads

3 thoughts on “Manage Your MEF Parts with NuGet

  1. Pingback: Creating Packages with NuGet the MSBuild Way « I had this idea once…

  2. Hey there,

    I’ve had the exaclty same problem and went a different approach. I’m not that a fan of nugets “Content” folder, i don’t like the way updates are handled and I didn’t want to see the dll’s as solution items.

    As I wanted a generic solution which I can reuse in other packages I’ve created a nuget package which copies everything you have in your packages “output” folder to the output path of the project by just adding a package dependency to your nuget package.

    I’ve written a small blog post about it:

    http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0

    Kind regards
    Daniel

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