Extract dlls at runtime
Okay, that's a geeky title, so I'll make it concrete: have you ever wanted to ship 1 dll that can target multiple API versions? Yeah, me too..!
Example: In Umbraco 7.1.5, we added some events that get triggered when Partial Views and Partial View Macros get added/saved/deleted through the tree in the backoffice. We wanted to subscribe to those events from Courier but of course that would mean that Courier version "x" would only work with Umbraco 7.1.5 and up. Trying to subscribe to events that don't exist in 7.1.4 would result in a beautiful YSOD. We don't really want to have the headache of deploying different versions of Courier for different versions of Umbraco.
I suddenly remembered Morten referring to a way to having a dll as an embedded resource which could be extracted on demand, he saw this clever little trick happening in the libgit library. So we'll have a look at the Umbraco version and if it's high enough, we'll extract the dll that can subscribe to those new events, if not, we'll just run the good old version and don't do anything else.
So I ran with that and I can tell you: this works!
There's a few steps:
- Add the embedded resource to your solution and change it's path in the csproj file
- During application startup, check the current Umbraco version and extract the file if possible
- When we update the dll, make sure that it's extracted again
So we'll start with adding an embedded resource, which is pretty simple: right-click on the project you want to add the embedded dll to, go to Properties > Resources > Add Resource > Find the file > done.
Now it gets a little trickier: we want the embedded resource to be updated every time we build the project, so that we always have the latest version of the embedded dll in our main dll. First of all you need to make sure that the build of your main dll depends on the build of the embedded dll. In your main project, right click the project > Build Dependencies> Project Dependencies and check on the project that builds your embedded dll.
Then we need to also make sure that this is not just a one-time copy that's being embedded (Visual Studio's default behavior) but that we refer to the path of the dll that's being built. Open up the csproj file of your main project in your favorite text editor and find the line where it says:
<none Include="Resources\MyDllName.dll" />
Change that Include to the path of MyDllName.dll relative to that project directory, for example:
<none Include="..\MyProjectName\bin\Debug\MyDllName.dll" />
Finally, update from "none" to:
<EmbeddedResource Include="..\MyProjectName\bin\Debug\MyDllName.dll"/>
Voilá, the biggest hurdle is taken. The rest is "just" code.
We'll create a new Umbraco ApplicationEventHandler and try extract the embedded dll when the version of Umbraco is 7.1.5 or higher:
using System; using System.IO; using System.Linq; using System.Reflection; using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; namespace MyApp.ExampleNamespace { internal class ExtractNewEventHandlers : ApplicationEventHandler { protected override void ApplicationInitialized( UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { if (UmbracoVersion.Current >= new Version(7, 1, 5)) ExtractEventHandlers(); } private void ExtractEventHandlers() { var fileName = Path.Combine(HttpRuntime.BinDirectory, "MyDllName.dll"); var fi = new FileInfo(fileName); try { (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) .ExtractResourceToFile( "MyApp.ExampleNamespace.MyDllName.dll", fi.FullName); } catch (Exception ex) { if ((ex is AccessViolationException) == false && (ex is IOException) == false) { throw; } } } } }
The one tricky bit here is that the resource name is a bit weird: it's your default namespace (MyApp.ExampleNamespace) plus the full name of the file you've embedded (MyDllName.dll).
Lastly, we have the extension method ExtractResourceToFile which does the extraction if the file doesn't exist or is different from an existing file.
using System; using System.IO; using System.Reflection; using System.Security.Cryptography; namespace MyApp.ExampleNamespace { internal static class AssemblyExtensions { public static bool ExtractResourceToFile( this Assembly assembly, string resourceName, string outPath) { assembly = assembly ?? Assembly.GetExecutingAssembly(); using (var resourceStream = assembly.GetManifestResourceStream(resourceName)) { var fi = new FileInfo(outPath); if (fi.Exists) { using (var stream = File.OpenRead(outPath)) { if (GetHash(stream)==GetHash(resourceStream)) return false; } } using (var output = File.Create(outPath)) { int bytesRead; var buf = new byte[4096]; resourceStream.Seek(0, SeekOrigin.Begin); while ((bytesRead = resourceStream .Read(buf, 0, buf.Length)) > 0) { output.Write(buf, 0, bytesRead); } } } return true; } internal static string GetHash(Stream stream) { var sha = new SHA256Managed(); var hash = sha.ComputeHash(stream); return BitConverter.ToString(hash) .Replace("-", String.Empty); } } }
And that's about it. On each application pool start, we check if the hash of the file on disk is the same as the hash of the embedded resource, if so we do nothing, else we'll overwrite the file in the bin folder with the new embedded version. And if someone running this upgrades their Umbraco version: boom! They'll get the extra functionality from our embedded dll for free, automagically.
I hope this might help some other people dealing with targeting multiple versions of APIs. However: remember that this is probably not so efficient for very large embedded resources. With great power comes great responsibility so before you go down this route make sure that this method is really what you want and need.