ASP.NET Web 窗体连接复原和命令截获

作者 :Erik Reitan

在本教程中,你将修改 Wingtip Toys 示例应用程序以支持连接复原和命令拦截。 通过启用连接复原能力,Wingtip Toys 示例应用程序将在云环境的典型暂时性错误发生时自动重试数据调用。 此外,通过实现命令拦截,Wingtip Toys 示例应用程序将捕获发送到数据库的所有 SQL 查询,以便记录或更改查询。

注意

本Web Forms教程基于 Tom Dykstra 的以下 MVC 教程:
在 ASP.NET MVC 应用程序中使用实体框架进行连接复原和命令拦截

学习内容:

  • 如何提供连接复原能力。
  • 如何实现命令拦截。

先决条件

在开始之前,请确保已在计算机上安装以下软件:

连接复原

考虑将应用程序部署到 Windows Azure 时,可以考虑将数据库部署到 WindowsAzure SQL 数据库(一种云数据库服务)。 与 Web 服务器和数据库服务器在同一数据中心直接连接在一起时,连接到云数据库服务时,暂时性连接错误的频率通常更高。 即使云 Web 服务器和云数据库服务托管在同一数据中心,它们之间的网络连接也更多,可能会出现问题,例如负载均衡器。

此外,云服务通常由其他用户共享,这意味着其响应能力可能会受到其他用户的影响。 对数据库的访问可能会受到限制。 限制意味着,尝试访问数据库服务的频率高于 服务级别协议 (SLA) 允许的频率,数据库服务会引发异常。

访问云服务时发生的许多或大多数连接问题都是暂时性的,即在短时间内自行解决。 因此,当你尝试数据库操作并收到通常是暂时性的错误类型时,可以在短暂的等待后再次尝试该操作,并且该操作可能会成功。 如果通过自动重试处理暂时性错误,则可以为用户提供更好的体验,使大多数错误对客户不可见。 Entity Framework 6 中的连接复原功能自动执行重试失败的 SQL 查询的过程。

必须为特定数据库服务正确配置连接复原功能:

  1. 它必须知道哪些异常可能是暂时性的。 例如,你想要重试因网络连接暂时丢失而导致的错误,而不是由程序 bug 引起的错误。
  2. 在重试失败的操作之间,它必须等待适当的时间。 与用户正在等待响应的联机网页相比,在批处理过程中重试之间的等待时间要长。
  3. 它必须重试适当的次数,然后才能放弃。 你可能希望在批处理中重试更多次,而联机应用程序中会重试。

可以为实体框架提供程序支持的任何数据库环境手动配置这些设置。

要启用连接复原,只需在程序集中创建派生自 DbConfiguration 类的类,并在该类中设置SQL 数据库执行策略,该策略在实体框架中是重试策略的另一个术语。

实现连接复原能力

  1. 在 Visual Studio 中Web Forms应用程序下载并打开 WingtipToys 示例。

  2. WingtipToys 应用程序的逻辑文件夹中,添加名为 WingtipToysConfiguration.cs 的类文件。

  3. 将现有代码替换为以下代码:

    using System.Data.Entity;
    using System.Data.Entity.SqlServer;
     
    namespace WingtipToys.Logic
    {
        public class WingtipToysConfiguration : DbConfiguration
        {
            public WingtipToysConfiguration()
            {
              SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            }
        }
    }
    

实体框架自动运行它在派生自 DbConfiguration的类中找到的代码。 可以使用 DbConfiguration 类在代码中执行配置任务,否则会在 Web.config 文件中执行这些任务。 有关详细信息,请参阅 EntityFramework Code-Based 配置

  1. 逻辑 文件夹中,打开 AddProducts.cs 文件。

  2. 添加 的 System.Data.Entity.Infrastructure 语句,using如黄色突出显示所示:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using WingtipToys.Models;
    using System.Data.Entity.Infrastructure;
    
  3. 向 方法添加一个 catchAddProductRetryLimitExceededException 以便记录 为以黄色突出显示的 :

    public bool AddProduct(string ProductName, string ProductDesc, string ProductPrice, string ProductCategory, string ProductImagePath)
    {
        var myProduct = new Product();
        myProduct.ProductName = ProductName;
        myProduct.Description = ProductDesc;
        myProduct.UnitPrice = Convert.ToDouble(ProductPrice);
        myProduct.ImagePath = ProductImagePath;
        myProduct.CategoryID = Convert.ToInt32(ProductCategory);
    
        using (ProductContext _db = new ProductContext())
        {
            // Add product to DB.
            _db.Products.Add(myProduct);
            try
            {
                _db.SaveChanges();
            }
            catch (RetryLimitExceededException ex)
            {
                // Log the RetryLimitExceededException.
                WingtipToys.Logic.ExceptionUtility.LogException(ex, "Error: RetryLimitExceededException -> RemoveProductButton_Click in AdminPage.aspx.cs");
            }
        }
        // Success.
        return true;
    }
    

