MVC 筛选器

轻松地向 MVC 应用程序中添加性能计数器

本 · 格罗弗

工作在企业的 Web 应用程序通常涉及的额外的代码来帮助监控主机和操作的应用程序。 在本文中,我将解释我如何清理和替换重复、 混乱遍布许多方法在应用程序中的代码使用模型视图控制器 (MVC) 的筛选器。

业务经理经常设置了 Microsoft 操作管理器 (MOM) 监视 Web 站点 (或服务) 的健康状况和触发警报阈值的值基于使用性能计数器。 这些警报帮助确保 Web 站点上的退化的经验快速找到。

问题的代码

我的工作相关的项目 (使用 ASP。NET MVC 框架) 要求将性能计数器添加到 Web 页面和 Web 服务,以帮助运营团队在哪里。 运营团队需要为每个页的计数器: 请求延迟、 故障率每秒的请求总数。

问题似乎总是出现的这种要求的执行情况。 我开始看更勤奋的开发人员已将其添加为以前的编码里程碑的一部分从这些计数器的当前实现。 我感到很失望。 我敢肯定你都已经有 — — 你看代码和丢得一干二净。 我看到了什么? 重复代码通过洒每个方法,在这里和那里的变量名的几个更改。 我并不满意当前执行。

让我感到害怕的代码可以看到在图 1

图 1代码,使我害怕

public ActionResult AccountProfileInformation()
{
  try
  {
    totalRequestsAccountInfoCounter.Increment();
    // Start counter for latency
    long startTime = Stopwatch.GetTimestamp();
 
    // Perform some operations action here
    long stopTime = Stopwatch.GetTimestamp();
    latencyAccountInfoCounter.IncrementBy((stopTime - startTime) / 
      Stopwatch.Frequency);
    latencyAccountInfoBaseCounter.Increment();
  }
  catch (Exception e)
  {
    failureAccountInfoCounterCounter.Increment();
  }
  return View();
}

这段代码来看,我知道我想要删除的每个项目的操作方法。 这种类型的模式,使极难看到真正的方法代码,是因为考虑性能监测到的所有多余的代码。 我在寻找聪明的方式,此代码的重构,这样它就不会乱抛垃圾每个操作方法。 输入 MVC 的筛选器。

MVC 筛选器

MVC 筛选器是你放到行动上的自定义属性方法 (或控制器) 添加常用功能。 MVC 筛选使您得以添加预处理和后处理的行为。 可以在这里找到的内置的 MVC 筛选器列表:bit.ly/jSaD5N。 我用一些内置的筛选器,如 OutputCache,但我知道 MVC 筛选器有很多隐藏的力量,我就永远不会拍成 (阅读更多有关筛选器属性,请参阅bit.ly/kMPBYB)。

于是,我开始对自己的思考: 如果我可以封装的所有这些性能计数器在 MVC 筛选器属性的逻辑吗? 一个想法诞生了 ! 我能满足前面列出的以下操作的每个性能计数器:

  1. 每秒的请求总数
    1. 实施 IActionFilter,有两个方法: OnActionExecuting 和 OnActionExecuted
    2. 增加 OnActionExecuting 中的计数器
  2. 请求延迟
    1. 实施 IResultFilter,有两个方法: OnResultExecuting 和 OnResultExecuted
    2. 启动计时器在 OnActionExecuting 和 OnResultExecuted 过程记录延迟
  3. 故障率
    1. 实施 IExceptionFilter,有方法 OnException

过程所示图 2

MVC Filter-Processing Pipeline

图 2MVC 过滤器处理管道

我将讨论每个筛选器的使用,如图所示,在图 2

IActionFilterOnActionExecuting (线 2a) 执行之前执行的操作方法。 OnActionExecuted (行 2b) 执行操作方法执行后,但之前执行结果。

IResultFilterOnResultExecuting (线 4a) 执行之前操作结果执行 (例如,渲染视图)。 OnResultExecuted (线 4b) 执行之后执行操作的结果。

IExceptionFilter OnException (不显示在 图 2为清晰起见) 执行时异常是引发 (和未处理)。

IAuthorizationFilter OnAuthorization (不包含在 图 2,也在这篇文章中使用) 授权时需要调用。

管理计数器

但是,如果我使用此性能计数器的筛选器属性,我会有一个问题: 我怎会性能计数器 (每个操作) 到每个这些筛选器在运行时? 我不想有一个单独的筛选器属性类的每个操作。 在这种情况下,我将不得不进行硬编码属性中的性能计数器名称。 实现解决方案所需的类名称的数量,这会爆炸。 我回来在技术上反映我用我第一次工作与微软。NET 框架: 反射 (双关 !)。 反射大量利用以及 MVC 框架。 您可以了解更多有关反射在这里:bit.ly/iPHdHz

我的想法是创建两个类:

  1. WebCounterAttribute
    1. 实现 MVC 筛选接口 (IExceptionFilter、 IActionFilter 和 IResultFilter)
    2. 存储在 WebCounterManager 中的增量计数器
  2. WebCounterManager
    1. 实现从每个 MVC 操作加载 WebCounterAttributes 反射代码
    2. 存储,以便查找的性能计数器对象的地图
    3. 提供递增存储在该图中的计数器的方法

实现设计

有这些类,我可以装饰 WebCounterAttribute 对每个操作方法的我想要实施,性能计数器,如下所示:

public sealed class WebCounterAttribute : FilterAttribute, IActionFilter, IExceptionFilter, IResultFilter
{
  /// Interface implementations not shown
}

下面是示例操作方法:

