从零开始实现 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 DEBUGapp.UseStaticFiles(new StaticFileOptions{FileProvider = new PhysicalFileProvider(@"G:\D1\Mystique\Mystique\wwwroot")});
#endif#if !DEBUGapp.UseStaticFiles();
#endifapp.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 的插件式开发(一) - 使用 Application Part 动态加载控制器和视图

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(二) - 如何创建项目模板

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(三) - 如何在运行时启用组件

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(四) - 插件安装

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(五) - 使用 AssemblyLoadContext 实现插件的升级和删除

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(六) - 如何加载插件引用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/309699.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++实现顺序串(完整代码)

代码如下: #include<iostream> #include <cstring> #define _CRT_SECURE_NO_WARNINGS using namespace std;class String { public:String(){size 0;str new char[size 1];str[0] \0;}String(const String &obj){size obj.size;str new char[size 1];i…

投影仪硬件边缘融合服务器,带你了解投影融合的边缘融合显示技术

原标题&#xff1a;带你了解投影融合的边缘融合显示技术边缘融合显示系统是一个专业、复杂的视屏显示系统。在设计组建的时候务必考虑周密&#xff0c;消除各类不良因素。因为边缘融合系统建设具有相关器材多、系统连接复杂、易受环境因素干扰的特性&#xff0c;所以如果没有在…

Sql Server之旅——第六站 为什么都说状态少的字段不能建索引

我们在学sqlserver的时候&#xff0c;大多教科书和前辈们都说状态少的字段不要建索引&#xff0c;由此带来的开销还不如不建索引&#xff0c;但是这句话有多少人真的知道&#xff0c;或者说有多少人真的对此有比较深刻的理解&#xff0c;而不是听别人道听途说。。。这样记得快&…

概率论复习题+部分详解

概率论与数理统计练习题 1.假设检验中&#xff0c;显著性水平α\alphaα 限制&#xff08;第一类错误&#xff08;拒真错误&#xff09;#&#xff09;的概率 分析&#xff1a; &#xff08;1&#xff09;&#xff0e;原假设为真时拒绝原假设的概率不超过α &#xff08;2&…

【壹刊】Azure AD B2C(一)初识

一&#xff0c;引言&#xff08;上节回顾&#xff09;上一节讲到Azure AD的一些基础概念&#xff0c;以及如何运用 Azure AD 包含API资源&#xff0c;Azure AD 是微软提供的云端的身份标识和资源访问服务&#xff0c;帮助员工/用户/管理员访问一些外部资源和内部资源&#xff1…

英语期末复习unit 3-4课后习题第一题及背诵段落

unit 3 背诵段落&#xff1a; 2 When a recent college graduate came into my office not too long ago looking for a sales job, I asked him what he had done to prepare for the interview. He said he’d read something about us somewhere. 不久前一个新近毕业的大…

操作系统知识点总结+最终版

1、测试题要搞明白 点击可得测试题详解 2、操作系统的四个基本特征&#xff0c;基本功能 操作系统的目标:方便性、有效性、可扩充性、开放性。 操作系统的四大基本特征&#xff1a;1、并发2、共享3、虚拟4、异步&#xff1b; 操作系统的五大功能分别是处理器管理、存储器管理…

如何看云服务器性能,从存储速度看云服务器性能测试

阿 贝云提供免 费云服务器、免 费云虚拟主机&#xff0c;大家有兴趣的可以看看&#xff0c;物超所值喔。衡量存储性能一般看吞吐量(传输速度)和IOPS两个指标。吞吐量主要指大文件的连续读写速度&#xff0c;在大文件的复制、备份等场景适用&#xff0c;用“HD Tune专业版”中的…

操作系统复习题

一、填空题 1&#xff0e;通常所说操作系统的四大模块是指处理机管理、存储管理、设备管理、文件 管理。 2&#xff0e;进程实体是由 进程控制块&#xff08;PCB&#xff09; 、程序段和数据段这三部分组成。 3&#xff0e;文件系统中&#xff0c;空闲存储空间的管理方法有空…

Polly:提升服务可用性

Polly是.NET生态非常著名的组件包一 介绍Polly 的能力• 失败重试&#xff1a;当我们服务调用失败时&#xff0c;能够自动的重试• 服务熔断&#xff1a;当我们服务部分不可用时&#xff0c;我们的应用可以快速响应一个熔断结果&#xff0c;避免持续的请求这些不可用的服务而导…

[汇编语言]实验一:查看CPU和内存,用机器指令和汇编指令编程。

实验一 实验任务: 查看CPU和内存&#xff0c;用机器指令和汇编指令编程。 实验内容: &#xff08;1&#xff09;实验代码: 开始执行命令: &#xff08;2&#xff09;实验代码: &#xff08;3&#xff09;实验代码: 找到了,日期为:01/01/92&#xff0c;这个是虚拟机dos环境(因…

为自己而活,这很难吗?

上周&#xff0c;我的朋友圈被 #翼装飞行失联女生死亡事件# 刷屏了&#xff0c;不知道你有没有被刷到&#xff1f;什么&#xff1f;你不知道这件事&#xff1f;没事&#xff0c;我来简单叙述一下。大致是说一个24岁女大学生翼装飞行员&#xff0c;在张家界天门山景区的一次翼装…

基于 abp vNext 和 .NET Core 开发博客项目 - 异常处理和日志记录

在开始之前&#xff0c;我们实现一个之前的遗留问题&#xff0c;这个问题是有人在GitHub Issues(https://github.com/Meowv/Blog/issues/8)上提出来的&#xff0c;就是当我们对Swagger进行分组&#xff0c;实现IDocumentFilter接口添加了文档描述信息后&#xff0c;切换分组时会…

操作系统复习题+最终版

一、单选题 1、在单处理器系统中&#xff0c;如果同时存在9个进程&#xff0c;则处于就绪队列中的进程最多有&#xff08;8&#xff09;个。 A.1 B.9 C.10 D.8 分析&#xff1a;不可能出现这样一种情况&#xff0c;单处理器系统9个进程都处于就绪状态&#xff0c;但是8个处于…