通过添加 RetryLimitExceededException 异常,可以提供更好的日志记录或向用户显示错误消息,用户可以选择再次尝试该过程。 通过捕获异常 RetryLimitExceededException ,可能为暂时性的唯一错误已多次尝试并失败。 返回的实际异常将包装在异常中 RetryLimitExceededException 。 此外,还添加了常规 catch 块。 有关异常 RetryLimitExceededException 的详细信息,请参阅 实体框架连接复原/重试逻辑

命令拦截

现在,你已打开重试策略,如何测试以验证它是否按预期工作? 强制发生暂时性错误并不容易,尤其是在本地运行时,将实际的暂时性错误集成到自动化单元测试中尤其困难。 若要测试连接复原功能,需要一种方法来截获 Entity Framework 发送到SQL Server的查询,并将SQL Server响应替换为通常暂时性的异常类型。

还可以使用查询拦截来实现云应用程序的最佳做法:记录对外部服务(如数据库服务)的所有调用的延迟和成功或失败。

在本教程的此部分,你将使用 Entity Framework 的 拦截功能 进行日志记录和模拟暂时性错误。

创建日志记录接口和类

日志记录的最佳做法是使用 interface 对 日志记录类的调用而不是硬编码调用 System.Diagnostics.Trace 来执行此操作。 这样,以后可以更轻松地更改日志记录机制(如果需要这样做)。 因此,在本部分中,你将创建日志记录接口和类来实现它。

根据上述过程,你已在 Visual Studio 中下载并打开了 WingtipToys 示例应用程序。

  1. WingtipToys 项目中创建一个文件夹,并将其命名为 Logging

  2. Logging 文件夹中,创建名为 ILogger.cs 的类文件,并将默认代码替换为以下代码:

    using System;
     
    namespace WingtipToys.Logging
    {
        public interface ILogger
        {
            void Information(string message);
            void Information(string fmt, params object[] vars);
            void Information(Exception exception, string fmt, params object[] vars);
    
            void Warning(string message);
            void Warning(string fmt, params object[] vars);
            void Warning(Exception exception, string fmt, params object[] vars);
    
            void Error(string message);
            void Error(string fmt, params object[] vars);
            void Error(Exception exception, string fmt, params object[] vars);
    
            void TraceApi(string componentName, string method, TimeSpan timespan);
            void TraceApi(string componentName, string method, TimeSpan timespan, string properties);
            void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars);
    
        }
    }
    

    接口提供三个跟踪级别来指示日志的相对重要性,一个跟踪级别旨在为数据库查询等外部服务调用提供延迟信息。 日志记录方法具有重载,可让你传入异常。 这样,实现 接口的类就能够可靠地记录包括堆栈跟踪和内部异常的异常信息,而不是依赖于在整个应用程序中每个日志记录方法调用中完成的异常信息。

    使用TraceApi方法可以跟踪对外部服务(如SQL 数据库)每次调用的延迟。

  3. Logging 文件夹中,创建名为 Logger.cs 的 类文件,并将默认代码替换为以下代码:

    using System;
    using System.Diagnostics;
    using System.Text;
     
    namespace WingtipToys.Logging
    {
      public class Logger : ILogger
      {
     
        public void Information(string message)
        {
          Trace.TraceInformation(message);
        }
     
        public void Information(string fmt, params object[] vars)
        {
          Trace.TraceInformation(fmt, vars);
        }
     
        public void Information(Exception exception, string fmt, params object[] vars)
        {
          Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars));
        }
     
        public void Warning(string message)
        {
          Trace.TraceWarning(message);
        }
     
        public void Warning(string fmt, params object[] vars)
        {
          Trace.TraceWarning(fmt, vars);
        }
     
        public void Warning(Exception exception, string fmt, params object[] vars)
        {
          Trace.TraceWarning(FormatExceptionMessage(exception, fmt, vars));
        }
     
        public void Error(string message)
        {
          Trace.TraceError(message);
        }
     
        public void Error(string fmt, params object[] vars)
        {
          Trace.TraceError(fmt, vars);
        }
     
        public void Error(Exception exception, string fmt, params object[] vars)
        {
          Trace.TraceError(FormatExceptionMessage(exception, fmt, vars));
        }
     
        public void TraceApi(string componentName, string method, TimeSpan timespan)
        {
          TraceApi(componentName, method, timespan, "");
        }
     
        public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars)
        {
          TraceApi(componentName, method, timespan, string.Format(fmt, vars));
        }
        public void TraceApi(string componentName, string method, TimeSpan timespan, string properties)
        {
          string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties);
          Trace.TraceInformation(message);
        }
     
        private static string FormatExceptionMessage(Exception exception, string fmt, object[] vars)
        {
          var sb = new StringBuilder();
          sb.Append(string.Format(fmt, vars));
          sb.Append(" Exception: ");
          sb.Append(exception.ToString());
          return sb.ToString();
        }
      }
    }
    

