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" />
<Target Name="AfterBuild">
<CreateItem Include="@(ReferencePath)" Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'">
<Output TaskParameter="Include" ItemName="IlmergeAssemblies"/>
</CreateItem>
<Message Text="MERGING: @(IlmergeAssemblies->'%(Filename)')" Importance="High" />
<Exec Command=""$(ProgramFiles)\Microsoft\Ilmerge\Ilmerge.exe" /out:@(MainAssembly) "@(IntermediateAssembly)" @(IlmergeAssemblies->'"%(FullPath)"', ' ')" />
</Target>
<Target Name="_CopyFilesMarkedCopyLocal"/>
</Project>
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:
...
<ItemGroup>
<ProjectReference Include="..\XmlStuffLibrary\XmlStuffLibrary.vbproj">
<Project>{someguid}</Project>
<Name>XmlStuffLibrary</Name>
<Private>True</Private>
<IlMerge>True</IlMerge>
</ProjectReference>
</ItemGroup>
<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.
MERGING: XmlStuffLibrary
"C:\Program Files (x86)\Microsoft\Ilmerge\Ilmerge.exe"
/out:bin\Debug\MainLibrary.dll "obj\Debug\MainLibrary.dll"
"C:\dev\CSharpVBTogetherAtLast\MergeConsole\XmlStuffLibrary
\bin\Debug\XmlStuffLibrary.dll"
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!
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
It would be way cooler if VS allowed you to mix code files written in different languages within one project.
Scott, now that you work for MS and all, can't you just make this happen? ;-)
Visual Studio doesn't support the "netmodule" type, but MSBuild does.
Add the VB project to your solution. Unload the project and edit the project file.
Change OutputType to module : <OutputType>module</OutputType>
Instead of adding a reference to the desired project, we add a module. Sadly, again VStudio fails here, but MSBUILD works just fine. Unload the project and edit the project file. Add an item group with AddModules include directives.
<ItemGroup>
<AddModules Include="..\VbXml\bin\Debug\VbXml.netmodule" />
</ItemGroup>
This will tell msbuild to tell CSC to use /addmodule directives, just like the Reference Item Group which Studio does manage.
Major Drawback: No Visual Studio Intellisense for the added module. We already have references, it is too bad we don't have modules.
SharpDevelop has the first step, but the second step, an "Add Module" gui has been open as a low priority item since SD 2.0.
With both my "cute hack" :P and Jay's solution the result is a single mixed-language assembly. I prefer the IDE experience in mine, but it's cool there are many alternatives - if folks want to get to one assembly, they can, and that's a good thing.
"Neat! I would have just written something like this but it's a matter of preference
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Target Name="AfterBuild">
<Message Text="MERGING: @(ReferencePath->'%(Filename)')" Importance="High" Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'/>
<Exec Command=""$(ProgramFiles)\Microsoft\Ilmerge\Ilmerge.exe" /out:@(MainAssembly) "@(IntermediateAssembly)" @(ReferencePath->'"%(FullPath)"', ' ')"
Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'/>
</Target>
<Target Name="_CopyFilesMarkedCopyLocal"/>
</Project>"
Comments are closed.