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

January 7, 2011 10:06

I’ve recently spent quite a lot of time researching and
prototyping different ways to create a plugin engine in ASP.NET MVC3 and
primarily finding a nice way to load plugins (DLLs) in from outside of
the ‘bin’ folder. Although this post focuses on MVC3, I am sure that the
same principles will apply for other MVC versions.

The Issues

Loading
DLLs from outside of the ‘bin’ folder isn’t really anything new or
cutting edge, however when working with MVC this becomes more difficult.
This is primarily due to how MVC loads/finds types that it needs to
process including controllers, view models (more precisely the generic
argument passed to a ViewPage or used with the @model declaration in Razor), model binders, etc… MVC is very tied to the BuildManager
which is the mechanism for compiling views, and locating other services
such as controllers. By default the BuildManager is only familiar with
assembies in the ‘bin’ folder and in the GAC, so if you start putting
DLLs in folders outside of the ‘bin’ then it won’t be able to locate the
MVC services and objects that you might want it to be referencing.

Another
issue that needs to be dealt with is DLL file locking. When a plugin
DLL is loaded and is in use the CLR will lock the file. This becomes an
an issue if developers want to update the plugin DLL while the website
is running since they won’t be able to unless they bump the web.config
or take the site down. This holds true for MEF and how it loads DLLs as well.

.Net 4 to the rescue… almost

One
of the new features in .Net 4 is the ability to execute code before the
app initializes which compliments another new feature of the
BuildManager that lets you add assembly references to it at runtime
(which must be done on application pre-init). Here’s a nice little
reference to these new features from Phil Haack: http://haacked.com/archive/2010/05/16/three-hidden-extensibility-gems-in-asp-net-4.aspx
This is essential to making a plugin framework work with MVC so that
the BuildManager knows where to reference your plugin DLLs outside of
the ‘bin’. However, this isn’t the end of the story.

Strongly typed Views with model Types located in plugin DLLs

Unfortunately
if you have a view that is strongly typed to a model that exists
outside of the ‘bin’, then you’ll find out very quickly that it doesn’t
work and it won’t actually tell you why. This is because the RazorViewEngine  uses the BuildManager to compile the view into a dynamic assembly but then uses Activator.CreateInstance
to instantiate the newly compiled object. This is where the problem
lies, the current AppDomain doesn’t know how to resolve the model Type
for the strongly typed view since it doesn’t exist in the ‘bin’ or GAC. 
An even worse part about this scenario is that you don’t get any error
message telling you why this isn’t working, or where the problem is.
Instead you get the nice MVC view not found error: “…or its master was not found or no view engine supports the searched locations. The following locations were searched: ….”
telling you that it has searched for views in all of the ViewEngine
locations and couldn’t find it… which is actually not the error at all. 
Deep in the MVC3 source, it tries to instantiate the view object from
the dynamic assembly and it fails so it just keeps looking for that view
in the rest of the ViewEngine paths.

NOTE: Even though in MVC3 there’s a new IViewPageActivator
which should be responsible for instantiating the views that have been
compiled with the BuildManager, implementing a custom IViewPageActivator
to handle this still does not work because somewhere in the MVC3
codebase fails before the call to the IViewPageActivator which has to do
with resolving an Assembly that is not in the ‘bin’.

Full trust

When working in Full Trust we have a few options for dealing with the above scenario:

  • Use the AppDomain’s ResolveAssembly event

    • By subscribing to this event, you are able to instruct the AppDomain where to look when it can’t find a reference to a Type.
    • This
      is easily done by checking if your plugin assemblies match the assembly
      being searched for, and then returning the Assembly object if found:
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
var pluginsFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/Plugins"));
return (from f in pluginsFolder.GetFiles("*.dll", SearchOption.AllDirectories)
let assemblyName = AssemblyName.GetAssemblyName(f.FullName)
where assemblyName.FullName == args.Name || assemblyName.FullName.Split(',')[0] == args.Name
select Assembly.LoadFile(f.FullName)).FirstOrDefault();
}
  • Shadow copy your plugin DLLs into the AppDomain’s DynamicDirectory.

    • This is the directory that the BuildManager compiles it’s dynamic assemblies into and is also a directory that the AppDomain looks to when resolving Type’s from Assemblies.
    • You can shadow copy your plugin DLLs to this folder on app pre-init and everything ‘should just work’
  • Replace the RazorViewEngine with a custom razor view engine that compiles views manually but makes references to the appropriate plugin DLLs
    • I actually had this working in an Umbraco v5 prototype but it is hugely overkill and unnecessary plus you actually would have to replace the RazorViewEngine which is pretty absurd.

The burden of Medium Trust

In the MVC world there’s only a couple hurdles to jump when loading in plugins from outside of the ‘bin’ folder in Full Trust. In Medium Trust however, things get interesting. Unfortunately in Medium Trust it is not possible to handle the AssemblyResolve event and it’s also not possible to access the DynamicDirectory of the AppDomain so the above two solutions get thrown out the window. Further to this it seems as though you can’t use CodeDom in Medium Trust to custom compile views.