实现使用 System.Diagnostics 执行跟踪。 这是 .NET 的内置功能,使生成和使用跟踪信息变得容易。 有许多“侦听器”可用于 System.Diagnostics 跟踪,例如,将日志写入文件,或将其写入 Windows Azure 中的 Blob 存储。 有关详细信息,请参阅 对 Visual Studio 中的 Windows Azure 网站进行故障排除中的一些选项和其他资源的链接。 在本教程中,你将仅查看 Visual Studio 输出 窗口中的日志。

在生产应用程序中,可以考虑使用 以外的 System.Diagnostics跟踪框架,如果决定这样做, ILogger 则接口可以相对轻松地切换到其他跟踪机制。

创建侦听器类

接下来,你将创建实体框架在每次向数据库发送查询时将调用的类,一个用于模拟暂时性错误,另一个用于执行日志记录。 这些侦听器类必须派生自 类 DbCommandInterceptor 。 在它们中,你编写了在即将执行查询时自动调用的方法替代。 在这些方法中,你可以检查或记录要发送到数据库的查询,并且可以在查询发送到数据库之前更改查询,或者自己将某些内容返回到 Entity Framework,甚至无需将查询传递到数据库。

  1. 若要在将每个 SQL 查询发送到数据库之前创建将记录该查询的侦听器类,请在逻辑文件夹中创建名为 InterceptorLogging.cs 的类文件,并将默认代码替换为以下代码:

    using System;
    using System.Data.Common;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure.Interception;
    using System.Data.Entity.SqlServer;
    using System.Data.SqlClient;
    using System.Diagnostics;
    using System.Reflection;
    using System.Linq;
    using WingtipToys.Logging;
    
    namespace WingtipToys.Logic
    {
      public class InterceptorLogging : DbCommandInterceptor
      {
        private ILogger _logger = new Logger();
        private readonly Stopwatch _stopwatch = new Stopwatch();
    
        public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
          base.ScalarExecuting(command, interceptionContext);
          _stopwatch.Restart();
        }
    
        public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
          _stopwatch.Stop();
          if (interceptionContext.Exception != null)
          {
            _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
          }
          else
          {
            _logger.TraceApi("SQL Database", "Interceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
          }
          base.ScalarExecuted(command, interceptionContext);
        }
    
        public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
          base.NonQueryExecuting(command, interceptionContext);
          _stopwatch.Restart();
        }
    
        public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
          _stopwatch.Stop();
          if (interceptionContext.Exception != null)
          {
            _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
          }
          else
          {
            _logger.TraceApi("SQL Database", "Interceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
          }
          base.NonQueryExecuted(command, interceptionContext);
        }
    
        public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
          base.ReaderExecuting(command, interceptionContext);
          _stopwatch.Restart();
        }
        public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
          _stopwatch.Stop();
          if (interceptionContext.Exception != null)
          {
            _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
          }
          else
          {
            _logger.TraceApi("SQL Database", "Interceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
          }
          base.ReaderExecuted(command, interceptionContext);
        }
      }
    }
    

    对于成功的查询或命令,此代码会编写包含延迟信息的信息日志。 对于异常,它会创建错误日志。

  2. 若要在名为 AdminPage.aspx 的页面的“名称”文本框中输入“Throw”时创建将生成虚拟暂时错误的侦听器类,请在逻辑文件夹中创建名为 InterceptorTransientErrors.cs 的类文件,并将默认代码替换为以下代码:

    using System;
    using System.Data.Common;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure.Interception;
    using System.Data.Entity.SqlServer;
    using System.Data.SqlClient;
    using System.Diagnostics;
    using System.Reflection;
    using System.Linq;
    using WingtipToys.Logging;
     
    namespace WingtipToys.Logic
    {
      public class InterceptorTransientErrors : DbCommandInterceptor
      {
        private int _counter = 0;
        private ILogger _logger = new Logger();
     
        public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
          bool throwTransientErrors = false;
          if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "Throw")
          {
            throwTransientErrors = true;
            command.Parameters[0].Value = "TransientErrorExample";
            command.Parameters[1].Value = "TransientErrorExample";
          }
     
          if (throwTransientErrors && _counter < 4)
          {
            _logger.Information("Returning transient error for command: {0}", command.CommandText);
            _counter++;
            interceptionContext.Exception = CreateDummySqlException();
          }
        }
     
        private SqlException CreateDummySqlException()
        {
          // The instance of SQL Server you attempted to connect to does not support encryption
          var sqlErrorNumber = 20;
     
          var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single();
          var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 });
     
          var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true);
          var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic);
          addMethod.Invoke(errorCollection, new[] { sqlError });
     
          var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single();
          var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() });
     
          return sqlException;
        }
      }
    }
    

    此代码仅重写 ReaderExecuting 方法,该方法针对可返回多行数据的查询调用。 如果要为其他类型的查询检查连接复原能力,还可以像日志记录侦听器那样重写 NonQueryExecutingScalarExecuting 方法。

    稍后,你将以“管理员”身份登录,然后选择顶部导航栏上的管理员链接。 然后,在 AdminPage.aspx 页上,将添加名为“Throw”的产品。 该代码为错误号 20(已知通常是暂时性的类型)创建一个虚拟SQL 数据库异常。 当前识别为暂时性的其他错误号为 64、233、10053、10054、10060、10928、10929、40197、40501 和 40613,但这些错误在新版本的 SQL 数据库 中可能会更改。 产品将重命名为“TransientErrorExample”,你可以在 InterceptorTransientErrors.cs 文件的代码中遵循该名称。

    代码将异常返回到 Entity Framework,而不是运行查询并传递回结果。 暂时性异常将返回 次,然后代码将还原为将查询传递到数据库的正常过程。

    由于记录了所有内容,因此你将能够看到 Entity Framework 在最终成功之前尝试执行查询四次,而应用程序的唯一区别是呈现包含查询结果的页面所需的时间更长。

    实体框架将重试的次数可配置;代码指定四次,因为这是SQL 数据库执行策略的默认值。 如果更改执行策略,则还会更改此处的代码,用于指定生成暂时性错误的次数。 还可以更改代码以生成更多异常,以便 Entity Framework 引发异常 RetryLimitExceededException

  3. Global.asax 中,添加以下 using 语句:

    using System.Data.Entity.Infrastructure.Interception;
    
  4. 然后,将突出显示的行添加到 Application_Start 方法:

    void Application_Start(object sender, EventArgs e)
    {
        // Code that runs on application startup
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    
        // Initialize the product database.
        Database.SetInitializer(new ProductDatabaseInitializer());
    
        // Create administrator role and user.
        RoleActions roleActions = new RoleActions();
        roleActions.createAdmin();
    
        // Add Routes.
        RegisterRoutes(RouteTable.Routes);
    
        // Logging.
        DbInterception.Add(new InterceptorTransientErrors());
        DbInterception.Add(new InterceptorLogging());
      
    }
    

