博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
MVC插件实现
阅读量:6948 次
发布时间:2019-06-27

本文共 20790 字,大约阅读时间需要 69 分钟。

      本人第一篇随笔,在园子里逛了这么久,今天也记录一篇自己的劳动成果,也是给自己以后留个记录。

    最近领导让我搞一下插件化,就是实现多个web工程通过配置文件进行组装。之前由于做过一个简单的算是有点经验,当时使用的不是area,后来通过翻看orchard源码有点启发,打算使用area改一下。

    实现插件化,需要解决四个问题:

          1、如何发现插件以及加载插件及其所依赖的dll

          2、如何注册路由,正确调用插件的Controller和Action

          3、如何实现ViewEngine,正确的发现View

          4、页面中的Url如何自动生成

 以下下我们带着这四个问题依次分析解决:

 1、如何发现插件以及加载插件及其所依赖的dll

     该问题我完全使用了Nop插件的实现方式,为每个工程定义一个Plugin.txt配置文件,运行时通过注册[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]这个方法,在Application_Start()之前发现和加载插件。PluginManager负责管理加载插件,通过解析Plugin.txt,识别插件的dll和它所依赖的dll。通过Assembly.Load()方法加载dll并使用BuildManager.AddReferencedAssembly(shadowCopiedAssembly)为web项目动态添加引用。由于web项目存在不同的信任级别,在FullTrust级别可以将这些dll直接拷贝到AppDomain.CurrentDomain.DynamicDirectory文件夹下面。但是在其他信任级别下无法访问该目录,Nop通过复制到一个临时目录并在web.config中修改 <probingprivatePath="Plugins/bin/" />的值来让iis自动探索该目录。

代码如下:

