使用ASP.NET MVC引擎开发插件系统

发布时间 - 2026-01-11 01:02:28    点击率:

一、前言

我心中的插件系统应该是像Nop那样(更牛的如Orchard,OSGI.NET),每个插件模块不只是一堆实现了某个业务接口的dll,然后采用反射或IOC技术来调用,而是一个完整的mvc小应用,我可以在后台控制插件的安装和禁用,目录结构如下:

生成后放在站点根目录下的Plugins文件夹中,每个插件有一个子文件夹

Plugins/Sms.AliYun/

Plugins/Sms.ManDao/

我是一个有强迫症的的懒人,我不想将生成的dll文件拷贝到bin目录。

二、要解决的问题

1.asp.net引擎默认只会加载“bin”文件夹中的dll,而我们想要的插件文件则是分散在Plugins目录下的各个子目录中。

2.视图中使用了模型时如何处理?默认情况下RazorViewEngine使用BuildManager将视图编译成动态程序集,然后使用Activator.CreateInstance实例化新编译的对象,而使用插件dll时,当前的AppDomain不知道如何解析这种引用了模型的视图,因为它不存在于“bin”或GAC中。更糟糕的是,不会收到任何错误消息,告诉您为什么它不工作,或者问题在哪。相反,他会告诉你,从View目录中找不到文件。

3.某个插件正挂在站点下运行着,直接覆盖插件的dll,会告诉你当前dll正在使用,不能被覆盖。

4.视图文件不放站点的View目录中,该如何加载。

三.Net 4.0让这一切变成可能

Net4.0有一个新特性是在应用程序初始化之前执行代码的能力(PreApplicationStartMethodAttribute),这个特性使得应用程序在Application_Star前可以做一些工作,例如我们可以在应用启动之前告知我们的mvc插件系统的dll放在哪,做预加载处理等。关于.net的几个新特性,有歪果仁写得有博客来介绍,点我。,关于PreApplicationStartMethodAttribute,有博友已经写过了,点我。 Abp的启动模块应该也是使用PreApplicationStartMethodAttribute这个特性原理来实现的,具体是不是这样还没看。

四、解决方案

1.修改主站点web.config目录,让运行时除了加载bin目录中的文件,还可以从其它目录加载

 <runtime>
 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
  <probing privatePath="Plugins/temp/" />
 </assemblyBinding>
 </runtime>

2.开发一个简易的插件管理类,这个类的作用就是在Application_Start之前就把Plugins各子目录中的dll拷贝到第1步指定的文件夹中,为了让demo尽可能简单,没有对重复的dll进行检测(比如插件中引用了ef程序集,主站点也引用了,在站点bin目录中已经存在ef的dll了,就没必要再把插件中的dll拷贝到上面设置的动态程序集目录中)

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
[assembly: PreApplicationStartMethod(typeof(Plugins.Core.PreApplicationInit), "Initialize")]
namespace Plugins.Core
{
 public class PreApplicationInit
 {

  static PreApplicationInit()
  {
   PluginFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/plugins"));
   ShadowCopyFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/plugins/temp"));
  }

  /// <summary>
  /// 插件所在目录信息
  /// </summary>
  private static readonly DirectoryInfo PluginFolder;

  /// <summary>
  /// 程序应行时指定的dll目录
  /// </summary>
  private static readonly DirectoryInfo ShadowCopyFolder;

  public static void Initialize()
  {
   Directory.CreateDirectory(ShadowCopyFolder.FullName);
   //清空插件dll运行目录中的文件
   foreach (var f in ShadowCopyFolder.GetFiles("*.dll", SearchOption.AllDirectories))
   {
    f.Delete();
   }
   foreach (var plug in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories).Where(i=>i.Directory.Parent.Name== "plugins"))
   {
    File.Copy(plug.FullName, Path.Combine(ShadowCopyFolder.FullName, plug.Name), true);
   }
   foreach (var a in
    ShadowCopyFolder
    .GetFiles("*.dll", SearchOption.AllDirectories)
    .Select(x => AssemblyName.GetAssemblyName(x.FullName))
    .Select(x => Assembly.Load(x.FullName)))
   {
    BuildManager.AddReferencedAssembly(a);
   }

  }
 }
}

3.如何让View引擎找到我们的视图呢?答案是重写RazorViewEngine的方法,我采用了约定大于配置的方式(假设我们的插件项目命名空间为Plugins.Apps.Sms,那么默认的控制器命名空间为Plugins.Apps.Sms.Controllers,插件生成后的文件夹必须为/Plugins/Plugins.Apps.Sms/),通过分析当前控制器就可以知道当前插件的View目录位置

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.WebPages.Razor;

namespace Plugins.Web
{
 public class CustomerViewEngine : RazorViewEngine
 {

  /// <summary>
  /// 定义视图页所在地址。
  /// </summary>
  private string[] _viewLocationFormats = new[]
  {
   "~/Views/Parts/{0}.cshtml",
   "~/Plugins/{pluginFolder}/Views/{1}/{0}.cshtml",
   "~/Plugins/{pluginFolder}/Views/Shared/{0}.cshtml",
   "~/Views/{1}/{0}.cshtml",
   "~/Views/Shared/{0}.cshtml",
  };
  public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
  {
   string ns = controllerContext.Controller.GetType().Namespace;
   string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");
   //说明是插件中的控制器,View目录需要单独处理
   if (ns.ToLower().Contains("plugins"))
   {
    var pluginsFolder = ns.ToLower().Replace(".controllers", "");
    ViewLocationFormats = ReplacePlaceholder(pluginsFolder);
   }
   return base.FindView(controllerContext, viewName, masterName, useCache);
  }
  /// <summary>
  /// 替换pluginFolder占位符
  /// </summary>
  /// <param name="folderName"></param>
  private string[] ReplacePlaceholder(string folderName)
  {
   string[] tempArray = new string[_viewLocationFormats.Length];
   if (_viewLocationFormats != null)
   {
    for (int i = 0; i < _viewLocationFormats.Length; i++)
    {
     tempArray[i] = _viewLocationFormats[i].Replace("{pluginFolder}", folderName);
    }
   }
   return tempArray;
  }
 }
}


