张清苏,机械迷城怎么玩啊,田心宁
[原文] developing a plugin framework in asp.net mvc with medium trust
[译文] 在asp.net mvc应用中开发一个插件框架
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.
我最近花了很多时间研究和原形设计在asp.net mvc3应用中创建插件引擎的不同方法,主要是为了找到一个能够加载"bin"目录之外的插件(dlls)的好的方法。虽然这篇文章聚焦在mvc3上,不过我相信这些相同的原则也适用于其他mvc版本。
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 or used with the @model declaration in razor), model binders, etc… mvc is very tied to the 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 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 managed extensibility framework (mef) and how it loads dlls as well.
问题
从"bin"目录以外加载dlls已不是什么新鲜、前沿的事儿,然而使用mvc时,这将变得更加困难。这主要是由于在处理控制器、模型视图(更准确的说是给viewpage传递参数或者在razor中使用@model定义)、模型绑定等等的过程中加载/查找类型的方式引起的。与buildmanager的机制息息相关,buildmanager是编译视图和定位诸如控制器等其他服务的机制。默认情况下,buildmanager只对"bin"目录和全局程序集中的程序集感冒,所以如果一开始就将需要引用的服务和对象的dlls放置在"bin"目录之外,mvc将不能定位到它们。
另外一个需要处理的问题就是dll文件所定。当一个插件dll被加载和使用时,clr就会将这个dll文件锁定。这造成一个问题,就是开发人员不能够在应用运行中更新这些dlls,除非他们修改web.config(将引发应用重新启动)或干脆将应用停止。这适用于mef以及它如何加载dll。
.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: . 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.
.net 4的救援措施
.net 4中的一个新功能是在应用程序初始化之前执行代码的能力,它与buildmanager的另一个新功能相辅相成,它允许您在运行时向其添加程序集引用(必须在应用程序预启动时完成)。 以下是phil haack对这些新功能的一个很好的参考:http://haacked.com/archive/2010/05/16/three-hidden-extensibility-gems-in-asp-net-4.aspx。 这对于使插件框架与mvc一起工作至关重要,以便buildmanager知道在"bin"目录之外引用插件dlls的位置。 然而,故事到这里还没完。
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 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 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’.
模型类型定义在插件程序集中的强类型视图
不幸的是,如果你有一个强类型视图引用的模型存在于'bin'目录之外,那么你会很快发现它不起作用,它实际上不会告诉你原因。这是因为razorviewengine使用buildmanager将视图编译为动态程序集,然后使用activator.createinstance来实例化新编译的对象。这就是问题所在,当前appdomain不知道如何解析强类型视图的模型类型,因为它在“bin”目录或gac中找不到。关于这种情况更糟糕的部分是你没有得到任何错误消息来告诉你为什么这不起作用,或问题出在哪里。相反,你得到了“友好”的mvc视图未找到错误:“...或者找不到它的主人或没有视图引擎支持搜索的位置。搜索了以下位置:...。“告诉您它已在所有viewengine位置搜索了视图,但找不到它......实际上根本的错误并不是它。在mvc3源代码深处,它尝试从动态程序集中实例化视图对象,但它失败了,所以它只是在其余的viewengine路径中继续查找该视图。
注:即使在mvc3有一个新的iviewpageactivator这应该是负责实例化已编译的buildmanager的,实现自定义iviewpageactivator来处理这个问题仍然没有效果的,因为调用iviewpageactivator之前,mvc3代码库就已经因为不能解析不在'bin'中的程序集而失败。
full trust
when working in we have a few options for dealing with the above scenario:
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(); }
完全信任
在full trust中工作时,我们有几个选项来处理上述场景:
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(); }
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.
中级信任的负担
在mvc世界中,当从full trust中的'bin'文件夹外部加载插件时只需要跳过几个障碍就可以了。然而,在medium trust中,事情变得有趣。不幸的是,在medium trust中,无法处理assemblyresolve事件,也无法访问appdomain的dynamicdirectory,因此上述两个解决方案都会被抛出窗外。除此之外,似乎您无法在medium trust中使用codedom来自定义编译视图。
previous attempts
for a while i began to think that this wasn’t possible and i thought i tried everything:
以前的尝试
有一段时间我开始认为这是不可能的,我以为我尝试了一切:
an easy solution
so, the easy solution is to just set a 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" />
一个简单的解决方案
因此,简单的解决方案是在web.config中的'probing'配置节上设置'privatepath'属性,以告知appdomain还可以在指定的文件夹中查找assemblies/types。我尝试从bin中的子文件夹加载插件,之前尝试过这个,但无法使其工作。我不确定是否“做错了”但它肯定不起作用,或者试图在bin的子文件夹中设置它只是不起作用。
<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.
dll文件锁的问题
由于插件dlls在加载时被clr锁定,我们需要解决这个问题。解决方案是在应用程序pre-init上将dlls副本复制到另一个文件夹。如前所述,这是在full trust中加载插件的方法之一,在我看来,这是最好的方法,因为它可以一石二鸟。但是,在medium trust中,我们必须跳过一些环节并将dlls复制到web应用程序中存在的临时文件夹。重要提示:当您复制dll时,您可能想通过添加版本号或类似名称来修改dll的名称,但这不起作用,您将获得“找到的程序集的清单定义...与程序集引用不匹配。”异常。
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:
thanks to @ 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!!)
解决方案
更新:此代码的最新版本可以在umbraco v5源代码中找到。以下代码确实有效,但在umbraco核心中有很多增强功能。这是截至16/16/2012 umbraco v5 pluginmanager.cs的最新变更集
使用full trust,最简单的解决方案是将插件dlls副本拷贝到appdomain dynamicdirectory中。
使用medium trust,您需要执行以下操作:
感谢glenn block @ microsoft,他给了我一些关于使用mef,装配加载上下文和probing路径锁定dll文件的建议!在我几乎放弃之后,你让我重回正轨。
这是在应用程序pre-init上进行副本拷贝并将程序集提供给buildmanager的代码(确保首先在web.config中的probing配置节上设置privatepath !!)
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 assembly name is part if it's manifest // (a reference to how assemblies are loaded: http://msdn.microsoft.com/en-us/library/yx7xezcf ) file.copy(plug.fullname, path.combine(di.fullname, plug.name), true); } // now, we need to tell the buildmanager that our plugin dlls exists and to reference them. // there are different assembly load contexts that we need to take into account which // are defined in this article here: // http://blogs.msdn.com/b/suzcook/archive/2003/05/29/57143.aspx // * this will put the plugin assemblies in the 'load' context // this works but requires a 'probing' folder be defined in the web.config foreach (var a in shadowcopyfolder .getfiles("*.dll", searchoption.alldirectories) .select(x => assemblyname.getassemblyname(x.fullname)) .select(x => assembly.load(x.fullname))) { buildmanager.addreferencedassembly(a); } // * this will put the plugin assemblies in the 'loadfrom' context // this works but requires a 'probing' folder be defined in the web.config // this is the slowest and most error prone version of the load contexts. //foreach (var a in // shadowcopyfolder // .getfiles("*.dll", searchoption.alldirectories) // .select(plug => assembly.loadfrom(plug.fullname))) //{ // buildmanager.addreferencedassembly(a); //} // * this will put the plugin assemblies in the 'neither' context ( i think ) // this nearly works but fails during view compilation. // this does work for resolving controllers but during view compilation which is done with the razorviewengine, // the codedom building doesn't reference the plugin assemblies directly. //foreach (var a in // shadowcopyfolder // .getfiles("*.dll", searchoption.alldirectories) // .select(plug => assembly.load(file.readallbytes(plug.fullname)))) //{ // buildmanager.addreferencedassembly(a); //} } } }
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
Blazor server side 自家的一些开源的, 实用型项目的进度之 CEF客户端
.NET IoC模式依赖反转(DIP)、控制反转(Ioc)、依赖注入(DI)
vue+.netcore可支持业务代码扩展的开发框架 VOL.Vue 2.0版本发布
网友评论