当实体框架向数据库发送查询时,这些代码行会导致侦听器代码运行。 请注意,由于为暂时性错误模拟和日志记录创建了单独的侦听器类,因此可以独立启用和禁用它们。

可以在代码中的任意位置使用 DbInterception.Add 方法添加侦听器;它不必位于 方法中 Application_Start 。 如果不在 方法中添加 Application_Start 侦听器,另一个选项是更新或添加名为 WingtipToysConfiguration.cs 的类,并将上述代码放在 类的构造函数的 WingtipToysConfiguration 末尾。

无论在何处放置此代码,请注意不要对同一侦听器执行 DbInterception.Add 多次,否则将获得其他侦听器实例。 例如,如果添加日志记录侦听器两次,则每个 SQL 查询将看到两个日志。

侦听器按注册顺序执行, (DbInterception.Add 调用方法的顺序) 。 顺序可能很重要,具体取决于你在侦听器中执行的操作。 例如,侦听器可能会更改它在 属性中获取的 CommandText SQL 命令。 如果它确实更改了 SQL 命令,则下一个侦听器将获取已更改的 SQL 命令,而不是原始 SQL 命令。

你编写了暂时性错误模拟代码,这样就可以通过在 UI 中输入其他值来引发暂时性错误。 作为替代方法,可以编写侦听器代码来始终生成暂时性异常序列,而无需检查特定参数值。 然后,仅当想要生成暂时性错误时,才能添加侦听器。 但是,如果执行此操作,则在数据库初始化完成之前不要添加侦听器。 换句话说,在开始生成暂时性错误之前,至少执行一个数据库操作,例如对其中一个实体集的查询。 实体框架在数据库初始化期间执行多个查询,它们不会在事务中执行,因此初始化过程中的错误可能会导致上下文进入不一致状态。

