.NET 4.0 ASP.NET MVC 3 嵌入式视图插件架构






4.92/5 (59投票s)
MVC 3 插件架构,带有嵌入式 Razor 视图:逐步说明和演示应用程序。
介绍
本文演示了如何使用 ASP.NET MVC 3 快速构建插件架构,以及如何为您的应用程序构建新插件。它还展示了如何将视图作为嵌入式资源创建到插件中,以及 .NET 4.0 功能如何帮助宿主应用程序发现新插件。
第二部分 展示了如何在插件中添加服务器端逻辑。
背景
我必须设计一个新的 ASP.NET MVC 企业应用程序,该应用程序被市场定位为软件即服务,基本上是按功能分发给多个客户。客户可以自定义他想要激活的服务并仅为这些服务付费。还需要对代码进行额外的安全保护,所有程序集都必须签名,并且视图必须内嵌以进行保护,因此最佳方法是创建一个插件化的架构,该架构可以帮助完成所有这些需求并轻松交付额外的功能。这还增加了额外的灵活性,以应对来自单个客户的定制化需求,并允许他们自己创建模块(如果他们愿意)。
创建带有嵌入式视图的插件系统的步骤
设置插件基础架构
宿主应用程序必须通过公共库识别所有插件。所有插件都必须有一个通用接口,任何插件根目录下的类都将实现该接口。该接口必须位于一个将被所有插件引用的公共程序集中,并包含用作插件与宿主应用程序之间桥梁的通用接口。
我们称该接口为 IModule
接口
public interface IModule
{
/// <summary>
/// Title of the plugin, can be used as a property to display on the user interface
/// </summary>
string Title { get; }
/// <summary>
/// Name of the plugin, should be an unique name
/// </summary>
string Name { get; }
/// <summary>
/// Version of the loaded plugin
/// </summary>
Version Version { get; }
/// <summary>
/// Entry controller name
/// </summary>
string EntryControllerName { get; }
}
实现该接口的类将包含有关插件名称、版本和默认访问权限的所有信息。
插件发现
ASP.NET 4 引入了一些非常有用的新扩展性 API。其中之一是一个名为 PreApplicationStartMethodAttribute
的新程序集属性。
这个新属性允许您在应用程序启动时,在 ASP.NET 管道的早期运行代码,甚至在 Application_Start
之前。这恰好也在您的 App_Code
文件夹中的代码被编译之前发生。 要使用此属性,请创建一个类库并将其添加为程序集级别的属性。示例
[assembly: PreApplicationStartMethod(typeof(PluginTest.PluginManager.PreApplicationInit),"InitializePlugins")]
如上所示,指定了一个类型和一个字符串。字符串代表一个公共静态 void 方法,该方法不带参数。现在,任何引用该程序集的 ASP.NET 网站将在应用程序即将启动时调用 InitializePlugins
方法,让该方法有机会执行一些早期初始化。
public class PreApplicationInit
{
/// <summary>
/// Initialize method
/// </summary>
public static void InitializePlugins()
{ ... }
}
现在,此属性的实际用途是您可以在运行时添加生成提供程序或新的程序集引用,而在早期版本的 Visual Studio 中,这只能通过 web.confing
完成。我们将使用它在应用程序启动前添加所有插件的引用。示例
System.Web.Compilation.BuildManager.AddReferencedAssembly(assembly);
插件应复制到 Web 应用程序内的某个目录中,但不能直接引用,而是应创建副本并引用复制的插件。所有这些步骤都必须在 PreApplicationInit
类的静态构造函数中完成。
public class PreApplicationInit
{
static PreApplicationInit()
{
string pluginsPath = HostingEnvironment.MapPath("~/plugins");
string pluginsTempPath = HostingEnvironment.MapPath("~/plugins/temp");
if (pluginsPath == null || pluginsTempPath == null)
throw new DirectoryNotFoundException("plugins");
PluginFolder = new DirectoryInfo(pluginsPath);
TempPluginFolder = new DirectoryInfo(pluginsTempPath);
}
/// <summary>
/// The source plugin folder from which to copy from
/// </summary>
private static readonly DirectoryInfo PluginFolder;
/// <summary>
/// The folder to copy the plugin DLLs to use for running the application
/// </summary>
private static readonly DirectoryInfo TempPluginFolder;
/// <summary>
/// Initialize method that registers all plugins
/// </summary>
public static void InitializePlugins()
{ ... }
}
当调用 InitializePlugins
方法时,它将刷新临时目录并引用所有插件程序集。在引用时,需要进行检查,通过验证插件是否包含实现 IModule
接口的类来验证插件。
public static void InitializePlugins()
{
Directory.CreateDirectory(TempPluginFolder.FullName);
//clear out plugins
foreach (var f in TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
{
f.Delete();
}
//copy files
foreach (var plug in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
{
var di = Directory.CreateDirectory(TempPluginFolder.FullName);
File.Copy(plug.FullName, Path.Combine(di.FullName, plug.Name), true);
}
//This will put the plugin assemblies in the 'Load' context
var assemblies = TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories)
.Select(x => AssemblyName.GetAssemblyName(x.FullName))
.Select(x => Assembly.Load(x.FullName));
foreach (var assembly in assemblies)
{
Type type = assembly.GetTypes()
.Where(t => t.GetInterface(typeof(IModule).Name) != null).FirstOrDefault();
if (type != null)
{
//Add the plugin as a reference to the application
BuildManager.AddReferencedAssembly(assembly);
//Add the modules to the PluginManager to manage them later
var module = (IModule)Activator.CreateInstance(type);
PluginManager.Current.Modules.Add(module, assembly);
}
}
}
为了使此方法生效,必须在 web.config 中配置一个探测文件夹,以告知 AppDomain 也在指定文件夹中查找程序集/类型。
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="plugins/temp" />
</assemblyBinding>
</runtime>
单例 PluginManager
用于跟踪所有插件,以便稍后在应用程序中使用。
在最后一步,在引用插件之后,有必要注册插件中的嵌入式视图。这将使用 BoC.Web.Mvc.PrecompiledViews.ApplicationPartRegistry
来完成。我更喜欢在一个 Bootstrapper
类中完成此操作,在该类中,我可以设置我的 DI 容器(如果需要)。更多信息将在下一章“创建嵌入式视图”中介绍。Bootstrapper
概述
public static class PluginBootstrapper
{
public static void Initialize()
{
foreach (var asmbl in PluginManager.Current.Modules.Values)
{
ApplicationPartRegistry.Register(asmbl);
}
}
}
创建新插件项目的步骤
- 创建新的类库项目。
- 创建类似于 MVC 应用程序的文件夹:Models、Views、Controllers。
- 创建一个实现
IModule
接口的模块初始化器类:
public class CalendarModule : IModule
{
public string Title
{
get { return "Calendar"; }
}
public string Name
{
get { return Assembly.GetAssembly(GetType()).GetName().Name; }
}
public Version Version
{
get { return new Version(1, 0, 0, 0); }
}
public string EntryControllerName
{
get { return "Calendar"; }
}
}
创建嵌入式视图
完整的教程,我也遵循了该教程,关于如何将嵌入式视图添加到 MVC 项目,可以在这里找到。本章仅描述此案例的绝对步骤。有一些简单的步骤需要遵循。
- 下载以下 Visual Studio 扩展并从这里安装。
- 视图必须像在普通 MVC 项目中一样创建,具有相同的结构,放在 Views 目录下。然后必须将视图设置为嵌入式资源,并将自定义工具命名空间设置为
MvcRazorClassGenerator
。 - 添加自定义工具后,项目将添加一些引用,但还需要一些额外的引用,这些引用与 MVC 项目相关。确保所有引用都已添加到项目中,例如:System.Web.Helpers、System.Web.WebPages、System.Web.MVC。
- 最后一步是确保注册嵌入式视图:
BoC.Web.Mvc.PrecompiledViews.ApplicationPartRegistry.Register(asmbl);
使用代码
所有必需的程序集都在 Binaries 文件夹中,与解决方案文件处于同一级别。您可以找到以下 DLL:BoC.Web.Mvc.PrecompiledViews.dll、Commons.Web.Mvc.PrecompiledViews.dll、System.Web.Helpers.dll、System.Web.WebPages.dll、System.Web.MVC.dll、System.Web.Razor.dll、System.Web.WebPages.Razor.dll。
所有插件 DLL 都编译在 PluginBin 目录中,与解决方案文件位于同一根目录。
插件在 Web 应用程序内的 plugins 目录中搜索,从解决方案文件开始的相对路径是 PluginTest.Web/plugins
首先确保项目已编译,然后将 PluginBin 中的内容复制到 PluginTest.Web/plugins
确保 Web 服务器已停止或重新启动 Web 服务器并运行应用程序。
随时通过删除一个或多个插件来尝试这些插件,然后重新启动应用程序并查看结果。
结论
插件 Web 应用程序可以像本文所述一样轻松实现,但 Web 应用程序很少需要这种额外的复杂性。这只是一个关于如何实现这一点的概念验证示例,它不代表一个完整的实现。本文的目的是分享一个基本实现,其他人可以根据需要重复使用和扩展。