;{dllPath}.old");
}
var sourceFiles = Directory.EnumerateFiles(
_pluginDirectory, "*.cs", SearchOption.AllDirectories);
var compilationOptions = new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Debug);
var references = _defaultAssemblies
.Select(assembly => assembly.Location)
.Where(path => !string.IsNullOrEmpty(path) && File.Exists(path))
.Select(path => MetadataReference.CreateFromFile(path))
.ToList();
var syntaxTrees = sourceFiles
.Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p)))
.ToList();
var compilation = CSharpCompilation.Create(_pluginName)
.WithOptions(compilationOptions)
.AddReferences(references)
.AddSyntaxTrees(syntaxTrees);
var emitResult = compilation.Emit(dllPath);
if (!emitResult.Success)
{
throw new InvalidOperationException(string.Join("\r\n",
emitResult.Diagnostics.Where(d => d.WarningLevel == 0)));
}
//return _context.LoadFromAssemblyPath(Path.GetFullPath(dllPath));
using (var stream = File.OpenRead(dllPath))
{
var assembly = _context.LoadFromStream(stream);
return assembly;
}
}
这个方法会调用 Roslyn 编译插件代码到 DLL,并使用自定义的 AssemblyLoadContext 加载编译后的 DLL。首先它需要删除原有的 DLL 文件,因为卸载程序集有延迟,原有的 DLL 文件在 Windows 系统上很可能会删除失败并提示正在使用,所以需要先重命名并在下次删除。接下来它会查找插件文件夹下的所有 C# 源代码,用 CSharpSyntaxTree
解析它们,并用 CSharpCompilation
编译,编译时引用的程序集列表是构造函数中取得的默认 AssemblyLoadContext 中的程序集列表 (包括宿主程序集,这样插件代码才可以使用 IPlugin 接口)。编译成功后会使用自定义的 AssemblyLoadContext 加载编译后的 DLL 以支持卸载。
这段代码中有两个需要注意的部分,第一个部分是 Roslyn 编译失败时不会抛出异常,编译后需要判断 emitResult.Success
并从 emitResult.Diagnostics
找到错误信息;第二个部分是加载插件程序集必须使用 AssemblyLoadContext.LoadFromStream
从内存数据加载,如果使用 AssemblyLoadContext.LoadFromAssemblyPath
那么下次从同一个路径加载时仍然会返回第一次加载的程序集,这可能是 .NET Core 3.0 的实现问题并且有可能在以后的版本修复。
PluginController.GetInstance
private IPlugin GetInstance()
{
var instance = _instance;
if (instance != null && !_changed)
return instance;
lock (_reloadLock)
{
instance = _instance;
if (instance != null && !_changed)
return instance;
UnloadPlugin();
_context = new AssemblyLoadContext(
name: $"Plugin-{_pluginName}", isCollectible: true);
var assembly = CompilePlugin();
var pluginType = assembly.GetTypes()
.First(t => typeof(IPlugin).IsAssignableFrom(t));
instance = (IPlugin)Activator.CreateInstance(pluginType);
_instance = instance;
_changed = false;
}
return instance;
}?
这个方法是获取最新插件实例的方法,如果插件实例已创建并且文件没有改变,则返回已有的实例,否则卸载原有的插件、重新编译插件、加载并生成实例。注意 AssemblyLoadContext 类型在 netstandard (包括 2.1) 中是 abstract 类型,不能直接创建,只有 netcoreapp3.0 才可以直接创建 (目前也只有 .NET Core 3.0 支持这项机制),如果需要支持可回收则创建时需要设置 isCollectible 参数为 true,因为支持可回收会让 GC 扫描对象时做一些额外的工作所以默认不启用。
PluginController.GetMessage
public string GetMessage()
{
return GetInstance().GetMessage();
}
这个方法是代理方法,会获取最新的插件实例并转发调用参数与结果,如果 IPlugin 有其他方法也可以像这个方法一样写。
PluginController.Dispose
public void Dispose()
{
UnloadPlugin();
_watcher?.Dispose();
_watcher = null;
}
这个方法支持主动释放 PluginController,会卸载已加载的插件并且停止监听插件文件。因为 PluginController 没有直接管理非托管资源,并且 AssemblyLoadContext 的析构函数 会触发卸载,所以 PluginController 不需要提供析构函数。
主函数代码
static void Main(string[] args)
{
using (var controller = new PluginController("MyPlugin", "../guest"