测试日志记录和连接复原能力

  1. 在 Visual Studio 中,按 F5 在调试模式下运行应用程序,然后使用“Pa$$word”作为密码以“管理员”身份登录。

  2. 从顶部导航栏中选择“管理员”。

  3. 输入名为“Throw”的新产品,其中包含相应的说明、价格和图像文件。

  4. “添加产品 ”按钮。
    你会注意到,在实体框架多次重试查询时,浏览器似乎挂起了几秒钟。 第一次重试发生得非常快,然后等待时间会增大,然后再进行一次额外的重试。 每次重试之前等待更长时间的过程称为 指数退避

  5. 等待页面不再尝试加载。

  6. 停止项目并查看 Visual Studio 输出 窗口以查看跟踪输出。 可以通过选择“调试”-Windows ->>“输出”找到“输出”窗口。 可能需要滚动浏览记录器写入的其他几个日志。

    请注意,可以看到发送到数据库的实际 SQL 查询。 可以看到 Entity Framework 为开始使用而执行的一些初始查询和命令,检查数据库版本和迁移历史记录表。
    输出窗口
    请注意,除非停止应用程序并重启应用程序,否则无法重复此测试。 如果希望能够在应用程序的一次运行中多次测试连接复原能力,可以编写代码来重置 中的 InterceptorTransientErrors 错误计数器。

  7. 若要查看执行策略 (重试策略) 的差异,请注释掉SetExecutionStrategy逻辑文件夹中 WingtipToysConfiguration.cs 文件中的行,在调试模式下再次运行管理员页,然后再次添加名为“Throw”的产品。

    这一次,调试器会在第一次尝试执行查询时立即停止第一次生成的异常。
    调试 - 查看详细信息

  8. 取消注释 SetExecutionStrategyWingtipToysConfiguration.cs 文件中的行。

总结

本教程介绍了如何修改Web Forms示例应用程序以支持连接复原和命令拦截。

后续步骤

在 ASP.NET Web Forms 中查看连接复原能力和命令拦截后,请查看 ASP.NET 4.5 中的 ASP.NET Web Forms主题异步方法。 本主题将介绍使用 Visual Studio 生成异步 ASP.NET Web Forms应用程序的基础知识。