使用 Fusion 解决未引用的部分类型名称





5.00/5 (8投票s)
使用 GAC 中的部分类型名称解析 .NET 应用中未引用的类型
背景
我最近的任务是编写一个数据提取/报告框架,该框架允许客户端应用程序定义要执行的存储过程,并将结果通过电子邮件发送给相关方。使这个项目有趣的是,它要求允许用户从一组查找表、Web 服务数据和应用程序派生数据中指定参数值。在没有任何外部引用的情况下,检索 Web 服务或应用程序派生数据需要一种执行未引用程序集中的方法的机制。
本文概述了我通过部分类型名称解析允许框架代码执行未引用程序集中的方法的做法。
问题
查找表数据的预填充非常直接——从表中选择 Id
和 Value
字段——并通过简单的 ParameterLookup
表实现。但当需要用 Web 服务数据或应用程序特定数据预填充查找时,我们需要一种机制来执行客户端应用程序中的方法。在没有任何对客户端应用程序的引用时,我们需要一种机制来解析和执行客户端应用程序代码。我们实现的方法是在 ParameterLookup
表中包含一个 LookupClass
字段,让框架代码解析类型并执行 search
方法。
通常有多种方法可以让 .NET 代码解析和执行未引用类型上的方法,尽管并非所有方法都适用于所有环境。当宿主进程是 COM+ 进程时,可以使用带有部分限定类型名称(即 MyCompany.MyApplication.Search.Lookup.SomeLookup, MyCompany.MyApplication
)的 Type.GetType()
调用。但当宿主进程是 IIS 时,必须指定完全限定的类型名称(类型名称、程序集名称、版本、区域性、公钥),或者程序集必须存在于 Web 包的/bin 目录下。
我们不想在每次应用程序发布/构建时都强制客户端应用程序更新 ParameterLookup
表,因此我们需要一种只使用类型和程序集名称部分的机制。
解决方案
如果无法获得完全限定的类型名称(类型名称、程序集类型、版本、区域性和公钥),则解析未引用程序集中的类型可能是一个真正的 PITA。
例如,类型 Microsoft.Build.BuildEngine.Engine
的完全限定名称是 Microsoft.Build.BuildEngine.Engine, Microsoft.Build.Engine, Culture=neutral, Version=4.0.0.0, PublicKeyToken=b03f5f7f11d50a3a
(真够长的!)。对于相对稳定的组件,可以在配置表/文件中指定完全限定的类型名称,但对于不太稳定的组件(例如客户端应用程序中的组件),其版本号每次构建都会更改,您该怎么办?
Type.GetType()
Type.GetType()
函数可用于加载项目中已引用的程序集或位于程序集搜索路径中的程序集。
Type t = Type.GetType("MyCompany.MyApplication.Lookup.SomeLookup, MyCompany.MyApplication ");
由于框架代码不会引用该程序集,因此我们唯一的希望是该程序集位于程序集搜索路径中。虽然这种情况可能,但肯定不能保证。
这个解决方案必须被舍弃。
如果我们向 Type.GetType()
函数提供完全限定的类型名称,会怎么样?
Type t = Type.GetType("MyCompany.MyApplication.Lookup.SomeLookup,
MyCompany.MyApplication, Culture=neutral, Version=1.0.0.3245, PublicKey= 1e9a8d893e3afa78");
此调用确实有效,并且成功返回了 Type 引用。所有要求都是将此完全限定的类型传递给框架代码——这是一项相对简单的任务。唉,当程序集被重新编译时,会创建一个具有不同版本号(假设您默认使用版本号)的新组件,而调用会返回原始程序集;而不是更新后的程序集。这并不理想。
这个解决方案必须被舍弃。
我需要一种方法来解析类型,而无需指定版本号或依赖于程序集位于程序集搜索路径中。
Assembly.LoadFrom()
Assembly.LoadFrom()
方法看起来很有希望,因为我可以使用此函数加载任何位置的程序集,并遍历该程序集中定义的所有类型。
internal static Type GetTypeFromAssembly(string assemblyName, string typeName)
{
Assembly assembly = Assembly.LoadFrom(gacPath);
Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
if (type.FullName == typeName))
return type;
}
return null;
}
if(type.FullName == typeName)
条件使我们能够不必指定完全限定类型的版本、区域性或公钥属性。因此,现在我们可以加载程序集并提取适当的类型供使用。
但这会导致代码与安装实现之间的依赖关系。如果支持人员决定修改应用程序安装参数,软件可能会被安装到未知位置。我们当然可以要求软件安装到特定位置(例如“Program Files”),但这会导致 32 位/64 位问题(“Program Files”是指“Program Files”还是“Program Files (x86)”?)。
但我认为我差不多成功了。
Fusion
如果包含我们所需类型的程序集已安装到全局程序集缓存 (GAC) 中,我们可以利用 CLR 程序集管理系统来为我们加载和查找程序集。
使用 Fusion.dll 程序集(有关详细信息,请参见此链接),我们可以创建对适当 GAC(32 位或 64 位)的引用,并使用 Assembly.LoadFrom()
方法安全地加载我们的程序集。
[DllImport("fusion.dll")]
internal static extern int CreateAssemblyCache(out IAssemblyCache ppAsmCache, int reserved);
private static string GetAssemblyPath(string name)
{
if (name == null)
throw new ArgumentNullException("name");
string finalName = name;
AssemblyInfo aInfo = new AssemblyInfo();
aInfo.cchBuf = 1024; // should be fine...
aInfo.currentAssemblyPath = new String('\0', aInfo.cchBuf);
IAssemblyCache ac;
int hr = CreateAssemblyCache(out ac, 0);
if (hr >= 0)
{
hr = ac.QueryAssemblyInfo(0, finalName, ref aInfo);
if (hr < 0)
return null;
}
return aInfo.currentAssemblyPath;
}
public static Type ResolveType(string assemblyName, string typeName)
{
string gacPath = GetAssemblyPath(assemblyName);
return GetTypeFromAssembly(assemblyName, typeName);
}
仅使用类型和程序集的名称,代码将搜索适合应用程序环境(x86 / x64)的 GAC 目录,并返回对该类型的引用。有了对正确类型的引用,调用 Activator.CreateInstance()
将创建一个可用的类型供使用。
Using the Code
框架定义了一个要解析的类型必须实现的接口...
namespace FrameworkApp
{
public interface ILookup
{
string Search();
}
}
...而类实现了该接口。
public class SearchType1 : ILookup
{
public string Search()
{
return string.Format("SearchType1.Search() from version {0}",
Assembly.GetExecutingAssembly().GetName().Version);
}
}
现在,我们只需调用 TypeResolver.ResolveType()
,创建返回类型的实例(强制转换为选择的接口),最后执行所需的方法。
Type type = TypeResolver.ResolveType("ClientApp.SearchType1,ClientApp");
ILookup lookup = Activator.CreateInstance(type) as ILookup;
lookup.Search();
限制
- 必须将程序集注册到 GAC 才能使此解决方案生效
- 不会加载不兼容的 .NET 版本程序集
- 将解析为最高版本,而不是最近编译的版本
致谢
历史
- 2013 年 8 月 23 日:初始版本