ASP.NET MVC2 插件架构 第一部分






4.67/5 (5投票s)
实现一个库,以在 ASP.NET MVC2 应用程序中启用插件架构。
引言
我支持的其中一个应用程序是一个庞大、单片式的 Web 应用程序,对我们公司来说至关重要。有时,我们有多个团队为其开发,并且必须经历分支/合并过程来管理所有内容。由于构成应用程序的代码、项目和其他工件数量庞大,这并不容易做到。有一天,当我有时间思考时,我开始想……
“如果这个应用程序像一个 CMS 一样,可以通过开发单独的模块来添加功能,那该有多好!”
我的 ASP.NET MVC 应用程序插件架构之旅由此开始。
背景
插件架构的概念并非新鲜事物,并且有诸如 MEF 之类的技术支持。我面临的挑战是,我必须坚持使用现有的、不可否认已经过时的技术。事实上,这里列出了此插件架构实现的要求和限制。
- 当前平台是 .NET 3.5, ASP.NET MVC2
- 易于开发和测试
- 可以有单独的 Visual Studio 解决方案/项目
- 可以作为独立应用程序存在
- 无需重启/重新部署主应用程序即可加载
- 提供将信息传递给宿主应用程序的方法
- 可以通过管理界面支持激活/停用
在谷歌搜索之后,我偶然发现了 Justin Slattery 在 FzySqr 上发表的一篇文章,他在其中逐步描述了他如何创建一个插件库,这正是我所需要的基础。您可以访问此处了解详细信息的优秀概述,我将在本文的其余部分重点介绍主要内容。
本文提供了三个基于 ASP.NET 2.0、MVC2 和 .NET 3.5 的 Visual Studio 2010 解决方案。请将这些解决方案解压缩到同一位置以便正确使用。
代码
MvcPluginLib
MvcPluginLib
实现了插件魔术。它被“宿主”应用程序和“插件”用于相互通信和协作。关键类和方法包括:
AssemblyResourceProvider
这个类继承自System.Web.Hosting.VirtualPathProvider
,允许插件请求由其程序集提供的资源,而不是基类VirtualPathProvider
使用的常规方法。
IsAppResourcePath
- 类使用的private
方法,用于判断所请求的资源是由插件还是宿主应用程序提供。如果虚拟路径包含“~/Plugins/
”,则表示该资源属于插件。FileExists
- 允许检查插件资源是否存在的方法重写。GetFile
- 允许检索插件资源的方法重写。GetCacheDependency
- 豁免插件资源不受基类VirtualPathProvider
缓存机制影响的方法重写。
AssemblyResourceVirtualFile
这个类继承自System.Web.Hosting.VirtualFile
,并允许插件从各自的程序集中提取资源(文件)。
AssemblyResourceVirtualFile
- 实例方法,将请求的资源路径存储到成员变量中以供后续使用。Open
- 重写方法,用于返回指向嵌入在插件程序集中的请求资源的流
。
IPlugin
此接口定义了插件所需的动作方法。
Index
- 所有插件都必须提供一个名为Index
的默认动作。DeactivatedPage
- 所有插件都必须提供一个显示插件已停用状态的页面。
PluginActionFilter
此类的作用是拦截 MVC 对执行动作的调用,以便插件可以在执行前检查其状态。
OnActionExecuting
- 此重写方法将在执行其Index
动作之前检查插件的状态。如果插件已激活,则将执行Index
动作。如果插件未激活,它将重定向到插件的DeactivatedPage
动作。
[注意:为了使其正常工作,插件必须在其Index
动作方法中添加[PluginActionFilter]
属性。]
MvcPluginViewLocations
此(在 PluginAttribute.cs 文件中定义的)类用于存储来自插件的自定义属性信息。更多内容将在下面的 PluginExample
部分中讨论。
PluginHelper
这个类用于从插件程序集中提取属性信息。
InitializePluginsAndLoadViewLocations
- 此方法遍历已加载的插件程序集,从中提取视图信息并将其提供给插件视图引擎类 (PluginViewEngine
)。它还将插件注册到PluginManager
并将其默认状态设置为作为参数传入的值。GetPluginActions
- 从插件程序集中检索动作/链接信息。GetPluginAssemblies
- 返回已加载的插件程序集列表。它们通过检查是否为typeof(MvcPluginViewLocations)
来确定。
PluginAction
这是一个用于存储插件动作信息的类。更多内容将在下面的 PluginExample
部分中讨论。
PluginManager
这个 static
类包含用于注册和管理插件状态的方法。
PluginStatus
- 一个包含插件状态信息的类。PluginList
- 一个维护已注册插件及其状态列表的属性。RegisterPlugin
- 通过将其添加到PluginList
并设置其激活成员变量来注册插件。GetPluginStatus
- 返回指定插件的激活状态。SetPluginStatus
- 设置指定插件的激活状态。
PluginViewEngine
此类继承自 System.Web.Mvc.WebFormViewEngine
,并拦截对基类 WebFormViewEngine
的调用以启用插件功能。
PluginViewEngine
- 创建视图引擎实例并添加传入的额外视图位置。IsAppResourcePath
- 功能与上面AssemblyResourceProvider
中定义的功能相同。FileExists
- 允许检查插件资源是否存在的方法重写。这会重写WebFormViewEngine
中的方法。
PluginExample
PluginExample
项目展示了如何开发插件。以下是需要注意的几个关键点:
独立 MVC 应用程序
插件应作为独立的 MVC2 应用程序进行开发。它可以拥有控制器、模型和视图。需要注意的是,任何额外的项目,例如图像、JavaScript 文件、引用的库等,都必须已经存在于宿主应用程序中。最好的办法是完全避免它们,但如果不能,则必须确保插件和宿主应用程序同步。
那视图呢?
啊,是的!视图是独立文件。我们需要将它们复制到宿主应用程序吗?
不,我们不会。至少不是你想象的那种方式。
在上面详细介绍类的部分中,定义了用于处理插件请求资源的方法。视图就是其中之一。这意味着视图必须像这样设置为“嵌入式资源”:
插件程序集属性
识别插件程序集的方法是使用自定义属性。下面是 AssemblyInfo.cs 文件中定义的自定义属性。
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
....
assembly: MvcPluginLib.MvcPluginViewLocations(
new string[]
{ "~/Plugins/PluginExample.dll/PluginExample.Views.{1}.{0}.aspx" },//View
//imbedded as a resource
true,//Add to "Dynamic Links" for plugin
action = "Index",//Action that will be called
controller = "SamplePlugin", //Controller that will be called
name = "SamplePlugin")]
MvcPluginViewLocations
- 如上所述,此类包含自定义属性。此信息让宿主应用程序知道这是一个插件以及如何链接到它。属性定义如下:
viewLocations
- 这是一个插件所需的视图位置数组。addLink
- 指定是否应将此链接添加到宿主应用程序controller
- 插件的控制器名称action
- 插件的控制器动作name
- 用于为此插件构建链接的名称
PluginHostExample
PluginHostExample
项目演示了宿主应用程序如何访问和管理插件。
是的,它使用了框架,别评判!
加载插件
要部署您的插件,只需将插件项目中的 .dll 文件(在此例中为 PluginExample.dll)复制到宿主应用程序的 bin 文件夹中。插件在 Application_Start
事件中通过注册 AssemblyResourceProvider
并调用 PluginHelper 的 InitializePluginsAndLoadViewLocations
方法来加载。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
//NOTE: Use the AssemblyResourceProvider of the MvcPluginLib library.
HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResourceProvider());
//NOTE: Set the view locations for the loaded plugin assemblies.
// Initialize all plugins to have an active status of false.
PluginHelper.InitializePluginsAndLoadViewLocations(false);
RegisterRoutes(RouteTable.Routes);
}
控制器冲突
由于插件旨在作为独立的 MVC 应用程序运行,除非您进行一项小改动,否则它将无法与宿主应用程序良好协作。MVC 应用程序的默认路由通常设置如下:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index",
id = UrlParameter.Optional } // Parameter defaults
);
当您的插件程序集与宿主应用程序的程序集一起可用时,意味着 MVC 无法确定使用哪个来处理路由,因此您将得到:
发现多个与控制器名称“Home”匹配的类型。
要解决此问题,请在路由定义中指定宿主应用程序的命名空间,如下所示...
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index",
id = UrlParameter.Optional }, // Parameter defaults
new string[] { "PluginHostExample.Controllers" }
);
Web.config'主义
我在宿主应用程序中遇到的另一个问题是,典型的 MVC 项目会在 Views 文件夹中放置一个 web.config 文件,其中包含有关处理强类型 .aspx 视图的重要信息。由于插件视图来自资源,它们只能访问应用程序的主 web.config,因此除非您向其中添加一些额外信息,否则您将收到以下错误:
无法加载类型 'System.Web.Mvc.ViewPage<PluginExample.Models.SamplePluginModel>'。
要解决此问题,请在宿主应用程序主 web.config 文件中的 <pages>
标签中添加以下属性。
<pages
validateRequest="false"
pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter,
System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35"
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
参考文献
思考
这是一个不错的开始,但还有一些事情困扰着我,例如……
IPlugin
接口要求您创建Index
和DeactivatedPage
动作方法,但除非您添加[PluginActionFilter]
属性,否则插件可以绕过停用功能。嗯……我们得想想办法解决这个问题。- 一个插件需要做很多事情才能被正确识别为插件。如果您忘记了某些东西怎么办?也许应该有一些通用测试用例来确保插件项目符合最低标准。是的,我们来研究一下。
- 插件中唯一嵌入的资源是视图。我们还能用插件做些什么来处理其他资源,例如 JavaScript 文件和/或图像吗?不确定,但也许我们应该进一步调查。
所有这些问题都已在 ASP.NET MVC2 插件架构 第二部分:Electric Boogaloo 中得到解决。
历史
- 首次发布日期:2014 年 11 月 3 日。
- 文章第二部分链接和修正:2015 年 1 月 20 日。
修正
本文中的代码版本有一个错误。在 ExamplePlugin
项目中,Global.asax.cs 文件中的 Application_Start
方法没有进行以下调用,因此它将无法作为独立应用程序运行。
PluginHelper.InitializePluginsAndLoadViewLocations(true)
这已在文章的第二部分中更正。