最近在开发一个MVC框架,开发过程中考虑到以后开发依托于框架的项目,为了框架的维护更新升级,代码肯定要和具体的业务工程分割开来,所以需要解决业务工程挂载在框架工程的问题,MVC与传统的ASP.NET不同,WebForm项目只需要挂在虚拟目录拷贝dll就可以访问,但是MVC不可能去引用工程项目的dll重新编译,从而产生了开发一个动态挂在MVC项目功能的想法,MVC项目挂载主要有几个问题,接下来进行详细的分析与完成解决方案

一般动态加载dll的方法是使用Assembly.LoadFIle的方法来调用,但是会存在如下问题:

1.如果MVC项目中存在依赖注入,框架层面无法将外部dll的类放入IOC容器

通过 BuildManager.AddReferencedAssembly方法在MVC项目启动前,动态将外部代码添加到项目的编译体系中,需要配合PreApplicationStartMethod注解使用,示例:

声明一个类,然后进行注解标记,指定MVC启动前方法

//使用PreApplicationStartMethod注解的作用是在mvc应用启动之前执行操作
[assembly: PreApplicationStartMethod(typeof(FastExecutor.Base.Util.PluginUtil), "PreInitialize")]
namespace FastExecutor.Base.Util
{
public class PluginUtil
{
public static void PreInitialize()
{ }
}
}

2.外部加载的dll中的Controller无法被识别

通过自定义的ControllerFactory重写GetControllerType方法进行识别

 public class FastControllerFactory : DefaultControllerFactory
{ protected override Type GetControllerType(RequestContext requestContext, string controllerName)
{
Type ControllerType = PluginUtil.GetControllerType(controllerName + "Controller");
if (ControllerType == null)
{
ControllerType = base.GetControllerType(requestContext, controllerName);
}
return ControllerType;
}
}

在Global.asax文件中进行ControllerFactory的替换

ControllerBuilder.Current.SetControllerFactory(new FastControllerFactory());

ControllerTypeDic是遍历外部dll获取到的所有Controller,这里需要考虑到Controller通过RoutePrefix注解自定义Controller前缀的情况

                IEnumerable<Assembly> assemblies = GetProjectAssemblies();
foreach (var assembly in assemblies)
{
foreach (var type in assembly.GetTypes())
{
if (type.GetInterface(typeof(IController).Name) != null && type.Name.Contains("Controller") && type.IsClass && !type.IsAbstract)
{
string Name = type.Name;
//如果有自定义的路由注解
if (type.IsDefined(typeof(System.Web.Mvc.RoutePrefixAttribute), false))
{
var rounteattribute = type.GetCustomAttributes(typeof(System.Web.Mvc.RoutePrefixAttribute), false).FirstOrDefault();
Name = ((System.Web.Mvc.RoutePrefixAttribute)rounteattribute).Prefix + "Controller";
}
if (!ControllerTypeDic.ContainsKey(Name))
{
ControllerTypeDic.Add(Name, type);
}
}
}
BuildManager.AddReferencedAssembly(assembly);
}

3.加载dll后如果要更新业务代码,dll会被锁定,无法替换,需要重启应用

解决办法是通过AppDomain对业务项目dll独立加载,更新时进行卸载

1)创建一个RemoteLoader一个可穿越边界的类,作为加载dll的一个包装

 public class RemoteLoader : MarshalByRefObject
{
private Assembly assembly; public Assembly LoadAssembly(string fullName)
{
assembly = Assembly.LoadFile(fullName);
return assembly;
} public string FullName
{
get { return assembly.FullName; }
} }

2)创建LocalLoader作为AppDomian创建与卸载的载体

public class LocalLoader
{
private AppDomain appDomain;
private RemoteLoader remoteLoader;
private DirectoryInfo MainFolder;
public LocalLoader()
{ AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation; setup.ShadowCopyDirectories = setup.ApplicationBase;
appDomain = AppDomain.CreateDomain("PluginDomain", null, setup); string name = Assembly.GetExecutingAssembly().GetName().FullName;
remoteLoader = (RemoteLoader)appDomain.CreateInstanceAndUnwrap(
name,
typeof(RemoteLoader).FullName);
} public Assembly LoadAssembly(string fullName)
{
return remoteLoader.LoadAssembly(fullName);
} public void Unload()
{
AppDomain.Unload(appDomain);
appDomain = null;
} public string FullName
{
get
{
return remoteLoader.FullName;
}
}
}

这里需要说明的,AppDomainSetup配置文件请使用AppDomain.CurrentDomain.SetupInformation也就是使用框架的作用于配置信息,因为业务代码会引用到很多框架的dll,如果独立创建配置信息,会有找不到相关dll的错误,同时这里也需要配置web.confg文件指定额外的dll搜索目录,因为业务工程代码也会有很多层多个dll相互引用,不指定目录也会存在找不到依赖dll的错误

<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<!--插件加载目录-->
<probing privatePath="PluginTemp" />
</assemblyBinding>
</runtime>

