First let me lay out the problem: you’re writing a library that so far is self-contained as a single DLL. You want to add some functionality to your library that is available in another library that your users may or may not have or wish to deploy with your library. You can either add a reference to that library and code against it in your library and require that your users deploy both libraries together from now on, or you can embed the functionality of the second library directly into your own library (by copying source code or rewriting the behavior yourself) so your users only have the one DLL to deploy. Both have their pros and cons. Isn’t there a way to capture the best of both worlds? Yes. Two, in fact.
Pros and cons to adding an external dependency
I’ve already written a post on An argument for the extra dependency of a library. Deploying another DLL to a web site’s Bin directory is trivial in cost and time. So what’s the big deal? Perhaps server administrators have a review process that every binary must go through before it is allowed in their web site’s Bin directory for security reasons. Perhaps if you think your customers are looking for a cool feature that your library implements but then discover that your library requires the presence of several other libraries as well (perhaps for features this individual doesn’t even care to use) they might look elsewhere.
On the flip side, if you just copy the code (or rewrite it yourself) into your library so that everything is in one big DLL, well now you have a versioning nightmare because every time the other library gets serviced (maintenance release, security bug found and fixed, etc.) you have to diff the changes and merge them into your own product. Also, if your Library A includes Library B in its DLL and your customers aren’t even aware of it, then when the author of Library B issues a broad security warning that your customer sees he may not even realize it applies to him, and even if he did, what could he do about it since he cannot modify Library A with the patch for Library B?
How to minimize the downsides of the external dependency
So let’s assume I’ve convinced you to use the external library as-is without bundling it into your own DLL. But you still want that other library to be optional to your customers. This may be because the other library provides a rarely used feature, or perhaps a common feature that is used everywhere but can be altogether turned off (like logging).
Logging in fact is the scenario that drove me to develop the pattern I’m about to present to you. I found .NET’s System.Diagnostics.Trace class completely incapable of running when in a partial trust environment, so I had to find another logging mechanism. Log4net is a pretty well-known one that works in partial trust environments so it fit the bill. The only trouble was many of my users didn’t want to have to deploy log4net.dll alongside my own library. So here were my requirements:
- If log4net.dll was present, the product should use it for its logging mechanism.
- If log4net.dll was not present, the product should quietly switch to using System.Diagnostics.Trace if the product was running with full trust.
- If log4net.dll was not present and the product was running under partial trust, logging would be disabled.
- This was logging so it had to be fast and could not slow the product down significantly whether logging was enabled or not.
The first obvious way that occurred to me to use log4net.dll if it was present but not actually require it to be deployed with my library was to use .NET reflection to discover the DLL and load it if it was present, and then using reflection invoke the various methods necessary to log messages. Using reflection to invoke methods is painfully slow however, so that was out.
Another way to do it without the full cost of reflection at every logged message was to use Reflection.Emit to generate code on-the-fly that would call into log4net. This generated code would consist of generated method stubs that would call into log4net in an early-bound way so that it was much faster. But Reflection.Emit still has an upfront cost, and besides it isn’t available at all in most partial trust environments, which was the whole point of using log4net. So Reflection.Emit was out.
Finally I came to the following solution, which consists of a reusable pattern for writing fast code that calls into a library that may or may not be there and you can decide what to do in either case. The pattern follows.
The pattern for using a library that may not be present
I’ll be using log4net throughout the description of this pattern for illustrative purposes, but the pattern works perfectly well for other external libraries (even if those external libraries come with external dependencies of their own).
First, add an assembly reference to the external library (log4net.dll).
We need to define an interface within our own project that exposes all the functionality in the external library that we will need to access. There is one interface in log4net that is interesting throughout our project: log4net.ILog. So we’ll define DotNetOpenId.Loggers.ILog and since both DotNetOpenId and log4net have liberal open source licenses we’ll copy ILog from log4net into DotNetOpenId and change the namespace. Although in our case the two interfaces will be identical, they need not be, and when working with less liberal licenses you probably should avoid copying the interface right out of someone else’s assembly.
namespace DotNetOpenId.Loggers { interface ILog { void Debug(object message); void DebugFormat(string format, params object[] args); bool IsDebugEnabled { get; } // many more members, skipped for brevity } }
Now we need to implement this DotNetOpenId.Loggers.ILog interface with our own class that does nothing but forward all calls to log4net. This class must be smart about handling cases when log4net.dll is missing however, so we very carefully design it thus: [Update 8/6/08: fixed bug that prevented code from working when log4net.dll was not present]
namespace DotNetOpenId.Loggers { class Log4NetLogger : ILog { private log4net.ILog log4netLogger; private Log4NetLogger(log4net.ILog logger) { log4netLogger = logger; } /// <summary> /// Returns a new log4net logger if it exists, or returns null if the assembly cannot be found. /// </summary> internal static ILog Initialize() { return isLog4NetPresent ? CreateLogger() : null; } static bool isLog4NetPresent { get { try { Assembly.Load("log4net"); return true; } catch (FileNotFoundException) { return false; } } } ////// Creates the log4net.LogManager. Call ONLY once log4net.dll is known to be present. /// static ILog CreateLogger() { return new Log4NetLogger(log4net.LogManager.GetLogger("DotNetOpenId")); } #region ILog Members public void Debug(object message) { log4netLogger.Debug(message); } public void DebugFormat(string format, params object[] args) { log4netLogger.DebugFormat(CultureInfo.InvariantCulture, format, args); } public bool IsDebugEnabled { get { return log4netLogger.IsDebugEnabled; } } // Again, many members skipped for brevity. #endregion } }
There are several critical techniques used in the Log4NetLogger class shown above.
- The Log4NetLogger class implements the DotNetOpenId.Loggers.ILog interface instead of the log4net.ILog interface, because again that would introduce an exposed reference to a log4net.dll type, which would defeat the purpose.
- All static members on the class make no reference to any types inside log4net.dll.
- The constructor is private and we use a static factory method that only allows instantiation of this class once log4net.dll is confirmed to be present, otherwise null is returned.
- Once the class is instantiated, all communication with log4net.dll types is done within the instance and not exposed outside the class. It’s ok that we have a private instance field of type log4net.ILog in our class because our class is only instantiated, and that field only gets touched, if we already know that log4net.dll is present.
At this point we have a class that provides safe access to log4net.dll when it is present, and only returns null if the referenced assembly is missing rather than having the CLR throw an assembly load exception.
Now we need another implementation of DotNetOpenId.Loggers.ILog that will be used when log4net.dll is not present. We’ll write one call the NoOpLogger that silently does nothing.
namespace DotNetOpenId.Loggers { class NoOpLogger : ILog { /// <summary> /// Returns a new logger that does nothing when invoked. /// </summary> internal static ILog Initialize() { return new NoOpLogger(); } #region ILog Members public void Debug(object message) { return; } public void DebugFormat(string format, params object[] args) { return; } public bool IsDebugEnabled { get { return false; } } // Again, many members skipped for brevity. #endregion } }
Now we need a centralized Logger class that manages the use of our ILog implementing classes so that throughout our project we can log with such a simple line as:
Logger.Debug("Some debugging log message");
From this use case, we know Logger must be a static class. Static classes cannot implement interfaces, so rather than actually implementing DotNetOpenId.Loggers.ILog, it will define all the same members as are in ILog but make them static. These static methods will forward the call on to some instance of ILog.
namespace DotNetOpenId { /// <summary> /// A general logger for the entire DotNetOpenId library. /// </summary> /// <remarks> /// Because this logger is intended for use with non-localized strings, the /// overloads that take <see cref="CultureInfo" /> have been removed, and /// <see cref="CultureInfo.InvariantCulture" /> is used implicitly. /// </remarks> static class Logger { static ILog facade = initializeFacade(); static ILog initializeFacade() { ILog result = Log4NetLogger.Initialize() ?? TraceLogger.Initialize() ?? NoOpLogger.Initialize(); return result; } #region ILog Members // Although this static class doesn't literally implement the ILog interface, // we implement (mostly) all the same methods in a static way. public static void Debug(object message) { facade.Debug(message); } public static void DebugFormat(string format, params object[] args) { facade.DebugFormat(CultureInfo.InvariantCulture, format, args); } public static bool IsDebugEnabled { get { return facade.IsDebugEnabled; } } // Again, many members skipped for brevity. #endregion } }
Here are the specific techniques used in the above Logger class:
- We have a static field that indicates the ILog instance to be used that is initialized automatically at first use of the Logger class.
- The static ILog initializeFacade() method first tries to initialize the Log4NetLogger, fails over to the TraceLogger (which we haven’t talked about yet), and finally gives up and reverts to the fail-safe NoOpLogger.
- All the members of ILog also appear as public (or internal if you choose) static members on this class, allowing extremely convenient use of whichever logger happens to be active.
- If the ?? syntax you see above is new to you, it’s a C# binary operator that returns the first operand that evaluates to non-null, or null if both are. For example, a ?? b is equivalent to (a != null) ? a : b. And it gets really powerful when you compare a ?? b ?? c to ((a != null) ? a : (b != null ? b : c)). It stacks much more elegantly than the more common ?: trinary operator as you can tell.
Why it works
At runtime, .NET only loads assemblies when they are first used. So although your library references log4net.dll, log4net.dll won’t be loaded (or noticed as missing) until the execution in your library draws very near to a call that actually requires log4net.dll to be loaded. By carefully surrounding all references to types found in log4net.dll with these forwarding classes in your library, you can avoid .NET ever trying to load log4net.dll if you know that it’s missing.
What was that TraceLogger I saw?
Well, since we have this fail-over mechanism for choosing which logger to use, it seemed a shame to fail immediately to logging nothing just because log4net.dll wasn’t present. System.Diagnostics.Trace is an adequate logging mechanism if you happen to be running in full trust so that’s worth a shot. Here’s a snippet of what TraceLogger looks like:
namespace DotNetOpenId.Loggers { class TraceLogger : ILog { TraceSwitch traceSwitch = new TraceSwitch("OpenID", "OpenID Trace Switch"); ////// Returns a new logger that uses the internal static ILog Initialize() { return isSufficientPermissionGranted ? new TraceLogger() : null; } static bool isSufficientPermissionGranted { get { PermissionSet permissions = new PermissionSet(PermissionState.None); permissions.AddPermission(new KeyContainerPermission(PermissionState.Unrestricted)); permissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); permissions.AddPermission(new RegistryPermission(PermissionState.Unrestricted)); permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.ControlEvidence | SecurityPermissionFlag.UnmanagedCode | SecurityPermissionFlag.ControlThread)); var file = new FileIOPermission(PermissionState.None); file.AllFiles = FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read; permissions.AddPermission(file); try { permissions.Demand(); return true; } catch (SecurityException) { return false; } } } #region ILog Members public void Debug(object message) { Trace.TraceInformation(message.ToString()); } public void DebugFormat(string format, params object[] args) { Trace.TraceInformation(format, args); } public bool IsDebugEnabled { get { return traceSwitch.TraceVerbose; } } // Again, many members skipped for brevity. #endregion } }class /// if sufficient CAS permissions are granted to use it, otherwise returns false. ///
The only new stuff in TraceLogger that you haven’t seen in the last two ILog classes we’ve seen so far in this post is that its success if based on whether sufficient permissions are granted for trace logging to actually succeed.
If you’re using some external reference other than a logger, you may choose to provide some other default implementation of your facade interface rather than a no-op one. This pattern is very flexible to accommodate various libraries and interfaces as you can hopefully see.
What was that second possible solution?
So I said in my opening paragraph that there are actually two solutions to this problem. The other solution is to use ILMerge. It combines two compiled managed DLLs into one. This allows you to just build your own library as if it had an external dependency, then you can merge the two DLLs together so your customers only see a single DLL. You still have to service your distribution every time the other one issues a release, but it’s not as bad as if you’d copied the other’s source code into your own and have to merge changes into your product by hand.
Great post this is exactly what I was looking to do. Thanks!
Really cool idea, thanks a lot for sharing it!