教程:在 ASP.NET MVC 应用中通过实体框架使用连接复原能力和命令拦截

到目前为止,应用程序已在开发计算机上的 IIS Express 本地运行。 若要使实际应用程序可供其他人通过 Internet 使用,必须将其部署到 Web 托管提供程序,并且必须将数据库部署到数据库服务器。

本教程介绍如何使用连接复原能力和命令拦截。 它们是 Entity Framework 6 的两项重要功能,在部署到云环境时尤其有价值:连接复原 (自动重试暂时性错误) 和命令拦截 (捕获发送到数据库的所有 SQL 查询,以便) 记录或更改它们。

此连接复原和命令拦截教程是可选的。 如果跳过本教程,后续教程中必须进行一些细微的调整。

在本教程中,你将了解:

  • 启用连接复原能力
  • 启用命令拦截
  • 测试新配置

先决条件

启用连接复原能力

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

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

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

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

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

可以为实体框架提供程序支持的任何数据库环境手动配置这些设置,但通常适用于使用 Windows Azure SQL 数据库的联机应用程序的默认值已经为你配置,这些是你将为 Contoso University 应用程序实现的设置。

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

  1. 在 DAL 文件夹中,添加名为 SchoolConfiguration.cs 的课堂文件。

  2. 将模板代码替换为以下代码:

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

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

  3. StudentController.cs 中,为 System.Data.Entity.Infrastructure添加 using 语句。

    using System.Data.Entity.Infrastructure;
    
  4. 更改捕获DataException异常的所有catch块,使其改为捕获RetryLimitExceededException异常。 例如:

    catch (RetryLimitExceededException /* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
    }
    

    你正在使用 DataException 来尝试识别可能是暂时性的错误,以便提供友好的“重试”消息。 但是,现在你已打开重试策略,可能为暂时性的唯一错误已经尝试并失败了几次,返回的实际异常将包装在异常中 RetryLimitExceededException

有关详细信息,请参阅 Entity Framework 连接复原/重试逻辑

启用命令拦截

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

还可以使用查询拦截来实现云应用程序的最佳做法: 记录对外部服务(如数据库服务)的所有调用的延迟和成功或失败 。 EF6 提供了 一个专用的日志记录 API ,可以更轻松地进行日志记录,但在本教程的此部分,你将了解如何直接使用 Entity Framework 的 拦截功能 进行日志记录和模拟暂时性错误。

创建日志记录接口和类

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

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

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

    using System;
    
    namespace ContosoUniversity.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 ContosoUniversity.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)
            {
                // Simple exception formatting: for a more comprehensive version see 
                // https://code.msdn.microsoft.com/windowsazure/Fix-It-app-for-Building-cdd80df4
                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 跟踪,例如,将日志写入文件,或将其写入 Azure 中的 Blob 存储。 有关详细信息,请参阅 对 Visual Studio 中的 Azure 网站进行故障排除中的一些选项和其他资源的链接。 在本教程中,你只会查看 Visual Studio “输出 ”窗口中的日志。

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

创建侦听器类

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

  1. 若要创建将记录发送到数据库的每个 SQL 查询的侦听器类,请在 DAL 文件夹中创建名为 SchoolInterceptorLogging.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 ContosoUniversity.Logging;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolInterceptorLogging : 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", "SchoolInterceptor.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", "SchoolInterceptor.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", "SchoolInterceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
                }
                base.ReaderExecuted(command, interceptionContext);
            }
        }
    }
    

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

  2. 若要在“搜索”框中输入“Throw”时创建将生成虚拟暂时性错误的侦听器类,请在 DAL 文件夹中创建名为 SchoolInterceptorTransientErrors.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 ContosoUniversity.Logging;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolInterceptorTransientErrors : 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 = "%an%";
                    command.Parameters[1].Value = "%an%";
                }
    
                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 方法,就像日志记录侦听器所做的那样。

    运行“学生”页并输入“Throw”作为搜索字符串时,此代码会为错误号 20 创建一个虚拟SQL 数据库异常,该错误类型通常为暂时性类型。 当前识别为暂时性的其他错误号为 64、233、10053、10054、10060、10928、10929、40197、40501 和 40613,但这些错误在新版本的 SQL 数据库 中可能会更改。

    代码将异常返回到实体框架,而不是运行查询并传递回查询结果。 暂时性异常返回四次,然后代码将恢复为将查询传递到数据库的正常过程。

    由于记录了所有内容,因此可以看到实体框架在最终成功之前尝试执行查询四次,应用程序的唯一区别是呈现包含查询结果的页面需要更长的时间。

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

    在“搜索”框中输入的值将位于 中 command.Parameters[0]command.Parameters[1] (一个用于名字,一个用于姓氏) 。 找到值“%Throw%”后,这些参数中的“Throw”将替换为“an”,以便找到并返回一些学生。

    这只是一种基于更改应用程序 UI 的输入来测试连接复原能力的便捷方法。 还可以编写代码,为所有查询或更新生成暂时性错误,如后面有关 DbInterception.Add 方法的注释中所述。

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

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

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        DbInterception.Add(new SchoolInterceptorTransientErrors());
        DbInterception.Add(new SchoolInterceptorLogging());
    }
    

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

    可以在代码中的任何位置使用 DbInterception.Add 方法添加侦听器;它不必位于 方法中 Application_Start 。 另一个选项是将此代码放入之前创建的 DbConfiguration 类中,以配置执行策略。

    public class SchoolConfiguration : DbConfiguration
    {
        public SchoolConfiguration()
        {
            SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            DbInterception.Add(new SchoolInterceptorTransientErrors());
            DbInterception.Add(new SchoolInterceptorLogging());
        }
    }
    

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

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

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

测试新配置

  1. F5 在调试模式下运行应用程序,然后单击“ 学生 ”选项卡。

  2. 查看“Visual Studio 输出 ”窗口以查看跟踪输出。 可能需要向上滚动浏览一些 JavaScript 错误才能访问记录器写入的日志。

    请注意,可以看到发送到数据库的实际 SQL 查询。 你将看到 Entity Framework 为入门而执行的一些初始查询和命令,查看数据库版本和迁移历史记录表, (你将在下一教程) 中了解迁移。 你会看到一个分页查询,用于了解有多少学生,最后会看到获取学生数据的查询。

    正常查询的日志记录

  3. “学生 ”页中,输入“Throw”作为搜索字符串,然后单击“ 搜索”。

    引发搜索字符串

    你会注意到,在实体框架多次重试查询时,浏览器似乎挂起了几秒钟。 第一次重试的发生速度非常快,然后在每次额外的重试之前等待时间增加。 每次重试之前等待更长时间的过程称为 指数退避

    显示页面时,显示其姓名中包含“an”的学生,查看输出窗口,你将看到同一查询已尝试五次,前四次返回暂时性异常。 对于每个暂时性错误,你将看到在类中 SchoolInterceptorTransientErrors 生成暂时性错误时写入的日志 (“返回命令的暂时性错误...”) ,并且你会看到在收到异常时 SchoolInterceptorLogging 写入的日志。

    显示重试的日志记录输出

    由于输入了搜索字符串,因此将参数化返回学生数据的查询:

    SELECT TOP (3) 
        [Project1].[ID] AS [ID], 
        [Project1].[LastName] AS [LastName], 
        [Project1].[FirstMidName] AS [FirstMidName], 
        [Project1].[EnrollmentDate] AS [EnrollmentDate]
        FROM ( SELECT [Project1].[ID] AS [ID], [Project1].[LastName] AS [LastName], [Project1].[FirstMidName] AS [FirstMidName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
            FROM ( SELECT 
                [Extent1].[ID] AS [ID], 
                [Extent1].[LastName] AS [LastName], 
                [Extent1].[FirstMidName] AS [FirstMidName], 
                [Extent1].[EnrollmentDate] AS [EnrollmentDate]
                FROM [dbo].[Student] AS [Extent1]
                WHERE ([Extent1].[LastName] LIKE @p__linq__0 ESCAPE N'~') OR ([Extent1].[FirstMidName] LIKE @p__linq__1 ESCAPE N'~')
            )  AS [Project1]
        )  AS [Project1]
        WHERE [Project1].[row_number] > 0
        ORDER BY [Project1].[LastName] ASC:
    

    你不会记录参数的值,但可以这样做。 如果要查看参数值,可以编写日志记录代码,以从 Parameters 在侦听器方法中获取的 DbCommand 对象的 属性获取参数值。

    请注意,除非停止应用程序并重启应用程序,否则无法重复此测试。 如果希望能够在应用程序的一次运行中多次测试连接复原能力,可以编写代码来重置 中的 SchoolInterceptorTransientErrors错误计数器。

  4. 若要查看重试策略 () 执行策略的差异,请在 SchoolConfiguration.cs 中注释掉SetExecutionStrategy行,再次在调试模式下运行“学生”页,然后再次搜索“Throw”。

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

    虚拟异常

  5. 取消注释 SchoolConfiguration.cs 中的 SetExecutionStrategy 行。

获取代码

下载已完成项目

其他资源

可以在 ASP.NET 数据访问 - 推荐资源中找到指向其他实体框架资源的链接。

后续步骤

在本教程中,你将了解:

  • 已启用连接复原能力
  • 已启用命令拦截
  • 测试了新配置

请转到下一篇文章,了解 Code First 迁移和 Azure 部署。