然后在主站点的Global.asax中将Razor引擎指定为我们重写过的

4.开始制作一个插件目录,跟我们平时建立的MVC项目并没有太大区别,只是发布时需要做一些设置。

.生成路径要按照第3条的约定来写,不然会找不到视图文件

.View目录下的web.config和.cshtml文件要复制到生成目录(在文件中点右键)

3.设置引用项目中的生成属性,主程序下面已经有了的就把“复制到输出目录”设置为无,要不然拷贝到动态bin目录时会出错,可以对第2步中的那个类改造一下,加入文件比较功能,bin目录中没有的,才拷贝到动态bin目录中。

4.生成后的目录结构如下:

5.跑一下,一切正常,插件中的控制器工作正常,视图中引用了Model也没问题

到此,一个插件系统的核心部分就算完成了,你可继续进行扩展,增加插件的发现、安装、卸载功能,这些相对于核心功能来说,都是小儿科。后续我会基于Abp框架出一个插件系统的文章,有兴趣的把小板凳准备好,瓜子花生买上:)

五、源代码

下载Plugins链接: https://pan.baidu.com/s/1nvmbL81 密码: 85v1

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


# ASP.NET  # MVC  # 引擎  # 插件系统  # 目录中  # 加载  # 放在  # 主站  # 告诉你  # 就把  # 拷贝到  # 夹中  # 应用程序  # 的是  # 都是  # 新特性  # 几个  # 有一  # 是在  # 我不  # 还没  # 我会  # 目录下  # 还可以 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: Midjourney怎么调整光影效果_Midjourney光影调整方法【指南】  Laravel怎么进行数据库事务处理_Laravel DB Facade事务操作确保数据一致性  Laravel怎么为数据库表字段添加索引以优化查询  JavaScript如何实现继承_有哪些常用方法  如何在万网利用已有域名快速建站?  如何在阿里云购买域名并搭建网站?  JS中使用new Date(str)创建时间对象不兼容firefox和ie的解决方法(两种)  Laravel中间件如何使用_Laravel自定义中间件实现权限控制  HTML5段落标签p和br怎么选_文本排版常用标签对比【解答】  Laravel如何配置.env文件管理环境变量_Laravel环境变量使用与安全管理  Edge浏览器提示“由你的组织管理”怎么解决_去除浏览器托管提示【修复】  深圳网站制作设计招聘,关于服装设计的流行趋势,哪里的资料比较全面?  怎么用AI帮你设计一套个性化的手机App图标?  Claude怎样写结构化提示词_Claude结构化提示词写法【教程】  如何选择可靠的免备案建站服务器?  公司门户网站制作流程,华为官网怎么做?  php json中文编码为null的解决办法  ChatGPT常用指令模板大全 新手快速上手的万能Prompt合集  Windows10电脑怎么查看硬盘通电时间_Win10使用工具检测磁盘健康  香港服务器网站推广:SEO优化与外贸独立站搭建策略  javascript中闭包概念与用法深入理解  七夕网站制作视频,七夕大促活动怎么报名?  Android自定义listview布局实现上拉加载下拉刷新功能  成都网站制作公司哪家好,四川省职工服务网是做什么用?  Python文件异常处理策略_健壮性说明【指导】  Laravel如何使用Gate和Policy进行权限控制_Laravel权限判定与策略规则配置  佛山网站制作系统,佛山企业变更地址网上办理步骤?  ai格式如何转html_将AI设计稿转换为HTML页面流程【页面】  如何在建站主机中优化服务器配置?  Python3.6正式版新特性预览  php在windows下怎么调试_phpwindows环境调试操作说明【操作】  Microsoft Edge如何解决网页加载问题 Edge浏览器加载问题修复  javascript中数组(Array)对象和字符串(String)对象的常用方法总结  javascript事件捕获机制【深入分析IE和DOM中的事件模型】  专业商城网站制作公司有哪些,pi商城官网是哪个?  如何为不同团队 ID 动态生成多个“认领值班”按钮  免费的流程图制作网站有哪些,2025年教师初级职称申报网上流程?  edge浏览器无法安装扩展 edge浏览器插件安装失败【解决方法】  网站优化排名时,需要考虑哪些问题呢?  Laravel怎么判断请求类型_Laravel Request isMethod用法  Java遍历集合的三种方式  怎么制作一个起泡网,水泡粪全漏粪育肥舍冬季氨气超过25ppm,可以有哪些措施降低舍内氨气水平?  Laravel项目结构怎么组织_大型Laravel应用的最佳目录结构实践  Laravel怎么集成Vue.js_Laravel Mix配置Vue开发环境  Laravel怎么多语言本地化设置_Laravel语言包翻译与Locale动态切换【手册】  Win11怎么恢复误删照片_Win11数据恢复工具使用【推荐】  百度输入法全感官ai怎么关 百度输入法全感官皮肤关闭  Laravel如何与Pusher实现实时通信?(WebSocket示例)  做企业网站制作流程,企业网站制作基本流程有哪些?  教你用AI润色文章,让你的文字表达更专业