Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Assembly.LoadFile versus Assembly.LoadFrom - .NET obscurity at its finest

4.67/5 (17 votes)
19 Mar 2009CPOL3 min read 110.7K   684  
How to easily use multiple versions of a class at the same time (without AppDomains!).

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

C#
// Located in Common.dll
namespace TestApp.Common
{
    public interface IPriceCalculator
    {
        string Calculate();
    }
}

PriceCalculator

C#
// Located in PriceCalculator.dll
using TestApp.Common;
namespace TestApp.PriceCalculator
{
    public class PriceCalculator : IPriceCalculator
    {
        public string Calculate()
        {
            return "algorithm version ONE";
        }
    }
}

MainWindow

C#
// Located in MainProg.exe
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
    {
        // Holds our test products
        private List<Product> products = null;
        // Used to cache PriceCalculators to avoid loading
        // the same physical file twice
        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) },
            };
        }

        // Finds the proper version of PriceCalculator.dll
        // on disk and loads it
        private IPriceCalculator GetCalculator(int productId)
        {
            IPriceCalculator retVal = null;

            // The mapping between product Id and PriceCalculator version
            // could be stored in a database to make it hot-swappable
            string versionNumber = SimulateDBLookup(productId);
            FileInfo fileInfo = new FileInfo(Directory.GetCurrentDirectory()
            + @"\Version" + versionNumber + @"\PriceCalculator.dll");
           
            // Check to see if we have already loaded the file
            if (calculators.ContainsKey(fileInfo.FullName))
            {
                retVal = calculators[fileInfo.FullName];
            }
            else if (fileInfo.Exists)
            {
                // Load the assembly file from disk
                // Example 1 uses LoadFrom; Example 2 uses LoadFile
                Assembly assem = Assembly.LoadFile(fileInfo.FullName);

                // Find the PriceCalculator Type 
                Type calcType = assem.GetType(
                    "TestApp.PriceCalculator.PriceCalculator");

                // Get the default contructor
                ConstructorInfo consInfo =
                             calcType.GetConstructor(new Type[] { });

                // Invoke the constructor and store the reference
                retVal = consInfo.Invoke(null) as IPriceCalculator;
                calculators.Add(fileInfo.FullName, retVal);
            }
            return retVal;
        }

        // This mapping would normally be done in a database
        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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)