65.9K
CodeProject 正在变化。 阅读更多。
Home

Assembly.LoadFile 与 Assembly.LoadFrom - .NET 最精妙的晦涩之处

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (16投票s)

2009年3月19日

CPOL

3分钟阅读

viewsIcon

111459

downloadIcon

696

如何在同一时间轻松使用一个类的多个版本(无需 AppDomain!)。

引言

最近,我遇到了一个问题,需要维护一个程序集的多个版本 (DLL),并根据需要动态加载到程序中。虽然这永远不是理想的解决之道,但业务需求和系统状态决定了这种解决方案。我立即开始规划版本控制、命名、部署等策略。令我惊讶的是,我偶然发现,大多数这些都是不必要的,你可以一次性将一个程序集的多个副本加载到一个应用程序中。

背景

为了最好地描述这个问题,我将使用一个简单但足够常见的场景来准确地说明这个解决方案。假设你有一个标准的商业应用程序;包括客户、发票、产品等。有一天,你的老板走进来,说:“从现在开始,Acme 产品的价格应该使用算法 X 而不是 Y 来计算”。然后,第二天他又来了,并更改了另外三个产品的算法。这种改变没有任何理由,而且逻辑无法被编码;你只需要能够“拨动开关”并立即进行更改。哦,顺便说一下,你不能简单地在代码中放入多种算法。由于系统/架构的限制,计算库的逻辑和接口一次只能包含一种算法。(我知道,请耐心等待,我们正在进入精彩部分。)

程序集

  • Common.dll – 包含 IPriceCalculator 接口,该接口在其他库之间共享。
  • PriceCalculator.dll – 提供定价算法的具体实现。
  • MainProg.exe – 主可执行文件,用于加载产品和定价计算器。

重要的是将 IPriceCalculator 保留在其自己的 DLL 中,以便 MainProg 可以在编译时引用和使用该接口,而无需引用动态加载的实现 (PriceCalculator.dll)。

代码

此示例代码不是一个完整的“插件”解决方案,但很好地说明了核心问题和解决方案。

IPriceCalculator

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

PriceCalculator

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

MainWindow

// 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());
            }
        }
    }
}

测试

为了模拟 PriceCalculator.dll 的多个版本的部署,我首先编译了一个返回“算法版本一”的版本,并将其放置在“version1”目录中。然后我对版本二和版本三做了同样的事情。极其重要的是要意识到我没有更改 DLL 的名称、版本或程序集信息,只是更改了从 Calculate 方法返回的字符串。

Assembly.LoadFrom()

正如你所看到的,只加载了 PriceCalculator.dll 的第一个版本,随后的加载被静默地忽略了。所有产品都返回“算法版本一”。

Assembly.LoadFile()

就这样,我们能够加载 PriceCalculator 的单个版本的多个副本;每个副本都经过编译,返回不同版本的算法。

结论

如果你和我一样,那么你总是假设一个 AppDomain 中一次只能加载一个特定类型的版本。当我第一次测试这个理论时,巧合的是,我使用了 LoadFile 而不是 LoadFrom。我执行了我的测试程序,期望它失败,但……它没有失败。我认为我做错了什么,我检查了三个加载的程序集的名称、版本,然后是 GUID,它们完全相同!我决定最好从头开始,并立即转到 MSDN。在那里我发现,在方法的“备注”部分(甚至不在主要描述中)隐藏着这样一句话

“使用 LoadFile 方法加载和检查具有相同身份但位于不同路径的程序集。”

好吧,自从 1.1 版本以来,我一直专门使用 .NET,这表明总是有新的东西要学习!

© . All rights reserved.