Previous attempts

For a while I began to think that this wasn’t possible and I thought I tried everything:

  • Shadow copying DLLs from the plugins folder into the ‘bin’ folder on application pre-init

    • This fails because even during app pre-init, the application pool will still recycle. Well, it doesn’t actually ‘fail’ unless you keep re-copying the DLL into the bin. If you check if it already exists and don’t copy into the bin than this solution will work for you but it’s hardly a ‘solution’ since you might as well just put all your DLLs into the ‘bin’ in the first place.
  • Trying to use sub folders of the ‘bin’ folder to load plugins.
    • Turns out that ASP.Net doesn’t by default load in DLLs that exist in sub folders of the bin, though from research it looks like standard .Net apps actually do.
    • Another interesting point was that if you try to copy a DLL into a sub folder of the bin during application pre-init you get a funky error:  “Storage scopes cannot be created when _AppStart is executing”. It seems that ASP.Net is monitoring all changes in the bin folder regardless of whether or not they are in sub folders but still doesn’t load or reference those assemblies.

An easy solution

So, the easy solution is to just set a ‘privatePath’ on the ‘probing’ element in your web.config to tell the AppDomain to also look for Assemblies/Types in the specified folders. I did try this before when trying to load plugins from sub folders in the bin and couldn’t get it to work. I’m not sure if I was ‘doing it wrong’ but it definitely wasn’t working then, either that or attempting to set this in sub folders of the bin just doesn’t work.

runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="Plugins/temp" />

DLL file locking

Since plugin DLLs get locked by the CLR when they are loaded, we need to work around this. The solution is to shadow copy the DLLs to another folder on application pre-init. As mentioned previously, this is one of the ways to get plugins loaded in Full Trust and in my opinion is the nicest way to do it since it kills 2 birds with one stone. In Medium Trust however, we’ll have to jump through some hoops and shadow copy the DLLs to a temp folder that exists within the web application. IMPORTANT: When you’re copying DLLs you might be tempted to modify the name of the DLL by adding a version number or similar, but this will NOT work and you’ll get a “The located assembly's manifest definition … does not match the assembly reference.” exception.

Solution

UPDATE: The latest version of this code can be found in the Umbraco v5 source code. The following code does work but there’s been a lot of enhancements to it in the Umbraco core. Here’s the latest changeset as of 16/16/2012 Umbraco v5 PluginManager.cs

Working in Full Trust, the simplest solution is to shadow copy your plugin DLLs into your AppDomain DynamicDirectory. Working in Medium Trust you’ll need to do the following:

  • On application pre-init:

    • Shadow copy all of your plugin DLLs to a temporary folder in your web application (not in the ‘bin’)
    • Add all of the copied DLLs to be referenced by the BuildManager
  • Add all folder paths to the privatePath attribute of the probing element in your web.config to point to where you will be copying your DLLs
    • If you have more than one, you need to semi-colon separate them

Thanks to Glenn Block @ Microsoft who gave me a few suggestions regarding DLL file locking with MEF, Assembly load contexts and probing paths! You put me back on track after I had pretty much given up.

Here’s the code to do the shadow copying and providing the Assemblies to the BuildManager on application pre-init (make sure you set the privatePath on the probing element in your web.config first!!)

using System.Linq; using System.Web; using System.IO; using System.Web.Hosting; using System.Web.Compilation; using System.Reflection; [assembly: PreApplicationStartMethod(typeof(PluginFramework.Plugins.PreApplicationInit), "Initialize")] namespace PluginFramework.Plugins { public class PreApplicationInit { static PreApplicationInit() { PluginFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/plugins")); ShadowCopyFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/plugins/temp")); } /// <summary> /// The source plugin folder from which to shadow copy from /// </summary> /// <remarks> /// This folder can contain sub folderst to organize plugin types /// </remarks> private static readonly DirectoryInfo PluginFolder; /// <summary> /// The folder to shadow copy the plugin DLLs to use for running the app /// </summary> private static readonly DirectoryInfo ShadowCopyFolder; public static void Initialize() { Directory.CreateDirectory(ShadowCopyFolder.FullName); //clear out plugins) foreach (var f in ShadowCopyFolder.GetFiles("*.dll", SearchOption.AllDirectories)) { f.Delete(); } //shadow copy files foreach (var plug in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories)) { var di = Directory.CreateDirectory(Path.Combine(ShadowCopyFolder.FullName, plug.Directory.Name)); // NOTE: You cannot rename the plugin DLL to a different name, it will fail because the assembl
 

