我有这段代码,它尝试加载文件夹中的所有程序集,然后对于每个程序集,还检查是否可以在同一文件夹中找到其所有依赖项。
private static string searchDirectory = @"C:\Users\Anon\Downloads\AssemblyLoadFilePOC_TESTFOLDER";
private static readonly List<string> errors = new List<string>();
static void Main(string[] args)
{
if (args.Length > 1)
searchDirectory = args[1];
var files = Directory
.GetFiles(searchDirectory, "*.*", SearchOption.AllDirectories)
.Where(s => s.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || s.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
foreach (var file in files)
{
try
{
AssemblyLoadHandler.Enable(Path.GetDirectoryName(file));
Assembly assembly = null;
if (File.Exists(file) && Path.GetExtension(file) == ".dll")
{
assembly = Assembly.LoadFrom(file);
}
else
{
var fileInfo = new FileInfo(file);
assembly = Assembly.LoadFile(fileInfo.FullName);
}
ValidateDependencies(Path.GetDirectoryName(file), assembly, errors);
}
catch (BadImageFormatException e)
{
errors.Add(e.Message);
}
catch (Exception ex)
{
errors.Add(ex.Message); ;
}
}
}
这是 AssemblyLoadHandler 类,其中包含 Assembly Resolve 事件的事件处理程序。
public static class AssemblyLoadHandler
{
/// <summary>
/// Indicates whether the load handler is already enabled.
/// </summary>
private static bool enabled;
/// <summary>
/// Path to search the assemblies from
/// </summary>
private static string path;
/// <summary>
/// Enables the load handler.
/// </summary>
public static void Enable(string directoryPath)
{
path = directoryPath;
if (enabled)
{
return;
}
AppDomain.CurrentDomain.AssemblyResolve += LoadAssembly;
enabled = true;
}
/// <summary>
/// A handler for the <see cref="AppDomain.AssemblyResolve"/> event.
/// </summary>
/// <param name="sender">The sender of the event.</param>
/// <param name="args">The event arguments.</param>
/// <returns>
/// A <see cref="Assembly"/> instance fot the resolved assembly, or <see langword="null" /> if the assembly wasn't found.
/// </returns>
private static Assembly LoadAssembly(object sender, ResolveEventArgs args)
{
// Load managed assemblies from the same path as this one - just take the DLL (or EXE) name from the first part of the fully qualified name.
var filePath = Path.Combine(path, args.Name.Split(',')[0]);
try
{
if (File.Exists(filePath + ".dll"))
{
return Assembly.LoadFile(filePath + ".dll");
}
}
catch (Exception)
{
}
try
{
if (File.Exists(filePath + ".exe"))
{
return Assembly.LoadFile(filePath + ".exe");
}
}
catch (Exception)
{
}
return null;
}
}
这是尝试加载程序集的所有依赖项的 ValidateDependency 方法:
private static void ValidateDependencies(string searchDirectory, Assembly assembly, List<string> errors)
{
var references = assembly.GetReferencedAssemblies();
foreach (var r in references)
{
var searchDirectoryPath = Path.Combine(searchDirectory, r.Name + ".dll");
var runtimeDirectoryPath = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), r.Name + ".dll", SearchOption.AllDirectories);
try
{
if (!File.Exists(searchDirectoryPath) && (runtimeDirectoryPath == null || runtimeDirectoryPath.Length == 0))
{
throw new FileNotFoundException("Dependency " + r.Name + " could not be found.", r.FullName);
}
else
{
Assembly foundAssembly = null;
if (File.Exists(searchDirectoryPath))
{
foundAssembly = Assembly.LoadFrom(searchDirectoryPath);
if (foundAssembly.GetName().Version != r.Version && !r.Flags.HasFlag(AssemblyNameFlags.Retargetable))
foundAssembly = null;
}
else
{
foundAssembly = Assembly.LoadFrom(runtimeDirectoryPath[0]);
}
if (foundAssembly == null)
{
throw new FileNotFoundException("Required version of dependency " + r.Name + " could not be found.", r.FullName);
}
}
}
catch (Exception e)
{
errors.Add(e.ToString());
}
}
}
为了测试,我只在测试路径中放置了 2 个程序集:
C:\Users\Anon\Downloads\AssemblyLoadFilePOC_TESTFOLDER
:
Microsoft.AspNetCore.DataProtection.dll 的程序集版本为 5.0.17.0 及其依赖项之一:Microsoft.Extensions.Logging.Abstractions.dll 具有汇编版本 5.0.0.0
请注意,Microsoft.AspNetCore.DataProtection.dll 版本 5.0.17.0 依赖于 Microsoft.Extensions.Logging.Abstractions.dll 版本 5.0.0.0
为了测试 Assembly Resolve 事件的事件处理程序是否正常工作,我在该控制台应用程序的应用程序配置中添加了此程序集绑定重定向。
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
此应用程序是一个 .NET 4.8 控制台应用程序,当我在应用程序配置中没有绑定重定向的情况下运行它时,如预期的那样,不会触发 Assembly Resolve 事件。但是,当我添加重定向时,Assembly Resolve 事件会被多次触发,应用程序最终会因 Stackoverflow 异常而崩溃。
此处引用 MSDN 文档:https://learn.microsoft.com/en-us/dotnet/standard/ assembly/resolve-loads#the- Correct-way-to-handle- assemblyresolve
从 AssemblyResolve 事件处理程序解析程序集时, 如果处理程序使用 StackOverflowException 最终将被抛出 Assembly.Load 或 AppDomain.Load 方法调用。 相反,使用 LoadFile 或 LoadFrom 方法,因为它们不会引发 AssemblyResolve 活动。
如您所见,我在事件处理程序中使用 Assembly.LoadFile 方法,但是该事件不断被触发多次。文档是否不正确?还是我做错了什么?
不,你提到的文档就很好了!请注意,在文档中,他们创建了一个名为“Test”的新 AppDomain;
AppDomain ad = AppDomain.CreateDomain("Test");
ad.AssemblyResolve += MyHandler;
如果您创建一个新的AppDomain并使用它而不是
AppDomain.CurrentDomain
,您的问题将得到解决。
但是假设由于某种原因您想要使用当前的 AppDomain。我们知道,存在不受控制的递归调用地狱,导致堆栈溢出异常。我们怎样才能摆脱它呢?简单的!只需在处理程序中加载程序集之前从
AssemblyResolve
事件中取消订阅处理程序方法即可;
private static Assembly AssemblyResolveHandler(object sender, ResolveEventArgs args)
{
AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolveHandler;
return Assembly.LoadFile("SomeAssembly.dll");
}
这是我从你的代码中得出的最小代码;
private const string SearchDirectory = @"...\AssemblyLoadFilePOC_TESTFOLDER";
private static void Main()
{
foreach (var file in Directory.GetFiles(SearchDirectory))
{
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolveHandler;
try
{
var assembly = Assembly.LoadFrom(file);
foreach (var referenceAssembly in assembly.GetReferencedAssemblies())
{
var referenceAssemblyPath = Path.Combine(SearchDirectory, referenceAssembly.Name + ".dll");
if (!File.Exists(referenceAssemblyPath))
{
continue;
}
try
{
var loadedAssembly = Assembly.LoadFrom(referenceAssemblyPath);
Console.WriteLine("Reference Assembly Version: " + referenceAssembly.Version);
Console.WriteLine("Loaded Assembly Version: " + loadedAssembly.GetName().Version);
Console.WriteLine("Is Reference Assembly Retargetable: " + referenceAssembly.Flags.HasFlag(AssemblyNameFlags.Retargetable));
}
catch (FileNotFoundException)
{
// We already check if the file exists or not in the beginning of foreach loop.
// So if we face any FileNotFoundException, it's because of version mismatch.
Console.WriteLine($"Required version of dependency {referenceAssembly.Name} could not be found.");
}
}
}
catch (FileNotFoundException)
{
// Same as above. We get files from the folder and then try to load them.
// So if we face any FileNotFoundException, it's because of version mismatch.
Console.WriteLine($"Redirected version of assembly {file} could not be found.");
}
}
Console.ReadKey(true);
}
private static Assembly AssemblyResolveHandler(object sender, ResolveEventArgs args)
{
AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolveHandler;
return Assembly.LoadFile(Path.Combine(SearchDirectory, args.Name.Split(',')[0]) + ".dll");
}
未使用程序集重定向时的输出 - 在
AssemblyResolveHandler
方法中注释掉取消订阅行没有影响;
Reference Assembly Version: 5.0.0.0
Loaded Assembly Version: 5.0.0.0
Is Reference Assembly Retargetable: False
使用程序集重定向时的输出并注释掉该行;
Process is terminated due to StackOverflowException.
使用程序集重定向并取消注释该行时的输出;
Required version of dependency Microsoft.Extensions.Logging.Abstractions could not be found.
Redirected version of assembly ...\AssemblyLoadFilePOC_TESTFOLDER\Microsoft.Extensions.Logging.Abstractions.dll could not be found.