using System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Reflection;using System.Text;using System.Threading.Tasks;namespace Framework.Core.Plugins{   public class Plugin    {        ///         /// 插件名称,唯一标识        ///         public string PluginName { get; set; }        ///         /// 插件显示名称        ///         public virtual string PluginFriendlyName { get; set; }        ///         /// 插件主文件(DLL)名称        ///         public string PluginFileName { get; set; }        ///         /// 插件控制器命名空间        ///         public string ControllerNamespace { get; set; }        ///         /// 插件主文件文件信息        ///         public virtual FileInfo PluginFileInfo { get; internal set; }        ///         /// 插件程序集        ///         public virtual Assembly ReferencedAssembly { get; internal set; }        ///         /// 描述        ///         public virtual string Description { get; set; }        ///         /// 显示顺序        ///         public virtual int DisplayOrder { get; set; }        ///         /// 是否已安装        ///         public virtual bool Installed { get; set; }    }}
View Code
using System;using System.Collections.Generic;using System.Configuration;using System.Diagnostics;using System.IO;using System.Linq;using System.Reflection;using System.Threading;using System.Web;using System.Web.Compilation;using Framework.Core.Plugins;using Framework.Core.Infrastructure;[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]namespace Framework.Core.Plugins{    public class PluginManager    {        #region Const        private const string InstalledPluginsFilePath = "~/App_Data/InstalledPlugins.txt";        private const string PluginsPath = "~/Plugins";        private const string ShadowCopyPath = "~/Plugins/bin";        #endregion        #region Fields        private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();        private static DirectoryInfo _shadowCopyFolder;        private static bool _clearShadowDirectoryOnStartup;        #endregion        #region Methods        public static IEnumerable
ReferencedPlugins { get; set; } ///
/// 初始化插件 /// public static void Initialize() { using (new WriteLockDisposable(Locker)) { var pluginFolder = new DirectoryInfo(CommonHelper.MapPath(PluginsPath)); _shadowCopyFolder = new DirectoryInfo(CommonHelper.MapPath(ShadowCopyPath)); var referencedPlugins = new List
(); _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) && Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]); try { //获取已经加载的插件名称 var installedPluginNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); Debug.WriteLine("创建临时目录"); Directory.CreateDirectory(pluginFolder.FullName); Directory.CreateDirectory(_shadowCopyFolder.FullName); //获取临时目录中的dll文件 var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories); if (_clearShadowDirectoryOnStartup) { //清除临时目录中的数据 foreach (var f in binFiles) { Debug.WriteLine("删除文件: " + f.Name); try { File.Delete(f.FullName); } catch (Exception exc) { Debug.WriteLine("删除文件异常: " + f.Name + ". 异常信息: " + exc); } } } //加载插件 foreach (var dfd in GetPluginFilesAndPlugins(pluginFolder)) { var pluginFile = dfd.Key; var plugin = dfd.Value; //验证插件名称 if (String.IsNullOrWhiteSpace(plugin.PluginName)) throw new Exception(string.Format("插件:'{0}' 没有设置名称. 请设置唯一的PluginName,重新编译.", pluginFile.FullName)); if (referencedPlugins.Contains(plugin)) throw new Exception(string.Format("插件名称:'{0}' 已经被占用,请重新设置唯一的PluginName,重新编译", plugin.PluginName)); //设置是否已经安装 plugin.Installed = installedPluginNames .FirstOrDefault(x => x.Equals(plugin.PluginName, StringComparison.InvariantCultureIgnoreCase)) != null; try { if (pluginFile.Directory == null) throw new Exception(string.Format("'{0}'插件目录无效,无法解析插件dll文件", pluginFile.Name)); //获取插件中的所有DLL var pluginDLLs = pluginFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories) //just make sure we're not registering shadow copied plugins .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName)) .Where(x => IsPackagePluginFolder(x.Directory)) .ToList(); //获取主插件文件 var mainPluginDLL = pluginDLLs .FirstOrDefault(x => x.Name.Equals(plugin.PluginFileName, StringComparison.InvariantCultureIgnoreCase)); plugin.PluginFileInfo = mainPluginDLL; //复制主文件到临时目录,并加载主文件 plugin.ReferencedAssembly = PerformFileDeploy(mainPluginDLL); //加载其他插件相关dll foreach (var dll in pluginDLLs .Where(x => !x.Name.Equals(mainPluginDLL.Name, StringComparison.InvariantCultureIgnoreCase)) .Where(x => !IsAlreadyLoaded(x))) PerformFileDeploy(dll); referencedPlugins.Add(plugin); } catch (ReflectionTypeLoadException ex) { var msg = string.Format("Plugin '{0}'. ", plugin.PluginFriendlyName); foreach (var e in ex.LoaderExceptions) msg += e.Message + Environment.NewLine; var fail = new Exception(msg, ex); throw fail; } catch (Exception ex) { var msg = string.Format("Plugin '{0}'. {1}", plugin.PluginFriendlyName, ex.Message); var fail = new Exception(msg, ex); throw fail; } } } catch (Exception ex) { var msg = string.Empty; for (var e = ex; e != null; e = e.InnerException) msg += e.Message + Environment.NewLine; var fail = new Exception(msg, ex); throw fail; } ReferencedPlugins = referencedPlugins; } } ///
/// 安装插件 /// ///
插件名称 public static void MarkPluginAsInstalled(string pluginName) { if (String.IsNullOrEmpty(pluginName)) throw new ArgumentNullException("pluginName"); var filePath = CommonHelper.MapPath(InstalledPluginsFilePath); if (!File.Exists(filePath)) using (File.Create(filePath)) { } var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); bool alreadyMarkedAsInstalled = installedPluginSystemNames .FirstOrDefault(x => x.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)) != null; if (!alreadyMarkedAsInstalled) installedPluginSystemNames.Add(pluginName); PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath); } ///
/// 卸载插件 /// ///
插件名称 public static void MarkPluginAsUninstalled(string pluginName) { if (String.IsNullOrEmpty(pluginName)) throw new ArgumentNullException("pluginName"); var filePath = CommonHelper.MapPath(InstalledPluginsFilePath); if (!File.Exists(filePath)) using (File.Create(filePath)) { } var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); bool alreadyMarkedAsInstalled = installedPluginSystemNames .FirstOrDefault(x => x.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)) != null; if (alreadyMarkedAsInstalled) installedPluginSystemNames.Remove(pluginName); PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath); } ///
/// 卸载所有插件 /// public static void MarkAllPluginsAsUninstalled() { var filePath = CommonHelper.MapPath(InstalledPluginsFilePath); if (File.Exists(filePath)) File.Delete(filePath); } #endregion #region 工具 ///
///获取指定目录下的所有插件文件(Plugin.text)和插件信息(Plugin) /// ///
Plugin目录 ///
插件文件和插件
private static IEnumerable
> GetPluginFilesAndPlugins(DirectoryInfo pluginFolder) { if (pluginFolder == null) throw new ArgumentNullException("pluginFolder"); var result = new List
>(); //add display order and path to list foreach (var descriptionFile in pluginFolder.GetFiles("Plugin.txt", SearchOption.AllDirectories)) { if (!IsPackagePluginFolder(descriptionFile.Directory)) continue; //解析插件配置文件 var plugin = PluginFileParser.ParsePluginFile(descriptionFile.FullName); result.Add(new KeyValuePair
(descriptionFile, plugin)); } //插件排序,数字越低排名越高 result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder)); return result; } ///
/// 判断程序集是否已经加载 /// ///
程序集文件 ///
Result
private static bool IsAlreadyLoaded(FileInfo fileInfo) { try { string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName); if (fileNameWithoutExt == null) throw new Exception(string.Format("无法获取文件名:{0}", fileInfo.Name)); foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) { string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault(); if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase)) return true; } } catch (Exception exc) { Debug.WriteLine("无法判断程序集是否加载。" + exc); } return false; } ///
///执行解析文件 /// ///
插件文件 ///
Assembly
private static Assembly PerformFileDeploy(FileInfo plug) { if (plug.Directory.Parent == null) throw new InvalidOperationException("插件" + plug.Name + ":目录无效" ); FileInfo shadowCopiedPlug; if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted) { //运行在MediumTrust下(在MediumTrust下无法访问DynamicDirectory,也无法设置ResolveAssembly event) //需要将所有插件dll都需要拷贝到~/Plugins/bin/下的临时目录,因为web.config中的probingPaths设置的是该目录 var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName); shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder); } else { //运行在FullTrust下,可以直接使用标准的DynamicDirectory文件夹,作为临时目录 var directory = AppDomain.CurrentDomain.DynamicDirectory; Debug.WriteLine(plug.FullName + " to " + directory); shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory)); } //加载程序集 var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName)); //添加引用信息到BuildManager Debug.WriteLine("添加到BuildManager: '{0}'", shadowCopiedAssembly.FullName); BuildManager.AddReferencedAssembly(shadowCopiedAssembly); return shadowCopiedAssembly; } ///
/// FullTrust级别下的插件初始化 /// ///
///
///
private static FileInfo InitializeFullTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder) { var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name)); try { File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } catch (IOException) { Debug.WriteLine(shadowCopiedPlug.FullName + " 文件已被锁, 尝试重命名"); //可能被 devenv锁住,可以通过重命名来解锁 try { var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old"; File.Move(shadowCopiedPlug.FullName, oldFile); } catch (IOException exc) { throw new IOException(shadowCopiedPlug.FullName + " 重命名失败, 无法初始化插件", exc); } //重新尝试复制 File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } return shadowCopiedPlug; } ///
/// MediumTrust级别下的插件初始化 /// ///
///
///
private static FileInfo InitializeMediumTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder) { var shouldCopy = true; var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name)); //检查插件是否存在,如果存在,判断是否需要更新 if (shadowCopiedPlug.Exists) { var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plug.CreationTimeUtc.Ticks; if (areFilesIdentical) { Debug.WriteLine("插件已经存在,不需要更新: '{0}'", shadowCopiedPlug.Name); shouldCopy = false; } else { //删除现有插件 Debug.WriteLine("有新插件; 删除现有插件: '{0}'", shadowCopiedPlug.Name); File.Delete(shadowCopiedPlug.FullName); } } if (shouldCopy) { try { File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } catch (IOException) { Debug.WriteLine(shadowCopiedPlug.FullName + " 文件已被锁, 尝试重命名"); //可能被 devenv锁住,可以通过重命名来解锁 try { var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old"; File.Move(shadowCopiedPlug.FullName, oldFile); } catch (IOException exc) { throw new IOException(shadowCopiedPlug.FullName + " 重命名失败, 无法初始化插件", exc); } //重新尝试复制 File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } } return shadowCopiedPlug; } ///
///判断文件是否属于插件目录下的文件(Plugins下) /// ///
///
private static bool IsPackagePluginFolder(DirectoryInfo folder) { if (folder == null) return false; if (folder.Parent == null) return false; if (!folder.Parent.Name.Equals("Plugins", StringComparison.InvariantCultureIgnoreCase)) return false; return true; } ///
/// 获取InstalledPlugins.txt文件的物理路径 /// ///
private static string GetInstalledPluginsFilePath() { return CommonHelper.MapPath(InstalledPluginsFilePath); } #endregion }}
View Code

 

