从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分解决方案
标题:从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分问题解决方案
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/12930713.html
源代码:https://github.com/lamondlu/Mystique

前景回顾
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图
- 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板
- 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
- 从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
- 从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除
- 从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用
简介
在上一篇中,我给大家讲解插件引用程序集的加载问题,在加载插件的时候,我们不仅需要加载插件的程序集,还需要加载插件引用的程序集。在上一篇写完之后,有许多小伙伴联系到我,提出了各种各样的问题,在这里谢谢大家的支持,你们就是我前进的动力。本篇呢,我就对这其中的一些主要问题进行一下汇总和解答。
如何在Visual Studio中以调试模式启动项目?
在所有的问题中,提到最多的问题就是如何在Visual Studio中使用调试模式启动项目的问题。当前项目在默认情况下,可以在Visual Studio中启动调试模式,但是当你尝试访问已安装插件路由时,所有的插件视图都打不开。

这里之前给出临时的解决方案是在bin\Debug\netcoreapp3.1目录中使用命令行dotnet Mystique.dll的方式启动项目。
视图找不到的原因及解决方案
这个问题的主要原因就是主站点在Visual Studio中以调试模式启动的时候,默认的Working directory是当前项目的根目录,而非bin\Debug\netcoreapp3.1目录,所以当主程序查找插件视图的时候,按照所有的内置规则,都找不到指定的视图文件, 所以就给出了The view 'xx' was not found的错误信息。
因此,这里我们要做的就是修改一下当前主站点的Working directory即可,这里我们需要将Working directory设置为当前主站点下的bin\Debug\netcoreapp3.1目录。
PS: 我在开发过程中,将.NET Core升级到了3.1版本,如果你还在使用.NET Core 2.2或者.NET Core 3.0,请将
Working directory配置为相应目录

这样当你在Visual Studio中再次以调试模式启动项目之后,就能访问到插件视图了。
随之而来的样式丢失问题
看完前面的解决方案之后,你不是已经跃跃欲试了?
但是当你启动项目之后,会心凉半截,你会发现整站的样式和Javascript脚本文件引用都丢失了。

这里的原因是主站点的默认静态资源文件都放置在项目根目录的wwwroot子目录中,但是现在我们将Working directory改为了bin\Debug\netcoreapp3.1了,在bin\Debug\netcoreapp3.1中并没有wwwroot子目录,所以在修改Working directory后,就不能正常加载静态资源文件了。
这里为了修复这个问题,我们需要对代码做两处修改。
首先呢,我们需要知道当我们使用app.UseStaticFiles()添加静态资源文件目录,并以在Visual Studio中以调试模式启动项目的时候,项目查找的默认目录是当前项目根目录中的wwwroot目录,所以这里我们需要将这个地方改为PhysicalFileProvider的实现方式,并指定当前静态资源文件所在目录是项目目录下的wwwroot目录。
其次,因为当前配置只是针对Visual Studio调试的,所以我们需要使用预编译指令#if DEBUG和`#if !DEBUG针对不同的场景进行不同的静态资源文件目录配置。
所以Configure()方法最终的修改结果如下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
#if DEBUG
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(@"G:\D1\Mystique\Mystique\wwwroot")
});
#endif
#if !DEBUG
app.UseStaticFiles();
#endif
app.MystiqueRoute();
}
在完成修改之后,重新编译项目,并以调试模式启动项目后,你就会发现,我们熟悉的界面又回来了。

如何实现插件间的消息传递?
这个问题是去年年底和衣明志大哥讨论动态插件开发的时候,衣哥提出来的功能,本身实现思路不麻烦,但是实践过程中,我却让AssemblyLoadContext给绊了一跤。
基本思路
这里因为需要实现两个不同插件的消息通信,最简单的方式是使用消息注册订阅。
PS: 使用第三方消息队列也是一种实现方式,但是本次实践中只是为了简单,没有使用额外的消息注册订阅组件,直接使用了进程内的消息注册订阅

