我正在尝试使用许多插件创建一个 MVC4 Web 应用程序,即本质上是通过 MEF 导出的控制器以及解压缩到其适当位置的内容文件。我找到了很多关于 MVC 插件的资料,主要与领域相关,但我不得不放弃 MvcContrib,这将是最明显的解决方案,因为它似乎没有更多的开发,显示了最新的 MVC 位的一些问题,而且我还想要这个架构的最简单的实现。
因此,我的要求是:
a) 基于 MEF 的 MVC 插件解决方案,我只需在我的站点中放置一个包即可使用它,理想情况下甚至无需重新启动。这意味着将插件存储在与 Bin 不同的文件夹中,这也提供了更好的隔离。
b) 一个与 IoC 工具兼容的解决方案,比 MEF 单独完成的解决方案更完整。我倾向于为此使用 Autofac,因为它与 MEF 和 MVC4(此时为 RC)集成。
除了视图之类的内容,最重要的任务是让 MVC 在 MEF 插件中定位控制器并实例化它们,所以我需要一个控制器工厂。我在这里找到了一篇关于此的好文章:http: //kennytordeur.blogspot.be/2012/08/mef-in-aspnet-mvc-4-and-webapi.html(我就此联系了肯尼,感谢他的指点我遇到了一些路由问题)。作者还将他的代码封装到了一个方便的 nuget 包 (MEF.MVC4) 中。无论如何,我发现了一个似乎与路由和命名空间有关的问题:当到达插件控制器的路由时,MEF 控制器工厂 GetControllerInstance 方法得到一个 null controllerType,最终导致 404。我想我可能找到了罪魁祸首通过阅读这些帖子:
http://blog.davebouwman.com/2011/12/08/asp-net-mvc3-and-404s-for-area-controllers/
和
ASP.NET MVC 的自定义控制器工厂、依赖注入/结构图问题
我想(但我可能错了)问题出在路由约定和插件区域控制器命名空间中:插件控制器的命名空间不在主机 web 的同一个“根”中。帖子中提出的解决方案只是向 Web 应用程序添加一条新路由,但这不适合将区域作为插件工作、动态添加到主机应用程序的解决方案。我的主机 web 应用程序必须不知道插件,这当然应该是一个相当普遍的要求,但我没有找到明显的解决方案。
复制解决方案
您可以快速创建重现解决方案,以便按照以下步骤查看我的方法的详细信息,或从此处下载:
1)创建一个空白解决方案。
2) 在其中创建一个 MVC4 Web 应用程序 (HostWeb),更新所有 NuGet 预安装包并添加 Mef.MVC4、Autofac MVC 4 (RC) 和 Autofac.Mef。在我的实际应用程序中,我想使用 Autofac 在构造函数中注入控制器的依赖项。
3) 在 HostWeb 中创建一个 Plugins 文件夹,并在其中创建一个子文件夹 Temp。这将包括使用 Temp 子文件夹作为卷影副本容器的插件,以便从中加载 MEF 目录,而不是直接从插件中加载。这与一些启动代码一起应该让我更新插件而无需重新启动网络应用程序(否则会锁定 DLL)。启动代码是一个名为 PreApplicationInit 的类,您可以在 Infrastructure 文件夹中找到(从http://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx稍作修改)。
4) 向主机 Web 添加一个 Parts 区域,并将视图根文件夹中的 _ViewStart 文件复制到其中(并更改布局视图中的现有链接,以便将空白区域添加到路由值,以免它们被破坏) . 所有插件控制器都将被命名为名为 Parts 的区域。主机 Web 应用程序有一个没有控制器的区域,只是为插件内容文件准备文件夹结构和路由(视图,将从插件安装程序模块解压缩到适当的位置,而二进制文件将放置在插件中)。
5)在HostWeb的App_Start中自定义MefConfig,添加处理Autofac的IocConfig。然后将调用添加到全局 asax 中:MefConfig.RegisterMef() 和 IocConfig.RegisterDependencies()。
6)在其中创建另一个 MVC4 Web 应用程序(AlphaPlugin),更新所有 NuGet 预安装包并添加 Autofac MVC 4 (RC) 和 Autofac.Mef。我选择了一个 Web 应用程序模板(而不是类库),这样我就可以将所有 VS 工具用于 MVC,并最终直接在那里进行一些测试。
7) 将部件区域添加到主机 Web 并将视图根文件夹中的 _ViewStart 文件复制到其中。
8)在零件区域添加一个可导出的控制器。我的称为 AlphaController,并且只有一个名为 Hail 的操作方法,它在 ViewBag 中放入一个字符串并返回默认视图。
9) 回到主机,只需在主视图中添加一个指向插件控制器操作的链接,以测试它是否可以通过 MEF 访问。
现在,如果我构建所有并将 AlphaPlugin.dll 二进制文件复制到 HostWeb Plugins 文件夹中,我希望 MVC 能够通过 MEF 找到它,但随后会抛出未找到视图的错误,因为我尚未在主机 Web 中复制任何内容文件. 相反,我得到以下信息:
Value cannot be null.
Parameter name: type
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.ArgumentNullException: Value cannot be null.
Parameter name: type
Source Error:
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.
Stack Trace:
[ArgumentNullException: Value cannot be null.
Parameter name: type]
System.ComponentModel.Composition.Hosting.ExportProvider.GetExportsCore(Type type, Type metadataViewType, String contractName, ImportCardinality cardinality) +263923
System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(Type type, Type metadataViewType, String contractName) +41
MEF.MVC4.MefControllerFactory.GetControllerInstance(RequestContext requestContext, Type controllerType) +84
System.Web.Mvc.DefaultControllerFactory.CreateController(RequestContext requestContext, String controllerName) +226
System.Web.Mvc.MvcHandler.ProcessRequestInit(HttpContextBase httpContext, IController& controller, IControllerFactory& factory) +326
System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state) +177
System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state) +88
System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData) +50
System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +301
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155
这是最相关的代码(您可以在 repro 解决方案中找到它):它处理 Plugins 文件夹内容,以便网络应用程序从副本加载它们:
[assembly: PreApplicationStartMethod(typeof(PreApplicationInit), "Initialize")]
static public class PreApplicationInit
{
///
/// The source plugin folder from which to shadow copy from.
///
/// This folder can contain sub folders to organize plugin types.
internal static DirectoryInfo PluginFolder { get; private set; }
///
/// The folder to shadow copy the plugin DLLs to use for running the app.
///
internal static DirectoryInfo ShadowCopyFolder { get; private set; }
static PreApplicationInit()
{
PluginFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/Plugins"));
ShadowCopyFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/Plugins/Temp"));
}
public static void Initialize()
{
if (!Directory.Exists(ShadowCopyFolder.FullName))
Directory.CreateDirectory(ShadowCopyFolder.FullName);
else
{
foreach (FileInfo fi in ShadowCopyFolder.GetFiles("*.dll", SearchOption.AllDirectories))
{
try
{
fi.Delete();
}
catch (Exception ex)
{
// TODO log
Debug.WriteLine(ex.ToString());
}
}
}
// shadow copy files
foreach (FileInfo fi in PluginFolder.GetFiles("*.dll"))
{
try
{
File.Copy(fi.FullName, Path.Combine(ShadowCopyFolder.FullName, fi.Name), true);
}
catch (Exception ex)
{
// TODO log
Debug.WriteLine(ex.ToString());
}
}
}
}
这是我的工厂,它无论如何都会得到一个空的控制器类型,所以它的代码永远不会超出它的第一行执行:
public class MefControllerFactory : DefaultControllerFactory
{
private readonly CompositionContainer _compositionContainer;
public MefControllerFactory(CompositionContainer compositionContainer)
{
_compositionContainer = compositionContainer;
}
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
// https://stackoverflow.com/questions/719678/custom-controller-factory-dependency-injection-structuremap-problems-with-asp
if (controllerType == null) return base.GetControllerInstance(requestContext, null);
var export = _compositionContainer.GetExports(controllerType, null, null).SingleOrDefault();
IController result;
if (export != null) result = export.Value as IController;
else
{
result = base.GetControllerInstance(requestContext, controllerType);
_compositionContainer.ComposeParts(result);
}
return result;
}