Mixing Languages in a Single Assembly in Visual Studio seamlessly with ILMerge and MSBuild
I really like the new LINQ to XML direct language support in VB9. However, while it's cool, I'm not ready (or willing) to dump C# and start using VB. But, if I could only use VB9 just for the XML stuff...
Sure, I could create a VB assembly and a C# assembly and add them to a solution, but then I'd have two assemblies. I could add a PostBuildEvent batch file and call ILMerge myself (merging the two assemblies into one), but that just doesn't seem pure enough.
What I really want is to be able to mark an assembly as merge-able and have it automatically merged in just because it's referenced.
Here's what I came up with. Thanks to Dan Moseley and Joshua Flanagan for their help. Thanks to Jomo Fisher for the Target File.
First, here's the sample project. There's a C# MergeConsole.exe that references a C# MainLibraryILMerge.dll that references a VB XmlStuffLibrary.
If I compile it as it is, I get a folder structure that includes three resulting assemblies.
Now, here's were we start messing around. Remember, we don't want to have the extra VB-specific XmlStuffLibrary right? We just want to use the XML features.
First, download and install ILMerge in the standard location.
Now, go to C:\Program Files\MSBuild (or C:\Program Files (x86)\MSBuild on x64) and make a new text file called "Ilmerge.CSharp.targets". This file is the start of a hack we're going to borrow from Jomo Fisher.
Note the red bits in the file below. We're creating an "AfterBuild" target...a Post Build Event in the MSBUILD world.
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<CreateItem Include="@(ReferencePath)" Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'">
<Output TaskParameter="Include" ItemName="IlmergeAssemblies"/>
<Message Text="MERGING: @(IlmergeAssemblies->'%(Filename)')" Importance="High" />
<Exec Command=""$(ProgramFiles)\Microsoft\Ilmerge\Ilmerge.exe" /out:@(MainAssembly) "@(IntermediateAssembly)" @(IlmergeAssemblies->'"%(FullPath)"', ' ')" />
In this target, we're going to look for assemblies that are marked CopyLocal but also that have IlMerge equal to true. Then we'll call IlMerge passing in those referenced assemblies.
Is this some undocumented MSBUILD thing? No, you can put whatever you want in an MSBUILD file and refer to it later. Since CSPROJ files (Visual Studio Projects) are MSBUILD files, we can open it in Notepad.
Open the CSPROJ for the C# project that references the VB one and make these changes:
<Import Project="$(MSBuildExtensionsPath)\Ilmerge.CSharp.targets" />
<!-- <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> -->
At the bottom there, we're commenting out Microsoft.CSharp.targets (but notice that it's included back in at the top of the new Ilmerge.CSharp.targets.
Then a totally made-up element <IlMerge> is added. That's the most important part. We're saying we want this specific Reference (or References) merged into the final assembly. This made-up element is referenced in the conditional "'%(ReferencePath.IlMerge)" above in the AfterBuild.
The addition of this IlMerge element to Jomo's original hack gives me the flexibility to pick which references I want to merge in, and in my specific case, I'll use the VB9 new XML hotness inside my C# assemblies. Schwing.
Close and save. If you're running Visual Studio, switch back there and it'll prompt you to Reload your project file.Because we've messed it it, you'll get this warning dialog. Select "Load project normally" and click OK.
At this point we can build either from the command-line using MSBUILD on the Solution (SLN) file, but more importantly we can (of course) build from inside Visual Studio.
You can see the output in the Output Window in VS.NET.
"C:\Program Files (x86)\Microsoft\Ilmerge\Ilmerge.exe"
And the results in the bin folder:
Looks like the VB XmlStuffLibrary is gone! But where it is? Let's load up MainLibrary in Reflector:
Looks like they are both in there. To refresh, if I want to merge in VB assemblies:
- Change the referencing CSPROJ to import "IlMerge.CSharp.targets"
- Add <IlMerge>True</IlMerge> to the references you want merged in.
- Save, Reload, Build
C# and VB, living together, mass hysteria!