2、如何注册路由,正确调用插件的Controller和Action

    路由我通过扩展现Mvc的RouteCollection的MapRoute方法,将插件名称作为area强行插入到DataToken中,这样在ViewEngine中可以使用area规则来发现视图。然后重写RegisterRoutes方法,通过遍历所有插件集合,添加指定的路由,并将所有插件的Controller的命名空间写入到插件匹配模式中,这样可以解决不同插件之间Controller重名的问题。

public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces,string area)        {            if (routes == null)            {                throw new ArgumentNullException("routes");            }            if (url == null)            {                throw new ArgumentNullException("url");            }            Route route = new Route(url, new MvcRouteHandler())            {                Defaults = new RouteValueDictionary(defaults),                Constraints = new RouteValueDictionary(constraints),                DataTokens = new RouteValueDictionary()            };            if ((namespaces != null) && (namespaces.Length > 0))            {                route.DataTokens["Namespaces"] = namespaces;            }            if (!string.IsNullOrEmpty(area))            {                route.DataTokens["area"] = area;            }            routes.Add(name, route);            return route;        }
View Code
public static void RegisterPluginRoutes(RouteCollection routes)        {            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");            foreach (var plugin in PluginManager.ReferencedPlugins)            {                    routes.MapRoute(plugin.PluginName,                    string.Concat(plugin.PluginName, "/{controller}/{action}/{id}"),                    new { area= plugin.PluginName, controller = "Home", action = "Index", id = UrlParameter.Optional },                   new string[]{ plugin.ControllerNamespace}, plugin.PluginName);            }            routes.MapRoute(                 name: "Default",                 url: "{controller}/{action}/{id}",                 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },                 namespaces:new string[] { "GWT.Framework.Web.Controllers" }              );            }
View Code

3、如何实现ViewEngine,正确的发现View

   关于这个问题我发现Nop和Orchard中好多地方都是硬编码,通过VIEW(~/Plugin/XXX/views/XXX/XX.csthml)的方式来发现视图。不知他们是何用意,我觉这样耦合度过高。此处我通过前面路由中插入的area并配合实现一个继承自RazorViewEngine的视图引擎,将所有的插件请求定位到~/Plugins/{area}/Views/{controller}/{action}.cshtml。同时替换掉原有的视图引擎。代码如下:

 

public class PluginViewEngine : RazorViewEngine    {        public PluginViewEngine()        {            AreaViewLocationFormats = new[] {                "~/Areas/{2}/Views/{1}/{0}.cshtml",                "~/Areas/{2}/Views/Shared/{0}.cshtml",                "~/Plugins/{2}/Views/{1}/{0}.cshtml",                "~/Plugins/{2}/Views/Shared/{0}.cshtml"            };            AreaMasterLocationFormats = new[] {                "~/Areas/{2}/Views/{1}/{0}.cshtml",                "~/Areas/{2}/Views/Shared/{0}.cshtml",                 "~/Plugins/{2}/Views/{1}/{0}.cshtml",                "~/Plugins/{2}/Views/Shared/{0}.cshtml"            };            AreaPartialViewLocationFormats = new[] {                "~/Areas/{2}/Views/{1}/{0}.cshtml",                "~/Areas/{2}/Views/Shared/{0}.cshtml",                "~/Plugins/{2}/Views/{1}/{0}.cshtml",                "~/Plugins/{2}/Views/Shared/{0}.cshtml"            };            FileExtensions = new[] { "cshtml" };        }    }
View Code
protected void Application_Start()        {            ViewEngines.Engines.Clear();            ViewEngines.Engines.Add(new PluginViewEngine());            AreaRegistration.RegisterAllAreas();            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);            ApplicationStartup.RegisterPluginRoutes(RouteTable.Routes);            BundleConfig.RegisterBundles(BundleTable.Bundles);        }
View Code

 4、页面中的Url如何自动生成

   我们知道页面中的url可以使用硬编码方式比如/Home/Index,也可以使用Html.ActionLink(“Index”,“Home”)或者Url.Action方式实现。前者硬编码的方式已经不适用于插件化,因为开发者不知道是否会被用作插件,如果强行写入/Pluin1/Home/Index,势必导致本地无法运行。在插件系统中应该使用后两者,因为他们都是用过路由系统输出URL的。MVC框架会基于当前的Controller到路由系统中找到匹配的路径返回给前台页面。

   对于URL我们可以使用Html和Url帮助器生成,但是对于Script和css等内容文件MVC框架就无能为力了。为了解决内容文件的加载,我扩展了UrlHelper帮助器,根据当前的请求中是否有area来生成相对路径。代码如下

