ASP.NET MVC2 插件架构第 2 部分:续集






4.83/5 (3投票s)
ASP.NET MVC2 应用程序中启用插件架构的库的实现,第二部分。
引言
在本文的第一部分中,我介绍了 ASP.NET MVC 应用程序插件架构的实现,并提供了解决方案代码。尽管最初的实现满足了当时的需求,但我在文章末尾指出,仍有几个悬而未决的问题需要解决。
- 如果插件未应用
[PluginActionFilter]
属性,则可以绕过停用功能。 - 应该有一些通用测试用例,以确保插件项目满足最低要求。
- 除了嵌入式视图外,对其他资源(如图像和 javascript 文件)的处理。
在本文的第二章(也是最后一章)中,我将讨论这些问题,并提供更新的解决方案来解决它们。
你不能通过!
插件的管理由 PluginManager 类处理。它维护每个插件的状态信息,例如 pluginName 和 activated 属性(在一个 PluginStatus 对象列表中),并提供查询和设置所述信息的方法。在示例宿主应用程序中,管理表单提供了激活和停用插件的功能,ASP.NET MVC 覆盖 OnActionExecuting 在 Index 操作上使用了一个名为 PluginActionFilter
的属性,以允许插件正常运行或重定向到 DeactivatedPage 操作。然而,这依赖于荣誉系统,即由插件开发人员将 [PluginActionFilter]
添加到 Index 操作。现在,在 PluginManager 类的更新版本中,我们拥有了强制执行此规则的工具,即要求插件只有在其 Index 方法具有 [PluginActionFilter]
属性时才被视为“有效”。
PluginHelper
PluginHelper 中添加了额外的方法和更新,以查询有关插件程序集的额外信息。
HasPluginActionFilter
- 此方法检查插件程序集是否在 Index 操作方法上实现了[PluginActionFilter]
属性。
Private static bool HasPluginActionFilter(Assembly plugin)
{
var types = plugin.GetTypes().Where(t => t.BaseType == typeof(System.Web.Mvc.Controller));
foreach (var type in types)
{
if (type != null)
{
var method = type.GetMethod("Index");
if (method != null)
{
var actionFilterAttributes = method.GetCustomAttributes(typeof(PluginActionFilter), false);
if (actionFilterAttributes.Count() > 0)
return true;
}
}
}
return false;
}
InitializePluginsAndLoadViewLocations
- 此方法已更新为调用 HasPluginActionFilter,并且仅当它返回“true”时才注册插件。如果插件的 Index 操作方法未指定PluginActionFilter
属性,则它将被标记为无效。
public static void InitializePluginsAndLoadViewLocations(bool defaultStatus)
{
Assembly[] pluginAssemblies = GetPluginAssemblies();
List<string> viewLocations = new List<string>();
foreach (Assembly plugin in pluginAssemblies)
{
var pluginAttribute = plugin.GetCustomAttributes(typeof(MvcPluginViewLocations), false).FirstOrDefault() as MvcPluginViewLocations;
if (pluginAttribute != null)
{
//Check to see if this plugin has the PluginActionFilter attribute.
if (HasPluginActionFilter(plugin))
{
viewLocations.AddRange(pluginAttribute.viewLocations);
//Register plugin with PluginManager class
PluginManager.RegisterPlugin(plugin.ManifestModule.Name,DefaultStatus);
//The PluginViewEngine is used to locate views in the assemlbies
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new PluginViewEngine(viewLocations.ToArray()));
}
else
{
//Mark plugin invalid
PluginManager.InvalidatePlugin(plugin.ManifestModule.Name);
}
}
}
}
插件管理器
以下是对 PluginManager 类所做的更改,以利用 PluginHelper 的更新。
PluginStatus
- 添加了属性 valid 以跟踪插件的有效性。
public class PluginStatus
{
public string pluginName;
public bool activated;
public bool valid;
}
IsPluginValid
- 如果插件有效则返回“true”,否则返回“false”。
public static bool IsPluginValid(string name)
{
var plugin = PluginList.First(p => p.pluginName == name);
return (plugin == null ? false : plugin.valid);
}
SetPluginStatus
- 此方法已更新,用于在激活插件之前检查其有效性。
public static void SetPluginStatus(string name, bool status)
{
var plugin = PluginList.First(p => p.pluginName == name);
if (!plugin.valid && status == true)
throw new Exception("An invalid plugin cannot be activated.");
if (plugin != null)
plugin.activated = status;
}
测试
为了确保插件满足成为插件所需的最低要求,我们将添加一些自动化单元测试。
有效性
有效插件的要求很简单。它必须有一个 Index 操作,并且该 Index 操作必须有一个 [PluginActionFilter]
属性。下面是测试方法。
[TestMethod]
public void IndexHasPluginActionFilter()
{
var indexMethod = pluginControllerInstance.GetType().GetMethod("Index");
Assert.IsNotNull(indexMethod, "Index action method does not exist in plugin.");
var actionFilterAttributes = indexMethod.GetCustomAttributes(typeof(PluginActionFilter), false);
Assert.IsTrue(actionFilterAttributes.Count() > 0, "Index action method does not have [PluginActionFilter] attribute.");
}
要尝试此操作,只需从 Index 方法中删除 [PluginActionFilter]
属性,重新编译并执行测试用例。结果如下。
此测试方法还会检查以确保您已定义 Index 操作方法,如果未定义,则会失败。
嵌入式视图
这很有趣。插件提供的任何视图都必须是嵌入式的,但没有什么能阻止插件使用宿主应用程序提供的视图。另一个问题是,“我们如何确定在操作中请求哪个视图?”
简短的回答是,“我认为我们做不到。”
我们能做的一件事是假设应该至少有一个嵌入式视图,如果没有,则发出某种警告。在这种情况下,警告将是 Inconclusive 断言。
[TestMethod]
public void HasEmbeddedViews()
{
int numViews = pluginControllerInstance.GetType().Assembly.GetCustomAttributes(typeof(MvcPluginViewLocations), false).Count();
if (numViews == 0)
Assert.Inconclusive("Warning: There are no embedded views.");
}
如果您的插件中没有嵌入式视图,您将看到以下内容。
额外资源
为了访问在正常 MVC 应用程序中是单独文件的视图,插件必须将它们内部化为嵌入式资源。那么,其他文件呢,例如图像和 javascript 文件?在本文的范围内,我们不关心预期属于宿主应用程序的文件,而将重点放在假定特定于插件的文件上。既然我们已经有了一些引用和使用嵌入式资源文件的基础结构,那么让我们看看如何利用它来访问其他文件。
引用资源
我们已经知道如何嵌入这些资源,所以现在我们需要做的是找出如何引用和使用它们。插件库已经预期嵌入式视图的 URL 以 ~/Plugins 开头。该库还在 URL 中引用插件。所以,让我们看看如何做同样的事情。经过一番探索,我得到了以下结果。
示例插件现在有一个图像。由于它是嵌入式的,我们需要以一种方式构成 URL,使插件库能够知道它是嵌入式的,知道它嵌入在哪个插件程序集中,并且能够在浏览器发出请求时检索它。这是 URL。
<img src="/Plugins/PluginExample.dll/Resources.Santa_hat_smiley_T.png" />
- /Plugins 标识此资源是嵌入式的。
- PluginExample.dll 标识程序集。
- Resources.Santa_hat_smiley_T.png 是嵌入式资源。
请注意,URL 中的嵌入式资源与反编译时在 DLL 中查看的不同,并且 URL 中没有 ~。这是因为资源前面加上了程序集名称,而 AssemblyResourceProvider 类的 IsAppResourcePath 方法在前面加上了波浪号。
处理资源请求
为了处理对嵌入式资源的请求,我们需要拦截它们。由于我们已经为视图这样做了,我们所需要做的就是更新现有代码以处理其他文件。我们首先通过在 Global.ascx.cs 的 RegisterRoutes 方法中添加以下内容,确保禁用我们感兴趣的文件(.css、.js、.gif、.jpg、.png)的路由。
routes.IgnoreRoute("{*staticfile}", new { staticfile = @".*\.(css|js|gif|jpg|png)(/.*)?" });
现在,我们必须更新 AssemblyResourceProvider 类以检查嵌入式资源。请注意,我们必须考虑命名空间,因为它已添加到程序集中的资源中。这是从程序集获取名称的代码
string assemblyNamespace = assembly.GetName().Name;
此信息用于提供程序的更新 FileExists 方法中,以便它也会检查程序集中是否存在请求的文件。
bool found = Array.Exists(resourceList,
delegate(string r) { return r.Equals(resourceName); })
|| Array.Exists(resourceList,
delegate(string r) { return r.Equals(assemblyNamespace + "." + resourceName); });
一旦 AssemblyResourceProvider 方法指示请求是针对嵌入式资源,我们需要确保更新 AssemblyResourceVirtualFile 类以从程序集检索文件。因此,我们更新 Open 方法以检查资源是否存在并带有命名空间前缀,如果存在,则在调用 GetManifestResourceStream 时使用它。这是代码片段
...
string assemblyNamespace = assembly.GetName().Name;
if (assembly != null)
{
//Check to see if this exists with the namespace. If so
//then prepend the namespace.
if (assembly.GetManifestResourceInfo(resourceName) == null)
{
resourceName = assemblyNamespace + "." + resourceName;
}
return assembly.GetManifestResourceStream(resourceName);
}
...
结论
现在我们有了一个更完整的 ASP.NET MVC2 应用程序插件架构实现解决方案。
参考文献
历史
- 首次发布日期:2015 年 1 月 20 日。
- ASP.NET MVC2 插件架构第一部分