我们把各个模块编译出来的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有个主菜单,一般是这样写的:
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: }
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,什么是模块化模式,这种架构的优缺点等我就不说了,自己百谷歌度。关于.NET下面的插件式架构和模块化开发实现方法,基本上用AppDomain实现,当检测到一个新的插件Plugin时,实例化一个新的AppDomain并加载Assembly反射类等,由于AppDomain很好的隔离各个Plugin,所以跨域通信要用MarshalByRefObject类,具体做法可以参考这篇文章《基于AppDomain的"插件式"开发》。另外,有很多框架提供了模块化/插件开发的框架,例如Prism、MEF(Managed Extensibility Framework,.NET 4.0 内置)等。
还有一种插件架构是客户端插件架构(Javascript 模块化),如jQuery UI Widget Factory,Silk Project就是很好的例子。
源码下载请点击此处。运行源码之前务必阅读此文和本文。注意:本文抛砖引玉,力求简单,易懂,并非完整的架构实现,更多丰富的功能实现一切皆有可能,只要在理解的基础上。