public static string PluginContent(this UrlHelper urlHelper, string url)        {            if (urlHelper.RequestContext.RouteData.Values.Keys.Contains("area"))            {                var area = urlHelper.RequestContext.RouteData.Values["area"].ToString();                if (!string.IsNullOrEmpty(area))                {                    url = url.Substring(url.IndexOf("/") + 1);                    return string.Format("~/Plugins/{0}/{1}", area, url);                }            }            return url;        }
View Code

在页面中可以如下调用: @Url.PluginContent("/Views/Shared/_Layout.cshtml")

 

参考文档:

https://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx

http://www.cnblogs.com/longyunshiye/p/5786446.html

 

转载于:https://www.cnblogs.com/pippenxu/p/6606621.html

你可能感兴趣的文章
jQuery基础之二(操作标签)
查看>>
关于swift语言中导入OC三方类找不到头文件的解决方法
查看>>
ALV中处理过滤掉的行
查看>>
PHP通过url下载远程图片到本地
查看>>
测试RESTful API利器-Postman
查看>>
十步完全理解 SQL(转载)
查看>>
windows 8 rtm installation key
查看>>
(一) request
查看>>
C#多线程间的同步问题
查看>>
[深度学习大讲堂]文化、进化与局部最小值
查看>>
Nginx
查看>>
【Error】IOError: [Errno 22] invalid mode
查看>>
repmgr学习记录(搭建主从复制)
查看>>
BinaryFormatter探讨
查看>>
用keras 和 tensorflow 构建手写字识别神经网路
查看>>
HDU 4101 Ali and Baba
查看>>
民意调查Django实现(三)
查看>>
[2018-01-13] 安装Django的一些笔记
查看>>
SQL Server 高性能写入的一些总结
查看>>
有关T-SQL的10个好习惯
查看>>