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






4.67/5 (16投票s)
如何在同一时间轻松使用一个类的多个版本(无需 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,这表明总是有新的东西要学习!