Developing a plugin framework in ASP.NET MVC with medium trust的更多相关文章

  1. Entity Framework在Asp.net MVC中的实现One Context Per Request(附源码)

    上篇中"Entity Framework中的Identity map和Unit of Work模式", 由于EF中的Identity map和Unit of Work模式,EF体现 ...

  2. 案例:1 Ionic Framework+AngularJS+ASP.NET MVC WebApi Jsonp 移动开发

    落叶的庭院扫的一干二净之后,还要轻轻把树摇一下,抖落几片叶子,这才是Wabi Sabi的境界. 介绍:Ionic是移动框架,angularjs这就不用说了,ASP.Net MVC WebApi提供数据 ...

  3. Entity Framework在Asp.net MVC中的实现One Context Per Request(转)

    上篇中"Entity Framework中的Identity map和Unit of Work模式", 由于EF中的Identity map和Unit of Work模式,EF体现 ...

  4. devexpress entity framework 与 asp.net mvc的坑

    最近在做一个使用ASP.NET MVC DEVEXPRESS和EF的OA模块 遇到不少问题这里记录一下: 1 如果项目中存在多个上下文类(DBContext的派生类),在做数据迁移的时候需要在不同目录 ...

  5. 在ASP.NET MVC应用中开发插件框架(中英对照)

    [原文] Developing a plugin framework in ASP.NET MVC with medium trust [译文] 在ASP.NET MVC应用中开发一个插件框架 I’v ...

  6. ASP.NET MVC:模块化/插件式文章汇总

    方案 Shazwazza | Developing a plugin framework in ASP.NET MVC with medium trust 基于ASP.NET MVC3 Razor的模 ...

  7. Using the Repository Pattern with ASP.NET MVC and Entity Framework

    原文:http://www.codeguru.com/csharp/.net/net_asp/mvc/using-the-repository-pattern-with-asp.net-mvc-and ...

  8. [转]Using the Repository Pattern with ASP.NET MVC and Entity Framework

    本文转自:http://www.codeguru.com/csharp/.net/net_asp/mvc/using-the-repository-pattern-with-asp.net-mvc-a ...

  9. ASP.NET MVC Overview

    ASP.NET MVC Overview The Model-View-Controller (MVC) architectural pattern separates an application  ...

随机推荐

  1. 如何“刷leetcode”

    做题目的: 获得 offer 巩固算法与数据结构知识,将学到的东西用出来 如何做题: 根据章节与难度来做. 比如你学了 linked list,就去找到标签为 linked list 的题目,然后根据 ...

  2. echo "scale=100; a(1)*4" | bc -l 输出圆周率

    突然看到echo "scale=100; a(1)*4" | bc -l可以输出圆周率,很惊奇,后来发现很简单. 首先bc是“basic calculator”的缩写,就是初级的计 ...

  3. JS clientHeight,scrollHeight,offsetHeight,scrollTop,offsetTop概念

    JS滚动页面到某一位置时触发指定事件能够增强用户体验或是提高性能,其中使用最多的场景是加载更多,当鼠标滚动至页面下方时,自动加载下一页的内容.另一个常用的场景是当用户滚动至页面某一地方时,页面会给出提 ...

  4. BigDecimal 使用方法详解

    BigDecimal 使用方法详解 博客分类: java基础 bigdecimalmultiplyadddivide  BigDecimal 由任意精度的整数非标度值 和 32 位的整数标度 (sca ...

  5. ultraEdit32 /uedit32 自定义快捷键/自定义注释快捷键

    编辑器一直用vim,但同事写VHDL 用的是utraledit32 ,为了更好的沟通,我也下载了最新破解版本:http://pan.baidu.com/s/1qWCYP2W 刚开始用找不到注释的快捷键 ...

  6. iOS程序中调用系统自带应用(短信,邮件,浏览器,地图,appstore,拨打电话,iTunes,iBooks )

    在网上找到了下在记录下来以后方便用 在程序中调用系统自带的应用,比如我进入程序的时候,希望直接调用safar来打开一个网页,下面是一个简单的使用:

  7. zoeDylan.js框架-数据底层

    zoeDylan.js是墨芈自己写的一套前端框架,不过由于墨芈经验不足,所以框架内部代码有些混乱. 墨芈写这套框架的目的是为了存储以后做前端开发过程中的一些代码,简单的说这套框架就是一个大杂烩. 这套 ...

  8. 在.net中为什么第一次执行会慢?

    众所周知.NET在第一次执行的时比第二第三次的效率要低很多,最常见的就是ASP.NET中请求第一个页面的时候要等上一段时间,而后面任意刷新响应都非常迅速,那么是什么原因导致的呢?为什么微软不解决这个问 ...

  9. 关于 jquery select2 多个关键字 模糊查询的解决方法

    select2 只针对 元素的text()进行匹配,实际开发过程中可能会存在通过id 或者特殊编码进行 多关键字匹配. 改动了下源码:红色为改动部分. process=function(element ...

  10. Newtonsoft.Json之JArray, JObject, JPropertyJValue

    JObject staff = new JObject(); staff.Add(new JProperty("Name", "Jack")); staff.A ...