65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2014 年 11 月 3 日

CPOL

9分钟阅读

viewsIcon

20117

downloadIcon

641

实现一个库,以在 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 Solution

独立 MVC 应用程序

插件应作为独立的 MVC2 应用程序进行开发。它可以拥有控制器、模型和视图。需要注意的是,任何额外的项目,例如图像、JavaScript 文件、引用的库等,都必须已经存在于宿主应用程序中。最好的办法是完全避免它们,但如果不能,则必须确保插件和宿主应用程序同步。

那视图呢?

啊,是的!视图是独立文件。我们需要将它们复制到宿主应用程序吗?

不,我们不会。至少不是你想象的那种方式。

在上面详细介绍类的部分中,定义了用于处理插件请求资源的方法。视图就是其中之一。这意味着视图必须像这样设置为“嵌入式资源”:

Embedded Resource

插件程序集属性

识别插件程序集的方法是使用自定义属性。下面是 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 接口要求您创建 IndexDeactivatedPage 动作方法,但除非您添加 [PluginActionFilter] 属性,否则插件可以绕过停用功能。嗯……我们得想想办法解决这个问题。
  • 一个插件需要做很多事情才能被正确识别为插件。如果您忘记了某些东西怎么办?也许应该有一些通用测试用例来确保插件项目符合最低标准。是的,我们来研究一下。
  • 插件中唯一嵌入的资源是视图。我们还能用插件做些什么来处理其他资源,例如 JavaScript 文件和/或图像吗?不确定,但也许我们应该进一步调查。

所有这些问题都已在 ASP.NET MVC2 插件架构 第二部分:Electric Boogaloo 中得到解决。

历史

修正

本文中的代码版本有一个错误。在 ExamplePlugin 项目中,Global.asax.cs 文件中的 Application_Start 方法没有进行以下调用,因此它将无法作为独立应用程序运行。

PluginHelper.InitializePluginsAndLoadViewLocations(true)

这已在文章的第二部分中更正。

© . All rights reserved.