[WebCounter("Contoso Site", "AccountProfileInformation")]
public ActionResult AccountProfileInformation()
{
  // Some model loading
  return View();
}

然后我可以使用反射的 Application_Start 方法在这些属性中读取和创建的计数器的每项操作,如中所示图 3。 (注意计数器系统中注册的在安装程序中,但在代码中创建的计数器的实例)。

图 3程序集的反思

/// <summary>
/// This method reflects over the given assembly(ies) in a given path 
/// and creates the base operations required  perf counters
/// </summary>
/// <param name="assemblyPath"></param>
/// <param name="assemblyFilter"></param>
public void Create(string assemblyPath, string assemblyFilter)
{
  counterMap = new Dictionary<string, PerformanceCounter>();
            
  foreach (string assemblyName in Directory.EnumerateFileSystemEntries(
    assemblyPath, assemblyFilter)) 
  {
    Type[] allTypes = Assembly.LoadFrom(assemblyName).GetTypes();
 
    foreach (Type t in allTypes)
    {
      if (typeof(IController).IsAssignableFrom(t))
      {
        MemberInfo[] infos = Type.GetType(t.AssemblyQualifiedName).GetMembers();
 
        foreach (MemberInfo memberInfo in infos)
        {
          foreach (object info in memberInfo.GetCustomAttributes(
            typeof(WebCounterAttribute), true))
          {
            WebCounterAttribute webPerfCounter = info as WebCounterAttribute;
            string category = webPerfCounter.Category;
            string instance = webPerfCounter.Instance;
            // Create total rollup instances, if they don't exist
            foreach (string type in CounterTypeNames)
            {
              if (!counterMap.ContainsKey(KeyBuilder(Total, type)))
              {
                counterMap.Add(KeyBuilder(Total, type), 
                  CreateInstance(category, type, Total));
              }
            }
            // Create performance counters
            foreach (string type in CounterTypeNames)
            {
              counterMap.Add(KeyBuilder(instance, type), 
                CreateInstance(category, type, instance));
            }
          }
        }
      }
    }
  }
}

注意,在填充地图的重要线:

(counterMap.Add(KeyBuilder(instance, type), CreateInstance(category, type, instance));),

它创建的行动,包括计数器类型中,WebCounterAttribute 的特定实例之间的映射,并将其映射到创建性能计数器-实例。

我然后可以编写代码,使我看起来没有任何实例 (和增加它),使用这种映射的特定实例的 WebCounterAttribute (请参见图 4)。

图 4WebCounterManager RecordLatency

/// <summary>
/// Record the latency for a given instance name
/// </summary>
/// <param name="instance"></param>
/// <param name="latency"></param>
public void RecordLatency(string instance, long latency)
{
  if (counterMap.ContainsKey(KeyBuilder(instance,   
    CounterTypeNames[(int)CounterTypes.AverageLatency]))
    && counterMap.ContainsKey(KeyBuilder(instance,   
    CounterTypeNames[(int)CounterTypes.AverageLatencyBase])))
  {
    counterMap[KeyBuilder(instance, 
      CounterTypeNames[(int)CounterTypes.AverageLatency])].IncrementBy(latency);
    counterMap[KeyBuilder(Total, 
      CounterTypeNames[(int)CounterTypes.AverageLatency])].IncrementBy(latency);
    counterMap[KeyBuilder(instance, 
      CounterTypeNames[(int)CounterTypes.AverageLatencyBase])].Increment();
    counterMap[KeyBuilder(Total, 
      CounterTypeNames[(int)CounterTypes.AverageLatencyBase])].Increment();
  }
}

然后我可以记录性能计数器数据,这些筛选器在运行时。 例如,在图 5,您看到的记录性能延迟执行。

图 5调用 WebCounterManager RecordLatency WebCounterAttribute

/// <summary>
/// This method occurs when the result has been executed (this is just 
/// before the response is returned).
/// This method records the latency from request begin to response return.
/// </summary>
/// <param name="filterContext"></param>
public void  OnResultExecuted(ResultExecutedContext filterContext)
{
  // Stop counter for latency
  long time = Stopwatch.GetTimestamp() - startTime;
  WebCounterManager countManager = GetWebCounterManager(filterContext.HttpContext);
  if (countManager != null)
  {
    countManager.RecordLatency(Instance, time);
    ...
}
}
private WebCounterManager GetWebCounterManager(HttpContextBase context)
{
  WebCounterManager manager =  
    context.Application[WebCounterManager.WebCounterManagerApplicationKey] 
    as WebCounterManager;
  return manager;
}

您会注意到在此调用中我从应用程序状态变 WebCounterManager。 为了使这工作,您需要将代码添加到您的 global.asax.cs:

WebCounterManager webCounterMgr = new WebCounterManager();
webCounterMgr.Create(Server.Map("~/bin"), "*.dll");
Application[WebCounterManager.WebCounterManagerApplicationKey] = webCounterMgr;

总结,MVC 筛选器提供大量的重复代码模式优雅的解决方案。 他们会帮助您将使代码更易于维护和清洁的常见代码重构。 很明显,平衡了两者之间的优雅和易于实现。 对我来说,我不得不向约 50 Web 页添加性能计数器。 节省的代码可读性和清晰度绝对是值得这些额外的工作。

MVC 筛选器是逻辑的一种不被侵扰性,所以是否您处理的性能计数器,日志记录或审核,您可以找到无限清洁执行必要的情况下添加行为的绝佳方式。

本 · 格罗弗 是一个程序员在微软在雷德蒙德,华盛顿,他曾在多个团队,从交易所 Lync 到 Windows。

衷心感谢以下技术专家对本文的审阅:埃隆立顿