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

作者: Erik Reitan

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

注意

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

学习内容:

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

先决条件

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

连接复原

考虑将应用程序部署到 Windows Azure 时,需要考虑的一个选项是将数据库部署到云数据库服务 Windows Azure SQL 数据库。 连接到云数据库服务时,暂时性连接错误通常比 Web 服务器和数据库服务器在同一数据中心直接连接在一起时更频繁。 即使云 Web 服务器和云数据库服务托管在同一数据中心,它们之间也有更多的网络连接可能存在问题,例如负载均衡器。

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

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

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

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

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

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

实现连接复原

  1. 下载并打开 Visual Studio 中的 WingtipToys 示例 Web 窗体应用程序。

  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());
            }
        }
    }
    

Entity Framework 会自动运行它在派生自 DbConfiguration的类中找到的代码。 可以使用DbConfiguration该类在 Web.config 文件中执行的代码中的配置任务。 有关详细信息,请参阅 EntityFramework 基于代码的配置

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

  2. using为黄色突出显示的语句添加语句System.Data.Entity.Infrastructure

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using WingtipToys.Models;
    using System.Data.Entity.Infrastructure;
    
  3. catch将块添加到AddProduct方法,以便以黄色突出显示的方式记录该RetryLimitExceededException块:

    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 连接复原/重试逻辑

命令拦截

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

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

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

创建日志记录接口和类

日志记录的最佳做法是使用 interface 对或日志记录类的硬编码调用 System.Diagnostics.Trace 来执行日志记录。 这样,如果以后需要更改日志记录机制,就更容易了。 因此,在本部分中,你将创建日志记录接口和一个用于实现它的类。

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

  1. WingtipToys 项目中创建文件夹并将其命名为 日志记录

  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 该方法,该方法针对可返回多行数据的查询调用。 如果想要检查其他类型的查询的连接复原能力,还可以替代 NonQueryExecuting 日志记录拦截器所做的操作和 ScalarExecuting 方法。

    稍后,你将以“管理员”身份登录,并选择 顶部导航栏上的“管理员 ”链接。 然后,在 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());
      
    }
    

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

可以在代码中的任何位置使用 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. 按“添加产品”按钮。
    你会注意到,当 Entity Framework 多次重试查询时,浏览器似乎挂起了几秒钟。 第一次重试非常快,然后等待将增加,然后再进行额外的重试。 在每次重试 称为指数退避 之前,此过程会等待更长时间。

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

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

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

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

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

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

总结

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

后续步骤

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