Introduction
Recently, I encountered a problem which required multiple versions of an assembly (DLL) to be maintained and dynamically loaded into the program as needed. While this is never the ideal path to take, the business requirements and state of the system dictated this solution. I immediately started planning strategies for versioning, naming, deployment, etc. Imagine my surprise when I discovered, by accident, that most of this is unnecessary, and you can load multiple copies of an assembly into an application at once.
Background
To best describe the problem, I will use a scenario which is simple but common enough to illustrate the solution accurately. Imagine you have a standard commerce application; customers, invoices, products, etc. One day your boss comes in and says, "From now on, the price of Acme products should be calculated using algorithm X instead of Y". Then, the next day he comes and changes the algorithm used for three other products too. There is no rhyme or reason for the change, and the logic can’t be coded; you just need to be able to “flip a switch” and make the change happen immediately. Oh, and by the way, you can not just simply put multiple algorithms in code. Because of system/architecture constraints, the logic and interface of the calculation library can only contain one algorithm at a time. (I know, bear with me, we’re getting to the good stuff.)
The Assemblies
- Common.dll – Contains the
IPriceCalculator
interface that is shared among the other libraries. - PriceCalculator.dll – Provides the concrete implementation of the pricing algorithm.
- MainProg.exe – The main executable which loads the products and the pricing calculators.
It is important to keep IPriceCalculator
in its own DLL so that MainProg
can reference and use the interface at compile time without the need to reference the implementation (PriceCalculator.dll) which is loaded dynamically.
The Code
This sample code is not a complete "plug-in" solution, but illustrates the core problem and solution nicely.
IPriceCalculator
namespace TestApp.Common
{
public interface IPriceCalculator
{
string Calculate();
}
}
PriceCalculator
using TestApp.Common;
namespace TestApp.PriceCalculator
{
public class PriceCalculator : IPriceCalculator
{
public string Calculate()
{
return "algorithm version ONE";
}
}
}
MainWindow
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Windows;
using TestApp.Common;
namespace TestApp.MainProg
{
public partial class MainWindow : Window
{
private List<Product> products = null;
private Dictionary<string, IPriceCalculator> calculators = null;
public MainWindow()
{
InitializeComponent();
calculators = new Dictionary<string, IPriceCalculator>();
products = new List<Product>()
{
new Product{ ProductId=1, Name="Acme",
Calculator=GetCalculator(1) },
new Product{ ProductId=2, Name="Bravo",
Calculator=GetCalculator(2) },
new Product{ ProductId=3, Name="Charlie",
Calculator=GetCalculator(3) },
new Product{ ProductId=4, Name="Delta",
Calculator=GetCalculator(4) },
new Product{ ProductId=5, Name="Echo",
Calculator=GetCalculator(5) },
};
}
private IPriceCalculator GetCalculator(int productId)
{
IPriceCalculator retVal = null;
string versionNumber = SimulateDBLookup(productId);
FileInfo fileInfo = new FileInfo(Directory.GetCurrentDirectory()
+ @"\Version" + versionNumber + @"\PriceCalculator.dll");
if (calculators.ContainsKey(fileInfo.FullName))
{
retVal = calculators[fileInfo.FullName];
}
else if (fileInfo.Exists)
{
Assembly assem = Assembly.LoadFile(fileInfo.FullName);
Type calcType = assem.GetType(
"TestApp.PriceCalculator.PriceCalculator");
ConstructorInfo consInfo =
calcType.GetConstructor(new Type[] { });
retVal = consInfo.Invoke(null) as IPriceCalculator;
calculators.Add(fileInfo.FullName, retVal);
}
return retVal;
}
private string SimulateDBLookup(int productId)
{
string retVal = "1";
if (productId == 2 || productId == 4)
{
retVal = "2";
}
else if (productId == 3 || productId == 5)
{
retVal = "3";
}
return retVal;
}
private void GetPricesButton_Click(object sender, RoutedEventArgs e)
{
foreach (Product product in products)
{
PricesList.Items.Add(product.Name + " -> " +
product.Calculate());
}
}
}
}
The Tests
To simulate the deployment of multiple versions of PriceCalculator.dll, I first compiled a version that returned “algorithm version one” and placed it in a “version1” directory. I then did the same thing with versions two and three. It is extremely important to realize that I did not change the name, version, or assembly information of the DLL, just the string that was being returned from the Calculate
method.
Assembly.LoadFrom()
As you can see, only the first version of PriceCalculator.dll was loaded, and the subsequent loads were quietly ignored. All products returned "algorithm version one".
Assembly.LoadFile()
There it is, we were able to load multiple copies of a single version of the PriceCalculator
; each of which were compiled, returning a different version of the algorithm.
Conclusion
If you are like me, then you have always assumed only one version of a particular type can be loaded in an AppDomain at a time. When I first tested this theory, by coincidence, I used LoadFile
instead of LoadFrom
. I executed my test program expecting it to fail and… it didn’t. Thinking I did something wrong, I checked the name, version, and then the GUID of the three assemblies I loaded, and they were all exactly the same! I decided it was best if I started at the beginning and immediately went to MSDN. There I found, buried in the “Remarks” section for the method (not even in the main description), the sentence:
“Use the LoadFile
method to load and examine assemblies that have the same identity, but are located in different paths.”
Well, I have been working with .NET exclusively since version 1.1, and it just goes to show there is always something new to learn!