MVC插件

MVC插件

最近领导让我搞一下插件化,就是实现多个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自动探索该目录。

代码如下:

MVC插件
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
{
/// <summary>
/// 插件名称,唯一标识
/// </summary>
public string PluginName { get; set; } /// <summary>
/// 插件显示名称
/// </summary>
public virtual string PluginFriendlyName { get; set; } /// <summary>
/// 插件主文件(DLL)名称
/// </summary>
public string PluginFileName { get; set; } /// <summary>
/// 插件控制器命名空间
/// </summary>
public string ControllerNamespace { get; set; } /// <summary>
/// 插件主文件文件信息
/// </summary>
public virtual FileInfo PluginFileInfo { get; internal set; } /// <summary>
/// 插件程序集
/// </summary>
public virtual Assembly ReferencedAssembly { get; internal set; } /// <summary>
/// 描述
/// </summary>
public virtual string Description { get; set; } /// <summary>
/// 显示顺序
/// </summary>
public virtual int DisplayOrder { get; set; } /// <summary>
/// 是否已安装
/// </summary>
public virtual bool Installed { get; set; }
}
}
MVC插件
MVC插件
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<Plugin> ReferencedPlugins { get; set; } /// <summary>
/// 初始化插件
/// </summary>
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<Plugin>(); _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; }
} /// <summary>
/// 安装插件
/// </summary>
/// <param name="pluginName">插件名称</param>
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);
} /// <summary>
/// 卸载插件
/// </summary>
/// <param name="pluginName">插件名称</param>
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);
} /// <summary>
/// 卸载所有插件
/// </summary>
public static void MarkAllPluginsAsUninstalled()
{
var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
if (File.Exists(filePath))
File.Delete(filePath);
} #endregion #region 工具 /// <summary>
///获取指定目录下的所有插件文件(Plugin.text)和插件信息(Plugin)
/// </summary>
/// <param name="pluginFolder">Plugin目录</param>
/// <returns>插件文件和插件</returns>
private static IEnumerable<KeyValuePair<FileInfo, Plugin>> GetPluginFilesAndPlugins(DirectoryInfo pluginFolder)
{
if (pluginFolder == null)
throw new ArgumentNullException("pluginFolder"); var result = new List<KeyValuePair<FileInfo, Plugin>>();
//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<FileInfo, Plugin>(descriptionFile, plugin));
}
//插件排序,数字越低排名越高
result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
return result;
} /// <summary>
/// 判断程序集是否已经加载
/// </summary>
/// <param name="fileInfo">程序集文件</param>
/// <returns>Result</returns>
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;
} /// <summary>
///执行解析文件
/// </summary>
/// <param name="plug">插件文件</param>
/// <returns>Assembly</returns>
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;
} /// <summary>
/// FullTrust级别下的插件初始化
/// </summary>
/// <param name="plug"></param>
/// <param name="shadowCopyPlugFolder"></param>
/// <returns></returns>
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;
} /// <summary>
/// MediumTrust级别下的插件初始化
/// </summary>
/// <param name="plug"></param>
/// <param name="shadowCopyPlugFolder"></param>
/// <returns></returns>
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;
} /// <summary>
///判断文件是否属于插件目录下的文件(Plugins下)
/// </summary>
/// <param name="folder"></param>
/// <returns></returns>
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;
} /// <summary>
/// 获取InstalledPlugins.txt文件的物理路径
/// </summary>
/// <returns></returns>
private static string GetInstalledPluginsFilePath()
{
return CommonHelper.MapPath(InstalledPluginsFilePath);
} #endregion
}
}
MVC插件

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

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

MVC插件
  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;
}
MVC插件
MVC插件
        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" }
); }
MVC插件

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

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

MVC插件
    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" };
}
}
MVC插件
MVC插件
 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);
}
MVC插件

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来生成相对路径。代码如下

MVC插件
        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; }
MVC插件

在页面中可以如下调用: @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

上一篇:我的cnblogs设置代码


下一篇:网络加速手段之一,JS文件资源合并下载