基本思路:
- 定义
INotificationHandler接口来处理消息 - 在每个独立组件中,我们通过
INotificationProvider接口向主程序公开当前组件订阅的消息及处理程序 - 在主站点中,我们通过
INotificationRegister接口实现一个消息注册订阅容器,当站点启动,系统可以通过每个组件的INotificationProvider接口实现,将订阅的消息和处理程序注册到主站点的消息发布订阅容器中。 - 每个插件中,使用
INotifcationRegister接口的Publish方法发布消息
根据以上思路,我们首先定义一个消息处理接口INotification
public interface INotificationHandler
{
void Handle(string data);
}
这里我没有采用强类型的来规范消息的格式,主要原因是如果使用强类型定义消息,不同的插件势必都要引用一个存放强类型强类型消息定义的的程序集,这样会增加插件之间的耦合度,每个插件就开发起来变得不那么独立了。
PS: 以上设计只是个人喜好,如果你喜欢使用强类型也完全没有问题。
接下来,我们再来定义消息发布订阅接口以及消息处理程序接口
public interface INotificationProvider
{
Dictionary<string, List<INotificationHandler>> GetNotifications();
}
public interface INotificationRegister
{
void Subscribe(string eventName, INotificationHandler handler);
void Publish(string eventName, string data);
}
这里代码非常的简单,INotificationProvider接口提供一个消息处理器的集合,INotificationRegister接口定义了消息订阅和发布的方法。
下面我们在Mystique.Core.Mvc项目中完成INotificationRegister的接口实现。
public class NotificationRegister : INotificationRegister
{
private static Dictionary<string, List<INotificationHandler>>
_containers = new Dictionary<string, List<INotificationHandler>>();
public void Publish(string eventName, string data)
{
if (_containers.ContainsKey(eventName))
{
foreach (var item in _containers[eventName])
{
item.Handle(data);
}
}
}
public void Subscribe(string eventName, INotificationHandler handler)
{
if (_containers.ContainsKey(eventName))
{
_containers[eventName].Add(handler);
}
else
{
_containers[eventName] = new List<INotificationHandler>() { handler };
}
}
}
最后,我们还需要在项目启动方法MystiqueSetup中配置消息订阅器的发现和绑定。
public static void MystiqueSetup(this IServiceCollection services,
IConfiguration configuration)
{
...
using (IServiceScope scope = provider.CreateScope())
{
...
foreach (ViewModels.PluginListItemViewModel plugin in allEnabledPlugins)
{
...
using (FileStream fs = new FileStream(filePath, FileMode.Open))
{
...
var providers = assembly.GetExportedTypes()
.Where(p => p.GetInterfaces()
.Any(x => x.Name == "INotificationProvider"));
if (providers != null && providers.Count() > 0)
{
var register = scope.ServiceProvider
.GetService<INotificationRegister>();
foreach (var p in providers)
{
var obj = (INotificationProvider)assembly
.CreateInstance(p.FullName);
var result = obj.GetNotifications();
foreach (var item in result)
{
foreach (var i in item.Value)
{
register.Subscribe(item.Key, i);
}
}
}
}
}
}
}
...
}
完成以上基础设置之后,我们就可以尝试在插件中发布订阅消息了。
首先这里我们在DemoPlugin2中创建消息LoadHelloWorldEvent,并创建对应的消息处理器LoadHelloWorldEventHandler.
public class NotificationProvider : INotificationProvider
{
public Dictionary<string, List<INotificationHandler>> GetNotifications()
{
var handlers = new List<INotificationHandler> { new LoadHelloWorldEventHandler() };
var result = new Dictionary<string, List<INotificationHandler>>();
result.Add("LoadHelloWorldEvent", handlers);
return result;
}
}
public class LoadHelloWorldEventHandler : INotificationHandler
{
public void Handle(string data)
{
Console.WriteLine("Plugin2 handled hello world events." + data);
}
}
public class LoadHelloWorldEvent
{
public string Str { get; set; }
}
然后我们修改DemoPlugin1的HelloWorld方法,在返回视图之前,发布一个LoadHelloWorldEvent的消息。
[Area("DemoPlugin1")]
public class Plugin1Controller : Controller
{
private INotificationRegister _notificationRegister;
public Plugin1Controller(INotificationRegister notificationRegister)
{
_notificationRegister = notificationRegister;
}
[Page("Plugin One")]
[HttpGet]
public IActionResult HelloWorld()
{
string content = new Demo().SayHello();
ViewBag.Content = content + "; Plugin2 triggered";
_notificationRegister.Publish("LoadHelloWorldEvent", JsonConvert.SerializeObject(new LoadHelloWorldEvent() { Str = "Hello World" }));
return View();
}
}
public class LoadHelloWorldEvent
{
public string Str { get; set; }
}
AssemblyLoadContext产生的灵异问题
上面的代码看起来很美好,但是实际运行的时候,你会遇到一个灵异的问题,就是系统不能将DemoPlugin2中的NotificationProvider转换为INotificationProvider接口类型的对象。

这个问题困扰了我半天,完全想象不出可能的问题,但是我隐约感觉这是一个AssemblyLoadContext引起的问题。
在上一篇中,我们曾经查找过.NET Core的程序集加载设计文档。
在.NET Core的设计文档中,对于程序集加载有这样一段描述
If the assembly was already present in A1's context, either because we had successfully loaded it earlier, or because we failed to load it for some reason, we return the corresponding status (and assembly reference for the success case).
However, if C1 was not found in A1's context, the Load method override in A1's context is invoked.
- For Custom LoadContext, this override is an opportunity to load an assembly before the fallback (see below) to Default LoadContext is attempted to resolve the load.
- For Default LoadContext, this override always returns null since Default Context cannot override itself.
这里简单来说,意思就是当在一个自定义LoadContext中加载程序集的时候,如果找不到这个程序集,程序会自动去默认LoadContext中查找,如果默认LoadContext中都找不到,就会返回null。
这里我突然想到会不会是因为DemoPlugin1、DemoPlugin2以及主站点的AssemblyLoadContext都加载了Mystique.Core.dll程序集的缘故,虽然他们加载的是同一个程序集,但是因为LoadContext不同,所以系统认为它是2个程序集。
PS: 主站点的
AssemblyLoadContext即默认的LoadContext
其实对于DemoPlugin1和DemoPlugin2来说,它们完全没有必须要加载Mystique.Core.dll程序集,因为主站点的默认LoadContext已经加载了此程序集,所以当DemoPlugin1和DemoPlugin2使用Mystique.Core.dll程序集中定义的INotificationProvider时,就会去默认的LoadContext中加载,这样他们加载的程序集就都是默认LoadContext中的了,就不存在差异了。
于是根据这个思路,我修改了一下插件程序集加载部分的代码,将Mystique.Core.*程序集排除在加载列表中。

