标题:从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分问题解决方案

作者:Lamond Lu

地址:https://www.cnblogs.com/lwqlun/p/12930713.html

源代码:https://github.com/lamondlu/Mystique

前景回顾

简介

在上一篇中,我给大家讲解插件引用程序集的加载问题,在加载插件的时候,我们不仅需要加载插件的程序集,还需要加载插件引用的程序集。在上一篇写完之后,有许多小伙伴联系到我,提出了各种各样的问题,在这里谢谢大家的支持,你们就是我前进的动力。本篇呢,我就对这其中的一些主要问题进行一下汇总和解答。

如何在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的插件式开发(七) - 近期问题汇总及部分解决方案的更多相关文章

  1. 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案

    标题:从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun ...

  2. 从零开始实现ASP.NET Core MVC的插件式开发(九) - 升级.NET 5及启用预编译视图

    标题:从零开始实现ASP.NET Core MVC的插件式开发(九) - 如何启用预编译视图 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1399 ...

  3. 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图

    标题:从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图 作者:Lamond Lu 地址:http://www.cnblogs ...

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

    标题:从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11155 ...

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

    标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/112 ...

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

    标题:从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11260750. ...

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

    标题:从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除 作者:Lamond Lu 地址:https://www.cnb ...

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

    标题:从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用. 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1171 ...

  9. asp.net mvc5轻松实现插件式开发

    在研究Nopcommece项目代码的时候,发现Nop.Admin是作为独立项目开发的,但是部署的时候却是合在一起的,感觉挺好 这里把他这个部分单独抽离出来, 主要关键点: 确保你的项目是MVC5 而不 ...

随机推荐

  1. 【Python可视化】使用Pyecharts进行奥运会可视化分析~

    项目全部代码 & 数据集都可以访问我的KLab --[Pyecharts]奥运会数据集可视化分析-获取,点击Fork即可- 受疫情影响,2020东京奥运会将延期至2021年举行: 虽然延期,但 ...

  2. 数据包的抓取[tcpdump]的应用

    [root@server ~]# yum install tcpdump [root@server ~]# yum install wireshark 1.默认情况下,直接启动tcpdump将监视第一 ...

  3. Windows API 中 OVERLAPPED 结构体 初始化

    出处:https://github.com/microsoft/Windows-classic-samples/blob/1d363ff4bd17d8e20415b92e2ee989d615cc0d9 ...

  4. Scala教程之:Enumeration

    Enumeration应该算是程序语言里面比较通用的一个类型,在scala中也存在这样的类型, 我们看下Enumeration的定义: abstract class Enumeration (init ...

  5. asp.net下载大文件代码

    public void Down(string filepath, HttpResponse aResponse) { System.IO.Stream iStream = null; // Buff ...

  6. 如何创建和部署自己的EOS代币

    本文我们将弄清楚什么是EOS代币以及如何自己创建和部署EOS代币. 与以太坊相反,EOS带有即插即用的代币智能合约.以太坊拥有ERC20智能合约,EOS拥有eosio.token智能合约.Eosio. ...

  7. Xapian实战(二):core concepts

    参考资料 core concepts 正文 1. 并发性 xapian不包含任何全局变量,所以多线程编程中,在没有共享资源的情况下可以安全使用xapian.在实际操作中,由于每个线程都可以创建自己的x ...

  8. Node Mysql事务处理封装

    node回调函数的方式使得数据库事务貌似并没有像java.php那样编写简单,网上找了一些事务处理的封装并没有达到自己预期的那样简单编写,还是自己封装一个吧.封装的大体思路很简单:函数接受一个事务处理 ...

  9. Codeforces Round #561 (Div. 2) A. Silent Classroom(贪心)

    A. Silent Classroom time limit per test1 second memory limit per test256 megabytes inputstandard inp ...

  10. 图论--2-SAT--HDU/HDOJ 4115 Eliminate the Conflict

    Problem Description Conflicts are everywhere in the world, from the young to the elderly, from famil ...