3)创建业务代码文件夹Plugin与临时dll文件夹PluginTemp

为什么要创建临时文件夹呢,因为我们需要在PluginTemp真正的加载dll,然后监听Plugin文件夹的文件变化,有变化时进行AppDomain卸载这个操作,将Plugin中的dll拷贝到PluginTemp文件夹中,再重新加载dll

监听Plugin文件夹:

private static readonly FileSystemWatcher _FileSystemWatcher = new FileSystemWatcher();
public static void StartWatch()
{
_FileSystemWatcher.Path = HostingEnvironment.MapPath("~/Plugin");
_FileSystemWatcher.Filter = "*.dll";
_FileSystemWatcher.Changed += _fileSystemWatcher_Changed; _FileSystemWatcher.IncludeSubdirectories = true;
_FileSystemWatcher.NotifyFilter =
NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size;
_FileSystemWatcher.EnableRaisingEvents = true;
}
private static void _fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{
DllList.Clear();
Initialize(false);
InjectUtil.InjectProject();
}

拷贝dll:

 if (PluginLoader == null)
{
PluginLoader = new LocalLoader();
}
else
{
PluginLoader.Unload();
PluginLoader = new LocalLoader();
} TempPluginFolder.Attributes = FileAttributes.Normal & FileAttributes.Directory;
PluginFolder.Attributes = FileAttributes.Normal & FileAttributes.Directory;
//清理临时文件。
foreach (var file in TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
{
try
{
File.SetAttributes(file.FullName, FileAttributes.Normal);
file.Delete();
}
catch (Exception)
{
//这里虽然能成功删除,但是会报没有权限的异常,就不catch了
} }
//复制插件进临时文件夹。
foreach (var plugin in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
{
try
{
string CopyFilePath = Path.Combine(TempPluginFolder.FullName, plugin.Name);
File.Copy(plugin.FullName, CopyFilePath, true);
File.SetAttributes(CopyFilePath, FileAttributes.Normal);
}
catch (Exception)
{
//这里虽然能成功删除,但是会报没有权限的异常,就不catch了
}
}

注:这里有个问题一直没解决,就是删除文件拷贝文件的时候,AppDomain已经卸载,但是始终提示无权限错误,但是操作又是成功的,暂时还未解决,如果大家有解决方法可以一起交流下

加载dll:

  public static IEnumerable<Assembly> GetProjectAssemblies()
{
if (DllList.Count==)
{
IEnumerable<Assembly> assemblies = TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories).Select(x => PluginLoader.LoadAssembly(x.FullName));
foreach (Assembly item in assemblies)
{
DllList.Add(item);
}
}
return DllList;
}

4.业务代码的cshtml页面如何加入到框架中被访问

在MVC工程中,cshtml也是需要被编译的,我们可以通过RazorBuildProvider将外部编译的页面动态加载进去

 public static void InitializeView()
{
IEnumerable<Assembly> assemblies = GetProjectAssemblies();
foreach (var assembly in assemblies)
{
RazorBuildProvider.CodeGenerationStarted += (object sender, EventArgs e) =>
{
RazorBuildProvider provider = (RazorBuildProvider)sender;
provider.AssemblyBuilder.AddAssemblyReference(assembly);
};
} }

RazorBuildProvider方法啊只是在路由层面将cshtml加入到框架中,我们还需要将业务工程View中模块的页面挂载虚拟目录到框架中,如图所示

5.框架启动后,更新业务dll带来的相关问题

在启动的项目中我们更新dll,我们希望达到的效果是和更新框架bin目录文件的dll一样,程序会重启,这样就会再次调用被PreApplicationStartMethod注解标注的方法,不需要在代码中做额外处理判断是首次加载还是更新加载,同时也做不到动态的将外部dll加入到MVC编译dll体系中,也只能启动前加载,查了很多资料,重新加载项目可以通过代码控制IIS回收程序池达到效果,但是因为各种繁琐的权限配置问题而放弃,我最后的解决方法是比较歪门邪道的方法,更新web.config文件的修改日期,因为iis会监控配置文件,更新了会重启引用,大家如果有更好的简单的方法,可以评论回复我呦

//这里通过修改webconfig文件的时间达到重启应用,加载项目dll的目的!
File.SetLastWriteTime(HostingEnvironment.MapPath("~/Web.config"), System.DateTime.Now);

博客园没找到资源上传地址,传到码云上了,放个地址:https://gitee.com/grassprogramming/FastExecutor/attach_files

ASP.NET MVC模块化开发——动态挂载外部项目的更多相关文章

  1. Asp.net Mvc模块化开发之分区扩展框架

    对于一个企业级项目开发,模块化是非常重要的. 默认Mvc框架的AreaRegistration对模块化开发真的支持很好吗?真的有很多复杂系统在使用默认的分区开发的吗?我相信大部分asp.net的技术团 ...

  2. Asp.net Mvc模块化开发系列(目录)

    模块化开发是非常重要的,模块化开发是个系统性问题,为此我觉得有必须要写一个系列的文章才能基本说的清楚 那又为什么要写一个目录呢? 其一.是对我昨天承诺写一个系列新的文章的回应 其二.是先写出一个大纲, ...

  3. asp.net MVC提高开发速度(创建项目模板)

  4. Asp.net Mvc模块化开发之“开启模块开发、调试的简单愉快之旅”

    整个世界林林种种,把所有的事情都划分为对立的两个面. 每个人都渴望的财富划分为富有和贫穷,身高被划分为高和矮,身材被划分为胖和瘦,等等. 我们总是感叹,有钱人的生活我不懂;有钱人又何尝能懂我们每天起早 ...

  5. Asp.net Mvc模块化开发之“部分版本部分模块更新(上线)”

    项目开发从来就不是一个简单的问题.更难的问题是维护其他人开发的项目,并且要修改bug.如果原系统有重大问题还需要重构. 怎么重构系统不是本文探讨的问题,但是重构后如何上线部署和本文关系密切.这个大家可 ...

  6. net Mvc模块化开发

    Asp.net Mvc模块化开发之“部分版本部分模块更新(上线)” 项目开发从来就不是一个简单的问题.更难的问题是维护其他人开发的项目,并且要修改bug.如果原系统有重大问题还需要重构. 怎么重构系统 ...

  7. 全面解析ASP.NET MVC模块化架构方案

    什么叫架构?揭开架构神秘的面纱,无非就是:分层+模块化.任意复杂的架构,你也会发现架构师也就做了这两件事. 本文将会全面的介绍我们团队在模块化设计方面取得的经验.之所以加了“全面”二字,是因为本文的内 ...

  8. ASP.NETCORE MVC模块化

    ASP.NETCORE MVC模块化编程 前言 记得上一篇博客中跟大家分享的是基于ASP.NETMVC5,实际也就是基于NETFRAMEWORK平台实现的这么一个轻量级插件式框架.那么今天我主要分享的 ...

  9. Easyui + asp.net mvc + sqlite 开发教程(录屏)适合入门

    Easyui + asp.net mvc + sqlite 开发教程(录屏)适合入门 第一节: 前言(技术简介) EasyUI 是一套 js的前端框架 利用它可以快速的开发出好看的 前端系统 web ...

随机推荐

  1. 大数相乘----C语言

    /* 大数相乘: 因为是大数,乘积肯定超出了能定义的范围,因此考虑用数组存储,定义三个数组,分别存储乘数,被乘数和积. 规则与平常手算一样,从个位开始分别与被乘数的每一位相乘,但是有一点不同的是:我们 ...

  2. React中的生命周期函数

    React的生命周期函数 什么是生命周期函数:生命周期函数是指在某一个时刻组件会自动调用执行的函数 Initialization:初始化 执行Constructor,初始state和props Mou ...

  3. 实用,Windows后台守护进程iNeuDaemon发布。Linux操作系统下使用使用supervisor

    目       录 1.      概述... 1 2.      iNeuDaemon部署... 2 3.      iNeuDaemon配置监控服务项... 3 4.      应用效果... 3 ...

  4. C++ 解决列车重排问题

    问题节选自<<数据结构.算法与应用(C++语言描述)>>, 思路与代码为原创, 如有疏漏及问题欢迎指正 问题描述: 一辆列车有n节车厢, 车厢排列乱序(如: 284657139 ...

  5. No mapping found for HTTP request with URI [/SLSaleSystem/js/jquery.dataTables.min.js] in DispatcherServlet with name 'spring' 静态资源文件访问不到,无解!!!!!!!

    报错信息:   网上三种修改 web.xml 文件方法尝试未果 尝试未果:<mvc:default-servlet-handler/> 尝试未果:方法2:直接告诉spring,这个你就得这 ...

  6. Redis实现访问控制频率

    为什么限制访问频率 做服务接口时通常需要用到请求频率限制 Rate limiting,例如限制一个用户1分钟内最多可以范围100次 主要用来保证服务性能和保护数据安全 因为如果不进行限制,服务调用者可 ...

  7. ssh远程连接到Ubuntu

    1.ubuntu首先得安装ssh sudo apt-get install openssh-server 2.启动ssh sudo /etc/init.d/ssh start 3.检查是否开启 ps ...

  8. cf1276B

    题意简述:给出无向图,会有重边,然后给你两个点a,b,让你计算有多少点对(x,y)满足从x到y的所有路径都经过a和b 题解:先从a,b两点出发进行dfs,dfs的过程中不能经过a,b两点(除了开始) ...

  9. [大数据技术]datax的安装以及使用

    1.datax简述 DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL.Oracle.SqlServer.Postgre.HDFS.Hive.ADS.HBase.Ta ...

  10. Scout YYF I POJ - 3744【矩阵乘法优化求概率】

    题意: 一条路上有 $n$ 个地雷,YYF 从位置 $1$ 出发,走一步的概率为 $p$,走两步的概率是 $(1-p)$.求 YYF 能顺利通过这条路的概率. 数据范围: $1\leq n \leq ...