【转】基于ASP.NET MVC3 Razor的模块化/插件式架构实现——Mainz
本文主要探讨了一种基于ASP.NET MVC3 Razor的模块化(Plugin)/插件(plugin)式架构的实现方法。本文借鉴了《Compile your asp.net mvc Razor views into a seperate dll》作者提供的方法。敬请注意。其实ASP.NET MVC的模块化(Plugin)/插件(plugin)式架构讨论的很多,但基于Razor视图引擎的很少(如:MVC2插件架构例子都是基于WebForm的,MVCContrib Portable Areas也是,还有这个Plugin架构)。要么就是非常复杂非常重量级的框架,例如Orchard CMS的模块化做的很好,可惜太重量级了,也没独立的模块可以剥离出来。所以我们追寻的是简单的基于ASP.NET MVC3 Razor的模块化(Plugin)/插件(plugin)式架构的实现方法。本文最后实现的项目结构如下图:(插件都放到~/Plugin目录下,按功能划分模块,每个模块都有M,V,C)
其中,业务模块(class library project)包含其所有的视图、控制器等,模型可以放在里面也可以单独放一个project。主web项目没有引用业务模块,业务模块会编译到主web项目的~/plugin目录下面(注意:不是bin目录),然后当web应用启动的时候自动加载plugin目录下面的模块。最后运行起来的效果如下图:
其中红色的区域都是plugin进去的,那个tab的标题plugin到母版页的主菜单,tab内容也来自plugin。下面说说如何实现这样的ASP.NET MVC插件式plugin架构(模块化架构)。
实现的难点在动态加载UI视图(*.cshtml, _layout.cshtml, _viewStart.cshtml)
废话少说,直入要害。基于ASP.NET MVC3 Razor的编译发生在两个层面:
- 控制器(Controller), 模型(Models),和其它所有的C#代码等有msbuild(或者VisualStudio)编译到bin目录下的程序集(assembly)
- 视图(*.aspx, *.cshtml)由ASP.NET在运行时动态编译。当一个Razor视图(*.cshtml)显示前,Razor视图引擎调用BuildManager把视图(*.cshtml)编译到动态程序集assembly,然后使用Activator.CreateInstance来实例化新编译出来的对象,最后显示出来。如果视图(*.cshtml)用到@model绑定model,那么还会自动加载bin或者GAC里面的Model。
所以如果我们要动态加载插件(plugin),用反射bin目录下的程序集(assembly)的方法很容易搞定上面的第一部分(C#代码的部分),但UI视图的部分(上面第二部分)(特别是*.cshtml, 母版_layout.cshtml, 基视图_viewStart.cshtml)就比较难搞定。而且每次报错都是一样的,那就是Controller找不到相应的视图View,基本不知所云而且根本不是要点:view …. or its master was not found or no view engine supports the searched locations. The following locations were searched: …,因此要搞定UI视图的部分(上面第二部分)(特别是*.cshtml, 母版_layout.cshtml, 基视图_viewStart.cshtml),就需要自己动手了,基本原理是:
- 重载RazorBuildProvider,用来动态编译视图
- 实现一个自定义VirtualPathProvider,从虚拟路径自定义判断读取资源(从插件中加载资源),如果要使用编译的视图就返回编译的VirtualFile
- 实现一个容器Dictionary保存已编译的视图和虚拟路径,例如path <~/views/team/index.cshtml> type <Area.Module2.Views.Team._Page_Views_Team_Index_cshtml>,或者path <~/views/_viewstart.cshtml> type <Area.Module1.Views._Page_Views__ViewStart_cshtml>
代码:自定义VirtualPathProvider,从虚拟路径自定义判断读取资源(从插件中加载资源),如果要使用编译的视图就返回编译的VirtualFile
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection; 5 using System.Web.Caching; 6 using System.Web.Hosting; 7 using System.Web.WebPages; 8 9 namespace Common.Framework 10 { 11 public class CompiledVirtualPathProvider: VirtualPathProvider 12 { 13 /// <summary> 14 /// Gets a value that indicates whether a file exists in the virtual file system. 15 /// </summary> 16 /// <returns> 17 /// true if the file exists in the virtual file system; otherwise, false. 18 /// </returns> 19 /// <param name="virtualPath">The path to the virtual file.</param> 20 public override bool FileExists(string virtualPath) 21 { 22 return 23 GetCompiledType(virtualPath) != null 24 || Previous.FileExists(virtualPath); 25 } 26 27 public Type GetCompiledType(string virtualPath) 28 { 29 return ApplicationPartRegistry.Instance.GetCompiledType(virtualPath); 30 } 31 32 /// <summary> 33 /// Gets a virtual file from the virtual file system. 34 /// </summary> 35 /// <returns> 36 /// A descendent of the <see cref="T:System.Web.Hosting.VirtualFile"/> class that represents a file in the virtual file system. 37 /// </returns> 38 /// <param name="virtualPath">The path to the virtual file.</param> 39 public override VirtualFile GetFile(string virtualPath) 40 { 41 if (Previous.FileExists(virtualPath)) 42 { 43 return Previous.GetFile(virtualPath); 44 } 45 var compiledType = GetCompiledType(virtualPath); 46 if (compiledType != null) 47 { 48 return new CompiledVirtualFile(virtualPath, compiledType); 49 } 50 return null; 51 } 52 53 public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart) 54 { 55 if (virtualPathDependencies == null) 56 return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); 57 58 return Previous.GetCacheDependency(virtualPath, 59 from vp in virtualPathDependencies.Cast<string>() 60 where GetCompiledType(vp) == null 61 select vp 62 , utcStart); 63 } 64 65 } 66 }
代码:容器Dictionary保存已编译的视图和虚拟路径,例如path <~/views/team/index.cshtml> type <Area.Module2.Views.Team._Page_Views_Team_Index_cshtml>,路径注册以后,会从容器库全局搜索所有注册过的视图,也就是说即使你视图引用的_layout.cshtml和_viewStart.cshtml在其他的Class library project照样可以找到。
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.Linq; 5 using System.Reflection; 6 using System.Web; 7 using System.Web.WebPages; 8 9 namespace Common.Framework 10 { 11 public class DictionaryBasedApplicationPartRegistry : IApplicationPartRegistry 12 { 13 private static readonly Type webPageType = typeof(WebPageRenderingBase); 14 private readonly Dictionary<string, Type> registeredPaths = new Dictionary<string, Type>(); 15 16 /// <summary> 17 /// 18 /// </summary> 19 /// <param name="virtualPath"></param> 20 /// <returns></returns> 21 public virtual Type GetCompiledType(string virtualPath) 22 { 23 if (virtualPath == null) throw new ArgumentNullException("virtualPath"); 24 25 //Debug.WriteLine(String.Format("---GetCompiledType : virtualPath <{0}>", virtualPath)); 26 27 if (virtualPath.StartsWith("/")) 28 virtualPath = VirtualPathUtility.ToAppRelative(virtualPath); 29 if (!virtualPath.StartsWith("~")) 30 virtualPath = !virtualPath.StartsWith("/") ? "~/" + virtualPath : "~" + virtualPath; 31 virtualPath = virtualPath.ToLower(); 32 return registeredPaths.ContainsKey(virtualPath) 33 ? registeredPaths[virtualPath] 34 : null; 35 } 36 37 public void Register(Assembly applicationPart) 38 { 39 ((IApplicationPartRegistry)this).Register(applicationPart, null); 40 } 41 42 public virtual void Register(Assembly applicationPart, string rootVirtualPath) 43 { 44 //Debug.WriteLine(String.Format("---Register assembly <{0}>, path <{1}>", applicationPart.FullName, rootVirtualPath)); 45 46 foreach (var type in applicationPart.GetTypes().Where(type => type.IsSubclassOf(webPageType))) 47 { 48 //Debug.WriteLine(String.Format("-----Register type <{0}>, path <{1}>", type.FullName, rootVirtualPath)); 49 50 ((IApplicationPartRegistry)this).RegisterWebPage(type, rootVirtualPath); 51 } 52 } 53 54 public void RegisterWebPage(Type type) 55 { 56 ((IApplicationPartRegistry)this).RegisterWebPage(type, string.Empty); 57 } 58 59 public virtual void RegisterWebPage(Type type, string rootVirtualPath) 60 { 61 var attribute = type.GetCustomAttributes(typeof(PageVirtualPathAttribute), false).Cast<PageVirtualPathAttribute>().SingleOrDefault<PageVirtualPathAttribute>(); 62 if (attribute != null) 63 { 64 var rootRelativeVirtualPath = GetRootRelativeVirtualPath(rootVirtualPath ?? "", attribute.VirtualPath); 65 66 //Debug.WriteLine(String.Format("---Register path/type : path <{0}> type <{1}>", rootRelativeVirtualPath.ToLower(), 67 // type.FullName)); 68 registeredPaths[rootRelativeVirtualPath.ToLower()] = type; 69 } 70 } 71 72 static string GetRootRelativeVirtualPath(string rootVirtualPath, string pageVirtualPath) 73 { 74 string relativePath = pageVirtualPath; 75 if (relativePath.StartsWith("~/", StringComparison.Ordinal)) 76 { 77 relativePath = relativePath.Substring(2); 78 } 79 if (!rootVirtualPath.EndsWith("/", StringComparison.OrdinalIgnoreCase)) 80 { 81 rootVirtualPath = rootVirtualPath + "/"; 82 } 83 relativePath = VirtualPathUtility.Combine(rootVirtualPath, relativePath); 84 if (!relativePath.StartsWith("~")) 85 { 86 return !relativePath.StartsWith("/") ? "~/" + relativePath : "~" + relativePath; 87 } 88 return relativePath; 89 } 90 } 91 }
下面的代码很关键,用PreApplicationStartMethod关键字(.NET 4.0开始支持)使得代码在Application_Start之前执行。
有关[assembly: PreApplicationStartMethod(typeof(SomeClassLib.Initializer), "Initialize")]详细信息请参考这个页面和这个页面。
1 using System.Web; 2 using System.Web.Compilation; 3 using System.Web.Hosting; 4 using Common.Framework; 5 using Common.PrecompiledViews; 6 7 [assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")] 8 9 namespace Common.Framework 10 { 11 public static class PreApplicationStartCode 12 { 13 private static bool _startWasCalled; 14 15 public static void Start() 16 { 17 if (_startWasCalled) 18 { 19 return; 20 } 21 _startWasCalled = true; 22 23 //Register virtual paths 24 HostingEnvironment.RegisterVirtualPathProvider(new CompiledVirtualPathProvider()); 25 26 //Load Plugin Folder, 27 PluginLoader.Initialize(); 28 } 29 } 30 }
代码:PluginLoader,加载plugin目录里面的东东(assembly和module配置文件)
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 }
此外,使用SingleFileGenerator的优点是性能提升,缺点是修改了视图就要重新编译。
如何让ASP.NET加载BIN目录之外的路径的Assembly
我们把各个模块编译出来的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\"
然后用下面的代码加载Plugin目录下的assembly:(只看LoadAssembly那一段)
1 private static void LoadAssemblies(string folder) 2 { 3 var directory = new DirectoryInfo(HostingEnvironment.MapPath(folder)); 4 var binFiles = directory.GetFiles("*.dll", SearchOption.AllDirectories).ToList(); 5 if (binFiles.Count == 0) return; 6 7 foreach (var plug in binFiles) 8 { 9 //running in full trust 10 //************ 11 //if (GetCurrentTrustLevel() != AspNetHostingPermissionLevel.Unrestricted) 12 //set in web.config, probing to plugin\temp and copy all to that folder 13 //************************ 14 var shadowCopyPlugFolder = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory); 15 var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name)); 16 File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); //TODO: Exception handling here... 17 var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName)); 18 19 //add the reference to the build manager 20 BuildManager.AddReferencedAssembly(shadowCopiedAssembly); 21 } 22 }
如果不是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,接口
模块配置文件{projectName}.config的位置:
为什么每个模块的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的"插件式"开发》。另外,有很多框架提供了模块化/插件开发的框架,例如Prism、MEF(Managed Extensibility Framework,.NET 4.0 内置)等。
客户端插件架构
还有一种插件架构是客户端插件架构(Javascript 模块化),如jQuery UI Widget Factory,Silk Project就是很好的例子。
转帖说明
本文是转自 Mainz 的文章,原文链接:http://www.cnblogs.com/Mainz/archive/2012/03/06/2382653.html
这是一篇难得的好文章,但原文作者排版对代码进行了 max-height:200px 的处理,可能是为了排版美观吧,但这样处理之后阅读代码老要受滚动条的影响,太不痛快,于是转帖于此,主要是为自己阅读的方便。
作者:郭明锋
Q群:MVC EF技术交流(5008599) OSharp开发框架交流(85895249)
出处:https://www.cnblogs.com/guomingfeng
声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。