• 基于ASP.NET MVC3 Razor的模块化/插件式架构实现



    我们把各个模块编译出来的assembly和各个模块的配置文件自动放到一个bin平级的plugin目录,然后web应用启动的时候自动扫描这个plugin目录并加载各个模块plugin,这个怎么做到的?大家也许知道,ASP.NET只允许读取Bin目录下的assbmely,不可以读取其他路径,包括Bin\abc等,即使在web.config这样配置probing也不行:(不信你可以试一下)

       1: <configuration> Element
       2:   <runtime> Element
       3:     <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
       4:       <probing privatePath="bin;bin\abc;plugin;"/>
       5:     </assemblyBinding>
       6:    </runtime>
       7: </configuration>

    这个和TrustLevel有关,在Full Trust的情况下,可以这样读取非Bin目录下的assembly:

    首先在和Bib平级的地方建一个目录Plugin,然后在模块class library project的属性里面加一个postBuildEvent,就是说在编译完成以后把模块的assbmely自动拷贝到主web项目的plugin目录:

       1: copy /Y "$(TargetDir)$(ProjectName).dll" "$(SolutionDir)ModularWebApplication\Plugin\"
       2: copy /Y "$(TargetDir)$(ProjectName).config" "$(SolutionDir)ModularWebApplication\Plugin\"
       3:  

    然后用下面的代码加载Plugin目录下的assembly:(只看LoadAssembly那一段)

       1: using System;
       2: using System.Collections.Generic;
       3: using System.IO;
       4: using System.Linq;
       5: using System.Reflection;
       6: using System.Text;
       7: using System.Threading;
       8: using System.Web;
       9: using System.Web.Compilation;
      10: using System.Web.Hosting;
      11: using Common.Framework;
      12: using Common.PrecompiledViews;
      13:  
      14: //[assembly: PreApplicationStartMethod(typeof(PluginLoader), "Initialize")]
      15:  
      16: namespace Common.PrecompiledViews
      17: {
      18:     public class PluginLoader
      19:     {
      20:         public static void Initialize(string folder = "~/Plugin")
      21:         {
      22:             LoadAssemblies(folder);
      23:             LoadConfig(folder);
      24:         }
      25:  
      26:         private static void LoadConfig(string folder, string defaultConfigName="*.config")
      27:         {
      28:             var directory = new DirectoryInfo(HostingEnvironment.MapPath(folder));
      29:             var configFiles = directory.GetFiles(defaultConfigName, SearchOption.AllDirectories).ToList();
      30:             if (configFiles.Count == 0) return;
      31:  
      32:             foreach (var configFile in configFiles.OrderBy(s => s.Name))
      33:             {
      34:                 ModuleConfigContainer.Register(new ModuleConfiguration(configFile.FullName));
      35:             }
      36:         }
      37:  
      38:         private static void LoadAssemblies(string folder)
      39:         {
      40:             var directory = new DirectoryInfo(HostingEnvironment.MapPath(folder));
      41:             var binFiles = directory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
      42:             if (binFiles.Count == 0) return;
      43:  
      44:             foreach (var plug in binFiles)
      45:             {
      46:                 //running in full trust
      47:                 //************
      48:                 //if (GetCurrentTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
      49:                 //set in web.config, probing to plugin\temp and copy all to that folder
      50:                 //************************
      51:                 var shadowCopyPlugFolder = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory);
      52:                 var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));
      53:                 File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); //TODO: Exception handling here...
      54:                 var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
      55:  
      56:                 //add the reference to the build manager
      57:                 BuildManager.AddReferencedAssembly(shadowCopiedAssembly);
      58:             }
      59:         }
      60:  
      61:         //private static AspNetHostingPermissionLevel GetCurrentTrustLevel()
      62:         //{
      63:         //    foreach (AspNetHostingPermissionLevel trustLevel in
      64:         //        new AspNetHostingPermissionLevel[]
      65:         //            {
      66:         //                AspNetHostingPermissionLevel.Unrestricted,
      67:         //                AspNetHostingPermissionLevel.High,
      68:         //                AspNetHostingPermissionLevel.Medium,
      69:         //                AspNetHostingPermissionLevel.Low,
      70:         //                AspNetHostingPermissionLevel.Minimal
      71:         //            })
      72:         //    {
      73:         //        try
      74:         //        {
      75:         //            new AspNetHostingPermission(trustLevel).Demand();
      76:         //        }
      77:         //        catch (System.Security.SecurityException)
      78:         //        {
      79:         //            continue;
      80:         //        }
      81:  
      82:         //        return trustLevel;
      83:         //    }
      84:  
      85:         //    return AspNetHostingPermissionLevel.None;
      86:         //}
      87:  
      88:     }
      89: }

    如果不是Full Trust,例如Medium Trust的情况下参考这个帖子《Developing-a-plugin-framework-in-ASPNET-with-medium-trust》。

    如何在_layout.cshtml的主菜单注入plugin的菜单

    在母版页_layout.cshtml有个主菜单,一般是这样写的:

       1: <ul>
       2:    <li>@Html.ActionLink("Home", "Index", "Home")</li>
       3:    <li>@Html.ActionLink("About", "About", "Home")</li>
       4:    <li>@Html.ActionLink("Team", "Index", "Team")</li>
       5: </ul>

    现在我们如何实现从模块插入plugin到这个主菜单呢?这个有点难。因为大家知道,_layout.cshml母版没有controller。怎么实现呢?方法是用controller基类,让所有controller继承自这个基类。然后在基类里面,读取plugin目录里面的配置文件,获取所有模块需要插入的主菜单项,然后放入viewBag,这样在_Layout.cshtml就可以获取viewBag,类似这样:

       1: <ul>
       2:    @foreach (MainMenuItemModel entry in ViewBag.MainMenuItems)
       3:     {
       4:         <li>@Html.ActionLink(entry.Text, 
       5:                entry.ActionName, 
       6:                 entry.ControllerName)</li>
       7:     }
       8: </ul>

    代码:基类Controller,读取plugin目录里面的配置文件,获取所有模块需要插入的主菜单项,然后放入viewBag

       1: using System;
       2: using System.Collections;
       3: using System.Collections.Generic;
       4: using System.ComponentModel;
       5: using System.Linq;
       6: using System.Net.Mime;
       7: using System.Text;
       8: using System.Web.Mvc;
       9:  
      10: namespace Common.Framework
      11: {
      12:     public class BaseController : Controller
      13:     {
      14:         protected override void Initialize(System.Web.Routing.RequestContext requestContext)
      15:         {
      16:             base.Initialize(requestContext);
      17:  
      18:             // retireve data from plugins
      19:             IEnumerable<ModuleConfiguration> ret = ModuleConfigContainer.GetConfig();
      20:  
      21:             var data = (from c in ret
      22:                         from menu in c.MainMenuItems
      23:                         select new MainMenuItemModel
      24:                                    {
      25:                                        Id = menu.Id, ActionName = menu.ActionName, ControllerName = menu.ControllerName, Text = menu.Text
      26:                                    }).ToList();
      27:             
      28:             ViewBag.MainMenuItems = data.AsEnumerable();
      29:         }
      30:  
      31:     }
      32: }

    代码:ModuleConfigContainer,用到单例模式,只读取一次

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5:  
       6: namespace Common.Framework
       7: {
       8:     public static class ModuleConfigContainer
       9:     {
      10:         static ModuleConfigContainer()
      11:         {
      12:             Instance = new ModuleConfigDictionary();
      13:         }
      14:  
      15:         internal static IModuleConfigDictionary Instance { get; set; }
      16:  
      17:         public static void Register(ModuleConfiguration item)
      18:         {
      19:             Instance.Register(item);
      20:         }
      21:  
      22:         public static IEnumerable<ModuleConfiguration> GetConfig()
      23:         {
      24:             return Instance.GetConfigs();
      25:         }
      26:     }
      27: }
    代码:ModuleConfigDictionary
       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5:  
       6: namespace Common.Framework
       7: {
       8:     public class ModuleConfigDictionary : IModuleConfigDictionary
       9:     {
      10:         private readonly Dictionary<string, ModuleConfiguration>  _configurations = new Dictionary<string, ModuleConfiguration>();
      11:  
      12:         public IEnumerable<ModuleConfiguration> GetConfigs()
      13:         {
      14:             return _configurations.Values.AsEnumerable();
      15:         }
      16:  
      17:         public void Register(ModuleConfiguration item)
      18:         {
      19:             if(_configurations.ContainsKey(item.ModuleName))
      20:             {
      21:                 _configurations[item.ModuleName] = item;
      22:             }
      23:             else
      24:             {
      25:                 _configurations.Add(item.ModuleName, item);
      26:             }
      27:         }
      28:     }
      29: }

    代码:ModuleConfiguration,读取模块的配置文件

       1: using System;
       2: using System.Collections.Generic;
       3: using System.IO;
       4: using System.Linq;
       5: using System.Text;
       6: using System.Xml;
       7: using System.Xml.Linq;
       8:  
       9: namespace Common.Framework
      10: {
      11:     public class ModuleConfiguration
      12:     {
      13:         public ModuleConfiguration(string filePath)
      14:         {
      15:             try
      16:             {
      17:                 var doc = XDocument.Load(filePath);
      18:                 var root = XElement.Parse(doc.ToString());
      19:  
      20:                 if (!root.HasElements) return;
      21:  
      22:                 var module = from e in root.Descendants("module")
      23:                              //where e.Attribute("name").Value == "xxxx"
      24:                              select e;
      25:  
      26:                 if (!module.Any()) return;
      27:  
      28:                 ModuleName = module.FirstOrDefault().Attribute("name").Value;
      29:  
      30:                 var menus = from e in module.FirstOrDefault().Descendants("menu")
      31:                             select e;
      32:  
      33:                 if (!menus.Any()) return;
      34:  
      35:                 var menuitems = menus.Select(xElement => new MainMenuItemModel
      36:                                                              {
      37:                                                                  Id = xElement.Attribute("id").Value,
      38:                                                                  Text = xElement.Attribute("text").Value,
      39:                                                                  ActionName = xElement.Attribute("action").Value,
      40:                                                                  ControllerName = xElement.Attribute("controller").Value
      41:                                                              }).ToList();
      42:  
      43:                 MainMenuItems = menuitems;
      44:             }
      45:             catch
      46:             {
      47:                 //TODO: logging
      48:             }
      49:         }
      50:         public string ModuleName { get; set; }
      51:         public IEnumerable<MainMenuItemModel> MainMenuItems { get; set; }
      52:     }
      53: }

    每个模块的配置文件为{projectName}.config,格式如下:

       1: <?xml version="1.0" encoding="utf-8" ?>
       2: <configuration>
       3:   <module name="Module2">
       4:     <mainmenu>
       5:       <menu id="modul2" text="Team" action="Index" controller="Team"/>
       6:     </mainmenu>
       7:   </module>
       8: </configuration>
    为了简单起见,只保留了注入主菜单的部分,为了让读者简单易懂。明白了以后你自己可以任意扩展…

    代码:IModuleConfigDictionary,接口

    dddd

    模块配置文件{projectName}.config的位置:

    Untitled

    为什么每个模块的Class library project都需要一个web.config呢?因为如果没有这个,那就没有Razor智能提示,大家可以参考这篇文章《How to get Razor intellisense for @model in a class library project》。

    闲话几句插件式架构(Plugin Architecture)或者模块化(Modular)架构

    插件式架构(Plugin Architecture)或者模块化(Modular)架构是大型应用必须的架构,关于什么是Plugin,什么是模块化模式,这种架构的优缺点等我就不说了,自己百谷歌度。关于.NET下面的插件式架构和模块化开发实现方法,基本上用AppDomain实现,当检测到一个新的插件Plugin时,实例化一个新的AppDomain并加载Assembly反射类等,由于AppDomain很好的隔离各个Plugin,所以跨域通信要用MarshalByRefObject类,具体做法可以参考这篇文章《基于AppDomain的"插件式"开发》。另外,有很多框架提供了模块化/插件开发的框架,例如PrismMEF(Managed Extensibility Framework,.NET 4.0 内置)等。

    客户端插件架构

    还有一种插件架构是客户端插件架构(Javascript 模块化),如jQuery UI Widget FactorySilk Project就是很好的例子。

    本文源码下载

    源码下载请点击此处。运行源码之前务必阅读此文和本文。注意:本文抛砖引玉,力求简单,易懂,并非完整的架构实现,更多丰富的功能实现一切皆有可能,只要在理解的基础上。

     
    分类: C#
    标签: 架构ASP.NET
  • 相关阅读:
    微信小程序 select 下拉框组件
    git 常用操作汇总
    VUE组件 之 Drawer 抽屉
    三、VUE项目BaseCms系列文章:axios 的封装
    C#数据采集用到的几个方法
    Mac环境安装非APP STORE中下载的软件,运行报错:“XXX” is damaged and can’t be opened. You should move it to the Trash. 解决办法
    Mac环境下执行npm install报权限错误解决办法
    VUE组件 之 高德地图地址选择
    Vue-Cli 3.0 中配置高德地图
    二、VUE项目BaseCms系列文章:项目目录结构介绍
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2611245.html
Copyright © 2020-2023  润新知