Asp.Net MVC 插件化开发简化方案

简介:

Web 管理系统可以庞大到不可想像的地方,如果想就在一个 Asp.Net MVC 项目中完成开发,这个工程将会变得非常庞大,协作起来也会比较困难。为了解决这个问题,Asp.Net MVC 引入了 Areas 的概念,将模块划分到 Area 中去——然而 Area 仍然是主项目的一部分,多人协作的时候仍然很容易造成 .csproj 项目文件的冲突。

对于这类系统,比较好的解决办法是采用 SOA 的方式,把一个大的 Web 系统划分成若干微服务,通过一个含授权中心的 Web 集散框架组织起来。不过这里我要讲的是另一种方法,插件化的开发方案。

完整的插件化开发会涉及到插件管理的方方面面,甚至还包括插件的热插拔处理——当然这些都是可以做到的——但今天我要说的是一个简化方案,只是将业务模块当作插件在单独的项目中开发,而后在发布的时候仍然以 Area 的形式集成到主 Web 项目当中。严格的说,这并不是插件化,而只是模块化,但它是插件化的第一步。

第 1 个实验

第一个实验的目的是为了把 Area 剥离出来作为单独的项目开发。所以先使用同样版本的 .NET Framework 的 Asp.Net MVC Framework 创建两个项目,这里我们选用了

  • .NET Framework 4.6

  • Microsoft.AspNet.Mvc 5.2.3

建立两个 MVC 项目,分别名为 PluginWebApp 和 Plugin1

wKioL1msBN_AklbEAABIogdYaws147.png

PluginWebApp 项目

这个项目作为 Web 主项目,现在暂时不改它。但要检查一下 Global.asax.cs 中,Application_Start 事件中有这么一句:

1
2
3
4
5
protected  void  Application_Start()
{
     AreaRegistration.RegisterAllAreas();
     // ....
}

这是在注册所有 Area。虽然现在 PluginWebApp 并没有建 Area,但是这句话对于我们来说是必不可少的。

Plugin1 项目

这是作为插件的项目,我们把它当作一个 Area 来开发。所以先添加 Area。

操作:在“解决方案资源管理器”中“Plugin1”项目中点击右键,选择“添加→区域(A)”,输入 Plugin1 为作 Area 名称

这样,Plugin1 项目中就存在一个 Areas 目录以及其目录 Plugin1,再把这个项目中除 Areas 目录、packages.config 和 Web.config 之外的所有其它目录和文件删除,之后整个项目看起来就像这样:

wKiom1msBRDycyjGAAA3781DghA650.png

注意项目中存在一个 Plugin1AreaRegistration.cs 文件,在向 Web 应用中注册 Area 的时候需要它。

现在在 Controllers 目录下面添加控制器 TestController,相应的在 Views 下面添加 Test/Index.cshtml 视图文件。内容都不重要,只要能识别出来就行,所以在 Test/Index.cshtml 中修改 <h2> 中的内容为

1
< h2 >Testing Page Index</ h2 >

准备运行

AreaRegistration.RegisterAllAreas() 会在加载的 Assembly 中查找所有 Area 定义(AreaRegistration 的子类),完成 Area 的注册。所以我们可以干两件事情来安装 Plugin

  • 把 Plugin1 项目的编译结果 Plugin1.dll 拷贝到 PluginWebApp 的 bin 目录下

  • 在 PluginWebApp 项目下创建 Areas 目录,下建 Plugin1 目录,再把 Plugin1 项目的 ~/Areas/Plugin1/Views目录拷贝过来

猜测做了这些操作之后,应该可以运行 PluginWebApp,输入正常的 url 路径之后可以访问到 Plugin1 的 Test 页面。

运行,并在浏览器中输入 http://localhost:5760/plugin1/test (这里的端口号是由 VS 自动分配的,请注意修改)——结果还不错

wKioL1msBQXD686iAAB3aXXI704307.png

解耦

第一个实验成功,实事证明猜想没有问题。但于对开发来说,就有问题了。插件动态库放在 PluginWebApp/bin 中,与 PluginWebApp 的编译结果混在一起了,这在以后发布、更新的时候可能造成麻烦。而且既然是插件,似乎应该独立一点,如果 Plugin1 发布的所有东西都只在 PluginWebApp/Areas/Plugin1 目录下就好了。