重新启动项目之后,项目正常运行,消息发布订阅能正常运行。

项目后续尝试添加的功能
由于篇幅问题,剩余的其他问题和功能会在下一篇中来完成。以下是项目后续会逐步添加的功能
- 添加/移除插件后,主站点导航栏自动加载插件入口页面(已完成,下一篇中说明)
- 在主站点中,添加页面管理模块
- 尝试一个页面加载多个插件,当前的插件只能实现一个插件一个页面。
不过如果大家如果有什么其他想法,也可以给我留言或者在Github上提Issue,你们的建议就是我进步的动力。
总结
本篇针对前一阵子Github Issue和文档评论中比较集中的问题进行了说明和解答,主要讲解了如何在Visual Studio中调试运行插件以及如何实现插件间的消息传输。后续我会根据反馈,继续添加新内容,大家敬请期待。
从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分解决方案的更多相关文章
- 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案
标题:从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun ...
- 从零开始实现ASP.NET Core MVC的插件式开发(九) - 升级.NET 5及启用预编译视图
标题:从零开始实现ASP.NET Core MVC的插件式开发(九) - 如何启用预编译视图 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1399 ...
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图
标题:从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图 作者:Lamond Lu 地址:http://www.cnblogs ...
- 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板
标题:从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11155 ...
- 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/112 ...
- 从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
标题:从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11260750. ...
- 从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级
标题:从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除 作者:Lamond Lu 地址:https://www.cnb ...
- 从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用
标题:从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用. 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1171 ...
- asp.net mvc5轻松实现插件式开发
在研究Nopcommece项目代码的时候,发现Nop.Admin是作为独立项目开发的,但是部署的时候却是合在一起的,感觉挺好 这里把他这个部分单独抽离出来, 主要关键点: 确保你的项目是MVC5 而不 ...
随机推荐
- (转)对 Linux 专家非常有用的 20 个命令
谢谢你你给了我们在这篇文章前两个部分的喜欢,美言和支持.在第一部分文章中我们讨论了那些都只是切换到 Linux 和linux新手所需的必要知识的用户的命令. 对 Linux 新手非常有用的 20 个命 ...
- RxJava--Buffer,GroupBy 对比
Buffer 设定收集n个元素为一组,以下方代码为例,三个为一组,则当组满三个元素时,返回一次List数据 没组满三个元素时,如果调用onComplete,直接发送剩余元素,没调用onComplete ...
- 深入认识CSS的块级元素
2019独角兽企业重金招聘Python工程师标准>>> 块级元素: 显示在一块内,会自动换行,元素会从上到下垂直排列,各自占一行.每个块级元素默认占一行高度,一行内添加一个块级元素后 ...
- 关于SPFA Bellman-Ford Dijkstra Floyd BFS最短路的共同点与区别
关于模板什么的还有算法的具体介绍 戳我 这里我们只做所有最短路的具体分析. 那么同是求解最短路,这些算法到底有什么区别和联系: 对于BFS来说,他没有松弛操作,他的理论思想是从每一点做树形便利,那么时 ...
- predixy源码学习
Predixy是一个代理,代理本质上就是用来转发请求的.其主要功能就是接收客户端的请求,然后把客户端请求转发给redis服务端,在redis服务端处理完消息请求后,接收它的响应,并把这个响应返回给客户 ...
- 5) ModelSerializer(重点) 基表 测试脚本 多表关系建外键 正反查 级联 插拔式连表 序列化反序列化整合 增删查 封装response
一.前戏要做好 配置:settings.py #注册drf INSTALLED_APPS = [ # ... 'api.apps.ApiConfig', 'rest_framework', ] # ...
- MySQL 8.0.20 源码安装数据库软件
官方支持的平台: https://www.mysql.com/support/supportedplatforms/database.html
- python操作ansible api示例
#!/usr/bin/env python # -*- coding:utf-8 -*- import json import shutil from collections import named ...
- msf的rpc和json-rpc,我该选择哪个?
msf的rpc有两种调用方式,那么我们应该调用哪一个呢? 其中restful接口暂且不谈,这个rest api其实是简单对接了一下msf的后端数据库,这个自己也能读数据库来做,这个以后有时间再谈 首先 ...
- E - No Pain No Game 线段树 离线处理 区间排序
E - No Pain No Game HDU - 4630 这个题目很好,以后可以再写写.这个题目就是线段树的离线写法,推荐一个博客:https://blog.csdn.net/u01003321 ...