基于这个设想,PluginWebApp/Areas/Plugin1 目录应该会是这样一个结构:

Plugin1
  |---bin
  `---Views

当然,把 Plugin1.dll 拷贝到 bin 目录中去很容易,但还得让 Asp.Net 加载它。于是尝试在 Application_Start 中写了几句代码来加载

// 先不考虑任意插件的问题,只加载 Plugin1 作为实验var dll = Sever.MapPath("~/Areas/Plugin/bin/Plugin1.dll");
Assembly.LoadFile(dll);

加载是加载了,但是 http://localhost:5760/plugin1/test 打不开,失败!

使用 BuildManager 和 PreApplicationStartMethodAttribute

上网查资料之后得知需要使用 BuildManager.AddReferencedAssembly() 将加载的 Assembly 添加到引用集合中,而这个事情似乎必须在 Application_Start 之前完成

文档里说应该在 Application_PreStartInit 阶段,不过我准备使用 PreApplicationStartMethodAttribute 来完成。为此,在 PluginWebApp 项目的 App_Start 下添加了一个 PluginInitializer 类来干这个事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using  System.Web;
using  System.Web.Hosting;
using  System.Web.Compilation;
 
[assembly: PreApplicationStartMethod( typeof (PluginWebApp.PluginInitializer),  "Initialize" )]
namespace  PluginWebApp
{
     public  static  partial  class  PluginInitializer
     {
         public  static  void  Initialize()
         {
             var  dll = HostingEnvironment( "~/Areas/Plugin1/bin/Plugin1.dll" );
             var  assembly = Assembly.LoadFile(dll);
             BuildManager.AddReferencedAssembly(assembly);
         }
     }
}

再次运行,成功!

搜索并加载插件

到目前为止还是直接加载的 Plugin1 插件,实际工作中应该去检查 Areas 下面的子目录,加载其 bin 目录下的动态库。所以还需要修改 PluginInitializer,让它动态搜索各插件目录的 bin/*.dll,并加载。

为此,不妨专门写一个 PluginLoader 类,因为这个类现在只由 PluginInitializer 使用,所以直接写成它的嵌套类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public  static  partial  class  PluginInitializer
{
     public  sealed  class  PluginLoader
     {
         public  void  Load()
         {
             FindPluginDll(HostingEnvironment( "~/Areas" ))
                 // 并行处理不是必须的,但在插件多的时候可能会更快
                 .AsParallel()
                 .ForAll(file => BuildManager.AddReferencedAssembly(Assembly.Load(file)));
         }
 
         // 从指定的插件根目录 (这里是 Areas) 搜索带 bin 目录的插件目录
         // 并将其中的 *.dll 找出来
         private  static  string [] FindPluginDll( string  root)
         {
             return  Directory.EnumerateDirectories(root)
                 .Select(dir => Path.Combine(dir,  "bin" ))
                 // 如果没有 bin 目录就忽略
                 .Where(Directory.Exists)
                 // 将 bin 目录下的所有 dll 加载到集合中
                 .SelectMany(bin => Directory
                     .EnumerateFiles(bin,  "*.dll" , SearchOption.AllDirectories))
                 .ToArray();
         }
     }
}

动态检索的问题解决了,但在实际开发中又存在另一个问题:运行 Web 之后,再次构建插件的并将插件内容 (bin 和View) 拷贝到主项目 Areas 下面对应的插件目录中时,会因为原来的 dll 文件在使用而不能覆盖。

解决不能在 Web 运行状态下更新插件的问题

在解决这个问题就不能让 Web 直接加载插件目录中的 dll。采用 Asp.Net 的 Shadow Copy 的思想,我们可以在 App_Data 目录中创建一个 PluginCache 目录,然后在加载插件 dll 之前把所有 dll 拷贝到这个目录下来,再从这个目录加载 dll。

再来改造一下 PluginLoader

创建目录和清空缓存都很简单,这里就不展示这两个步骤的代码了。
FindPluginDll 的代码在前面可以找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public  sealed  class  PluginLoader
{
     string  PluginFolder {  get ; } = HostingEnvironment.MapPath( "~/Areas" );
     string  PluginCacheFolder {  get ; } = HostingEnvironment.MapPath( "~/App_Data/PluginCache" );
 
     public  void  Load()
     {
         // 上述两个目录不存在,则创建,保证目录存在
         MakeSureFolderExists();
         // 先清空缓存,避免已废弃的插件还缓存在这里
         ClearCacheFolder();
         // 从各插件目录把 dll 拷贝到缓存目录
         CachePlugins();
         // 从缓存目录加载所有 dll        
         LoadAssemblies();
     }
 
     private  void  CachePlugins()
     {
         // 找到所有插件的 dll
         FindPluginDll(PluginFolder)
             // 并行处理
             .AsParallel()
             .ForAll(file =>
             {
                 var  target = Path.Combine(PluginCacheFolder, Path.GetFileName(file));
                 // 拷贝到缓存目录
                 File.Copy(file, target,  true );
             });
     }
 
     private  void  LoadAssemblies()
     {
         // 在缓存目录中查找所有 dll
         Directory.EnumerateFiles(PluginCacheFolder,  "*.dll" , SearchOption.AllDirectories)
             // 并行
             .AsParallel()
             // 加载所有 assembly
             .ForAll(file => BuildManager.AddReferencedAssembly(Assembly.LoadFile(file)));
     }
}

搞定!

细节处理

解决 Controller 寻址冲突

主 Web 程序和多个插件之间如果存在同名的 Controller,就可能造成访问 URL 的时候出现 Controller 寻址冲突,为了解决这个问题,需要在注册路径的时候指定 Controller 的命名空间

主项目 PluginWebApp 的 App_Start/RouteConfig.cs

1
2
3
4
5
6
7
8
9
10
11
public  static  void  RegisterRoutes(RouteCollection routes)
{
     routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" );
 
     routes.MapRoute(
         name:  "Default" ,
         url:  "{controller}/{action}/{id}" ,
         defaults:  new  { controller =  "Home" , action =  "Index" , id = UrlParameter.Optional },
         namespaces:  new [] {  "PluginWebApp.Controllers"  }     // 加了这句话
     );
}

插件的 Plugin1AreaRegistration.cs

1
2
3
4
5
6
7
8
9
public  override  void  RegisterArea(AreaRegistrationContext context)
{
     context
         .MapRoute(
             "Plugin1_default" ,
             "Plugin1/{controller}/{action}/{id}" ,
             new  { controller =  "Home" , action =  "Index" , id = UrlParameter.Optional },
             new [] {  "Plugin1.Areas.Plugin1.Controllers"  });  // 加了这一句
}

处理删除或拷贝 dll 文件时可能出现的异常

在作为 ForAll 的 Lambda 表达式中,每次删除文件或拷贝文件都有可能出现异常,而出现这些异常的时候,不应该中断整个处理过程,所以需要使用 try ... catch 来处理异常。正常的处理方式应该是记录日志,这里偷个懒,直接忽略(生产环境严重不推荐忽略异常)。

由于这个操作在几个地方都会用到,所以写一个 IgnoreError 来封装 Lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private  static  Action<T> IgnoreError<T>(Action<T> action)
{
     return  arg =>
     {
         try
         {
             action(arg);
         }
         catch
         {
             // ignore exceptions,
             // should log the error in production environment
         }
     };
}

然后在 ForAll 中这样使用:

1
     .ForAll(IgnoreError< string >(file => DealWithFile(file)));

后记

上述内容充其量只是一个插件化开发的简化方案。不过这个方案基本上也把一个插件化框架的结构介绍清楚了。而且采用这种方式开发还有一个好处:Plugin1 本身就是一个 Web 项目,所以如果之前不删除那么多东西,并加以适当的调整,它是可以独立运行的,便于开发期调试。

当然这个框架要用于工作中还需要完善不少工作,包括:

  • 定义插件接口和抽象基类,提供初始化,注入上下文(比如应用配置等),注册路由等接口方法。

  • 主项目或框架项目中定义插件管理器,管理插件的生命周期,实现热插拔

    • 加载、注册

    • 检查更新、新增插件等

    • 卸载插件 Assembly 并重新加载

  • 使用 Plugins 代替 Areas 目录,让插件与 Area 区分开来,这需要

    • 在插件管理器中实现 AreaRegistration.RegisterAllAreas() 的一些功能

    • 把 Plugins 目录添加到 Razor 视图搜索路径中 (需要自定义 RazorViewEngine)

  • 设计插件间的资源共享和通信机制

  • 插件管理的 UI 或 CLI


本文转自边城__ 51CTO博客,原文链接:http://blog.51cto.com/jamesfancy/1962363,如需转载请自行联系原作者
相关文章
|
17天前
|
开发框架 算法 Java
.NET 开发:实现高效能的秘诀
【7月更文挑战第4天】探索.NET高效开发涉及理解运行时(如GC、JIT)、代码与算法优化及工具利用。关键点包括适应性垃圾回收、异步编程、明智的并发控制;编写高效代码(避免对象创建,选对数据结构和算法);使用性能分析工具,善用高性能框架如ASP.NET Core,并借助云服务和CI/CD流程持续优化。性能优化是持续学习与实践的过程。
27 1
|
1月前
|
开发框架 前端开发 .NET
LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码
集成于VS 2019,EXT.NET前端和ASP.NET后端,搭配MSSQL 2018数据库。系统覆盖样品管理、数据分析、报表和项目管理等实验室全流程。应用广泛,包括生产质检(如石化、制药)、环保监测、试验研究等领域。随着技术发展,现代LIMS还融合了临床、电子实验室笔记本和SaaS等功能,以满足复杂多样的实验室管理需求。
37 3
LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码
|
17天前
|
人工智能 前端开发 Devops
NET技术在现代开发中的影响力日益增强,本文聚焦其核心价值,如多语言支持、强大的Visual Studio工具、丰富的类库和跨平台能力。
【7月更文挑战第4天】**.NET技术在现代开发中的影响力日益增强,本文聚焦其核心价值,如多语言支持、强大的Visual Studio工具、丰富的类库和跨平台能力。实际应用涵盖企业系统、Web、移动和游戏开发,以及云服务。面对性能挑战、容器化、AI集成及跨平台竞争,.NET持续创新,开发者应关注技术趋势,提升技能,并参与社区,共同推进技术发展。**
15 1
|
17天前
|
机器学习/深度学习 人工智能 开发者
.NET 技术:为开发带来新机遇
【7月更文挑战第4天】**.NET技术开启软件开发新篇章,通过跨平台革命(.NET Core, Xamarin, .NET MAUI)、云服务与微服务(Azure, DevOps, Docker)及AI集成(ML.NET, 认知服务, TensorFlow)为开发者创造新机遇。开源社区的繁荣与性能提升使.NET更具竞争力,推动智能应用的创新与发展。开发者需紧跟潮流,利用这些工具和框架构建高效、创新的解决方案。**
15 1
|
27天前
|
JSON 前端开发 Java
Springboot mvc开发之Rest风格及RESTful简化开发案例
Springboot mvc开发之Rest风格及RESTful简化开发案例
27 2
|
1月前
|
开发框架 JavaScript 前端开发
分享7个.NET开源、功能强大的快速开发框架
分享7个.NET开源、功能强大的快速开发框架
|
2月前
|
存储 JSON 前端开发
利用Spring MVC开发程序2
利用Spring MVC开发程序
30 1
|
2月前
|
开发框架 .NET C#
使用C#进行.NET框架开发:深入探索与实战
【5月更文挑战第28天】本文探讨了C#在.NET框架中的应用,展示了其作为强大编程语言的特性,如类型安全、面向对象编程。C#与.NET框架的结合,提供了一站式的开发环境,支持跨平台应用。文中介绍了C#的基础知识,如数据类型、控制结构和面向对象编程,以及.NET的关键技术,包括LINQ、ASP.NET和WPF。通过一个实战案例,展示了如何使用C#和ASP.NET开发Web应用,包括项目创建、数据库设计、模型和控制器编写,以及视图和路由配置。本文旨在揭示C#在.NET开发中的深度和广度,激发开发者探索更多可能性。
|
2月前
|
设计模式 存储 前端开发
Java的mvc设计模式在web开发中应用
Java的mvc设计模式在web开发中应用
|
2月前
|
设计模式 JSON 前端开发
利用Spring MVC开发程序1
利用Spring MVC开发程序
29 0