[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:MVC程序中实体框架的链接恢复和命令拦截

这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第四篇:MVC程序中实体框架的链接恢复和命令拦截程序员

原文:Connection Resiliency and Command Interception with the Entity Framework in an ASP.NET MVC Applicationweb

译文版权全部,谢绝全文转载——但您能够在您的网站上添加到该教程的连接。sql

到目前为止,应用程序已经能够在您本地机器上正常地运行。但若是您想将它发布在互联网上以便更多的人来使用,您须要将程序部署到WEB服务器并将数据库部署到数据库服务器。数据库

在本教程中,您将学习在将实体框架部署到云环境时很是有价值的两个特色:链接回复(瞬时错误的自动重试)和命令拦截(捕捉全部发送到数据库的SQL查询语句,以便将它们记录在日志中或更改它们)。编程

注意:本节教程是可选的。若是您跳过本节,咱们会在后续的教程中作一些细微的调整。windows

启用链接恢复

当您将应用程序部署到Windows Azure时,您会将数据库部署到Windows Azure SQL数据库中——一个云数据库服务。和您将Web服务器和数据库直接链接在同一个数据中心相比,链接一个云数据库服务更容易遇到瞬时链接错误。即便云Web服务器和云数据库服务器在同一数据中心机房中,它们之间在出现大量数据链接时也很容易出现各类问题,好比负载均衡。浏览器

另外,云服务器一般是由其余用户共享的,这意味着可能会受到其它用户的影响。您对数据库的存取权限可能受到限制,当您尝试频繁的访问数据库服务器时也可能遇到基于SLA的带宽限制。大多数链接问题都是在您链接到云服务器时瞬时发生的,它们会尝试在短期内自动解决问题。因此当您尝试链接数据库并遇到一个错误,该错误极可能是瞬时的,当您重复尝试后可能该错误就再也不存在。您可使用自动瞬时错误重试来提升您的客户体验。实体框架6中的链接恢复能自动对错误的SQL查询进行重试。服务器

链接恢复功能只能针对特定的数据库服务进行正确的配置后才可用:网络

  • 必须知道那些异常有多是暂时的,您想要重试因为网络链接而形成的错误,而不是编程Bug带来的。
  • 在失败操做的间隔中必须等待适当的时间。批量重试时在线用户可能会须要等待较长时间才可以得到响应。
  • 须要设置一个适当的重试次数。在一个在线的应用程序中,您可能会进行屡次重试。

您能够为任何实体框架提供程序支持的数据库环境来手动配置这些设定,但实体框架已经为使用Windows Azure SQL数据库的在线应用程序作了缺省配置。接下来咱们将在Contoso大学中实施这些配置。mvc

若是要启用链接恢复,您须要在您的程序集中建立一个从DbConfiguration派生的类,该类将用来配置SQL数据库执行策略,其中包含链接恢复的重试策略。

  1. 在DAL文件夹中,添加一个名为SchoolConfiguration.cs的新类。
  2. 使用如下代码替换类中的:
     1 using System.Data.Entity;
     2 using System.Data.Entity.SqlServer;
     3 
     4 namespace ContosoUniversity.DAL
     5 {
     6     public class SchoolConfiguration : DbConfiguration
     7     {
     8         public SchoolConfiguration()
     9         {
    10             SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
    11         }
    12     }
    13 }

    实体框架会自动运行从DbConfiguration类派生的类中找到的代码,你一样也可使用Dbconfiguration类来在web.config中进行配置,详细信息请参阅EntityFramework Code-Based Configuration

  3. 在学生控制器中,添加引用:
    using System.Data.Entity.Infrastructure;
  4. 更改全部捕获DataException的异常代码块,使用RetryLimitExcededException:
    catch (RetryLimitExceededException)
                {
                    ModelState.AddModelError("", "保存数据时出现错误。请重试,若是问题依旧存在请联系系统管理员。");
                }

    在以前,你使用了DataException。这样会尝试找出可能包含瞬时错误的异常,而后返回给用户一个友好的重试提示消息,但如今你已经开启自动重试策略,屡次重试仍然失败的错误将被包装在RetryLimitExceededException异常中返回。

有关详细信息,请参阅Entity Framework Connection Resiliency / Retry Logic

启用命令拦截

如今你已经打开了重试策略,但你如何进行测试已验证它是否像预期的那样正常工做?强迫发出一个瞬时错误并不容易,尤为是您正在本地运行的时候。并且瞬时错误也难以融入自动化的单元测试中。若是要测试链接恢复功能,您须要一种能够拦截实体框架发送到SQL数据库查询的方法并替代SQL数据库返回响应。

你也能够在一个云应用程序上按照最佳作法:log the latency and success or failure of all calls to external services来实现查询拦截。实体框架6提供了一个dedicated logging API使它易于记录。但在本教程中,您将学习如何直接使用实体框架的interception feature(拦截功能),包括日志记录和模拟瞬时错误。

建立一个日志记录接口和类

best practice for logging是经过接口而不是使用硬编码调用System.Diagnostice.Trace或日志记录类。这样可使得之后在须要时更容易地更改日志记录机制。因此在本节中,咱们将建立一个接口并实现它。

  1. 在项目中建立一个文件夹并命名为Logging。
  2. 在Logging文件夹中,建立一个名为ILogger.cs的接口类,使用下面的代码替换自动生成的:
     1 using System;
     2 
     3 
     4 namespace ContosoUniversity.Logging
     5 {
     6     public interface ILogger
     7     {
     8         void Information(string message);
     9         void Information(string fmt, params object[] vars);
    10         void Information(Exception exception, string fmt, params object[] vars);
    11 
    12         void Warning(string message);
    13         void Warning(string fmt, params object[] vars);
    14         void Warning(Exception exception, string fmt, params object[] vars);
    15 
    16         void Error(string message);
    17         void Error(string fmt, params object[] vars);
    18         void Error(Exception exception, string fmt, params object[] vars);
    19 
    20         void TraceApi(string componentName, string method, TimeSpan timespan);
    21         void TraceApi(string componentName, string method, TimeSpan timespan, string properties);
    22         void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars);
    23 
    24 
    25     }
    26 }

    该接口提供了三个跟踪级别用来指示日志的相对重要性,而且设计为能够提供外部服务调用(例如数据库查询)的延迟信息。日志方法提供了可让你传递异常的重载。这样异常信息能够包含在栈中而且内部异常可以可靠地被该接口实现的类记录下来,而不是依靠从应用程序的每一个日志方法来调用并记录。

    TraceAPI方法使您可以跟踪到外部服务(例如SQL Server)的每次调用的延迟时间。
  3. 在Logging文件夹中,建立一个名为Logger.cs的类,使用下面的代码替换自动生成的:
     1 using System;
     2 using System.Diagnostics;
     3 using System.Text;
     4 
     5 namespace ContosoUniversity.Logging
     6 {
     7     public class Logger : ILogger
     8     {
     9 
    10         public void Information(string message)
    11         {
    12             Trace.TraceInformation(message);
    13         }
    14 
    15         public void Information(string fmt, params object[] vars)
    16         {
    17             Trace.TraceInformation(fmt, vars);
    18         }
    19 
    20         public void Information(Exception exception, string fmt, params object[] vars)
    21         {
    22             Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars));
    23         }
    24 
    25         public void Warning(string message)
    26         {
    27             Trace.TraceWarning(message);
    28         }
    29 
    30         public void Warning(string fmt, params object[] vars)
    31         {
    32             Trace.TraceWarning(fmt, vars);
    33         }
    34 
    35         public void Warning(Exception exception, string fmt, params object[] vars)
    36         {
    37             throw new NotImplementedException();
    38         }
    39 
    40         public void Error(string message)
    41         {
    42             Trace.TraceError(message);
    43         }
    44 
    45         public void Error(string fmt, params object[] vars)
    46         {
    47             Trace.TraceError(fmt, vars);
    48         }
    49 
    50         public void Error(Exception exception, string fmt, params object[] vars)
    51         {
    52             Trace.TraceError(FormatExceptionMessage(exception, fmt, vars));
    53         }
    54 
    55 
    56 
    57         public void TraceApi(string componentName, string method, TimeSpan timespan)
    58         {
    59             TraceApi(componentName, method, timespan, "");
    60         }
    61 
    62         public void TraceApi(string componentName, string method, TimeSpan timespan, string properties)
    63         {
    64             string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties);
    65             Trace.TraceInformation(message);
    66         }
    67 
    68         public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars)
    69         {
    70             TraceApi(componentName, method, timespan, string.Format(fmt, vars));
    71         }
    72         private string FormatExceptionMessage(Exception exception, string fmt, object[] vars)
    73         {
    74             var sb = new StringBuilder();
    75             sb.Append(string.Format(fmt, vars));
    76             sb.Append(" Exception: ");
    77             sb.Append(exception.ToString());
    78             return sb.ToString();
    79         }
    80     }
    81 }

    咱们使用了System.Diagnostics来进行跟踪。这是.Net的使它易于生成并使用跟踪信息的一个内置功能。你可使用System.Diagnostics的多种侦听器来进行跟踪并写入日志文件。例如,将它们存入blob storage或存储在Windows Azure。在 Troubleshooting Windows Azure Web Sites in Visual Studio中你能够找到更多选项及相关信息。在本教程中您将只在VS输出窗口看到日志。

    在生产环境中您可能想要使用跟踪包而非System.Diagnostics,而且而当你须要时,ILogger接口可以使它相对容易地切换到不一样的跟踪机制下。

建立拦截器类

接下来您将建立几个类,这些类在实体框架在每次查询数据库时都会被调用。其中一个模拟瞬时错误而另外一个进行日志记录。这些拦截器类必须从DbCommandInterceptor类派生。你须要重写方法使得查询执行时会自动调用。在这些方法中您能够检查或记录被发往数据库中的查询,而且能够再查询发送到数据库以前对它们进行修改,甚至不将它们发送到数据库进行查询而直接返回结果给实体框架。

  1. 在DAL文件夹中建立一个名为SchoolInterceptorLogging.cs的类,并使用下面的代码替换自动生成的:
     1 using System;
     2 using System.Data.Common;
     3 using System.Data.Entity;
     4 using System.Data.Entity.Infrastructure.Interception;
     5 using System.Data.Entity.SqlServer;
     6 using System.Data.SqlClient;
     7 using System.Diagnostics;
     8 using System.Reflection;
     9 using System.Linq;
    10 using ContosoUniversity.Logging;
    11 
    12 namespace ContosoUniversity.DAL
    13 {
    14     public class SchoolInterceptorLogging : DbCommandInterceptor
    15     {
    16         private ILogger _logger = new Logger();
    17         private readonly Stopwatch _stopwatch = new Stopwatch();
    18 
    19         public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    20         {
    21             base.ScalarExecuting(command, interceptionContext);
    22             _stopwatch.Restart();
    23         }
    24 
    25         public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    26         {
    27             _stopwatch.Stop();
    28             if (interceptionContext.Exception != null)
    29             {
    30                 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
    31             }
    32             else
    33             {
    34                 _logger.TraceApi("SQL Database", "SchoolInterceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
    35             }
    36             base.ScalarExecuted(command, interceptionContext);
    37         }
    38         public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    39         {
    40             base.NonQueryExecuting(command, interceptionContext);
    41             _stopwatch.Restart();
    42         }
    43 
    44         public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    45         {
    46             _stopwatch.Stop();
    47             if (interceptionContext.Exception != null)
    48             {
    49                 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
    50             }
    51             else
    52             {
    53                 _logger.TraceApi("SQL Database", "SchoolInterceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
    54             }
    55             base.NonQueryExecuted(command, interceptionContext);
    56         }
    57 
    58         public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    59         {
    60             base.ReaderExecuting(command, interceptionContext);
    61             _stopwatch.Restart();
    62         }
    63         public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    64         {
    65             _stopwatch.Stop();
    66             if (interceptionContext.Exception != null)
    67             {
    68                 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
    69             }
    70             else
    71             {
    72                 _logger.TraceApi("SQL Database", "SchoolInterceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
    73             }
    74             base.ReaderExecuted(command, interceptionContext);
    75         }
    76     }
    77 }

    对于成功查询的命令,这段代码将相关信息及延时信息写入日志中,对于异常,它将建立错误日志。

  2. 在DAL文件夹中建立一个名为SchoolInterceptorTransientErrors.cs的类,该类在当您输入"Throw"到搜索框并进行查询时生成虚拟的瞬时错误。使用如下代码替换自动生成的:

     1 using System;
     2 using System.Data.Common;
     3 using System.Data.Entity;
     4 using System.Data.Entity.Infrastructure.Interception;
     5 using System.Data.Entity.SqlServer;
     6 using System.Data.SqlClient;
     7 using System.Diagnostics;
     8 using System.Reflection;
     9 using System.Linq;
    10 using ContosoUniversity.Logging;
    11 
    12 namespace ContosoUniversity.DAL
    13 {
    14     public class SchoolInterceptorTransientErrors : DbCommandInterceptor
    15     {
    16         private int _counter = 0;
    17         private ILogger _logger = new Logger();
    18 
    19         public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    20         {
    21             bool throwTransientErrors = false;
    22             if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "Throw")
    23             {
    24                 throwTransientErrors = true;
    25                 command.Parameters[0].Value = "an";
    26                 command.Parameters[1].Value = "an";
    27             }
    28 
    29             if (throwTransientErrors && _counter < 4)
    30             {
    31                 _logger.Information("Returning transient error for command: {0}", command.CommandText);
    32                 _counter++;
    33                 interceptionContext.Exception = CreateDummySqlException();
    34             }
    35         }
    36 
    37         private SqlException CreateDummySqlException()
    38         {
    39             // The instance of SQL Server you attempted to connect to does not support encryption
    40             var sqlErrorNumber = 20;
    41 
    42             var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single();
    43             var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 });
    44 
    45             var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true);
    46             var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic);
    47             addMethod.Invoke(errorCollection, new[] { sqlError });
    48 
    49             var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single();
    50             var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() });
    51 
    52             return sqlException;
    53         }
    54     }
    55 }

    这段代码仅重写了用来返回多行查询结果数据的ReaderExcuting方法。若是你想要检查其余类型的链接恢复,你能够重写如NonQueryExecuting和ScalarExecuting方法就像在日志拦截器中所作的那样。
    当您运行学生页面并输入"Throw"做为搜索字符串时,代码将建立一个虚拟的SQL数据库错误数20,被当作瞬时错误类型。目前公认的瞬时错误号码有64,233,10053,10060,10928,10929,40197,40501及40613等,你能够检查新版的SQL 数据库来确认这些信息。

    这段代码返回异常给实体框架而不是运行查询并返回查询结果。瞬时异常将返回4次而后代码将正常运行并将查询结果返回。
    因为咱们有所有的日志记录,你能够看到实体框架进行了4次查询才执行成功,而在应用程序中,惟一的区别是呈现页面所花费的事件变长了。
    实体框架的重试次数是能够配置的,在本代码中咱们设定了4,由于这是SQL数据库执行策略的缺省值。若是您更改执行策略,你一样须要更改现有的代码来指定生成瞬时错误的次数。您一样能够更改代码来生成更多的异常来引起实体框架的RetryLimitExceededException异常。
    您在搜索框中输入的值将保存在command.Parameters[0]和command.Parameters[1]中(一个用于姓而另外一个用于名)。当发现输入值为"Throw"时,参数被替换为"an"从而查询到一些学生并返回。
    这仅仅只是一种经过应用程序的UI来对链接恢复进行测试的方法。您也能够针对更新来编写代码生成瞬时错误。

  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方法添加拦截器,并不必定要在Applicetion_Start中来作。另外一个选择是将这段代码放进以前你建立执行策略的DbConfiguration类中。
    public class SchoolConfiguration : DbConfiguration
    {
        public SchoolConfiguration()
        {
            SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            DbInterception.Add(new SchoolInterceptorTransientErrors());
            DbInterception.Add(new SchoolInterceptorLogging());
        }
    }

    无论你在何处放置这些代码,要当心不要超过一次。对于相同的拦截器执行DbInterception.Add可能会使你获得额外的拦截器实例。例如,若是添加两第二天志记录拦截器,您将看到查询被记录在两个日志中。
    拦截器是按照Add方法的注册顺序执行的。根据你所要进行的操做,顺序可能很重要。例如,第一个拦截器可能会更改CommandText属性,而下一个拦截器获取到的会是更改过的该属性。

    您已经编写完成了模拟瞬时错误的代码,如今能够在用户界面经过输入一个不一样的值来进行测试了。做为替代方法,您能够在拦截器中编写不检查特定参数值而直接生成瞬时错误的代码,记得仅仅当你想要测试瞬时错误时才添加拦截器。

测试日志记录和链接恢复

  1. 按下F5在调试模式下运行该程序,而后点击学生选项卡。
  2. 检查VS输出窗口,查看跟踪输出,您可能要向上滚动窗口内容。
    您能够看到实际被发送到数据库的SQL查询。
  3. 在学生索引页中,输入"Throw"进行查询。

    你会注意到浏览器会挂起几秒钟,显然实体框架正在进行重试查询。第一次重试发生速度很快,而后每次重试查询都会增长一点等待事件。

    当页面执行完成后,检查输出窗口,你会看到相同的查询尝试了5次,前4次都返回了一个瞬时错误异常。对于每一个瞬时错误,你在日志中看到异常的信息。

    返回学生数据的查询是参数化的:
    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 (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstMidName])) AS int)) > 0)
            )  AS [Project1]
        )  AS [Project1]
        WHERE [Project1].[row_number] > 0
        ORDER BY [Project1].[LastName] ASC

    你没有在日志中记录值的参数,固然你也能够选择记录。你能够在拦截器的方法中经过从DbCommand对象的参数属性中获取到属性值。

    请注意您不能重复该测试,除非你中止整个应用程序并从新启动它。若是你想要可以在单个应用程序的运行中进行屡次测试,您能够编写代码来重置SchoolInterceptorTransientErrors中的错误计数器。
  4. 要查看执行策略的区别,注释掉SchoolConfiguration.cs,而后关闭应用程序并从新启动调试,运行学生索引页面并输入"Throw"进行搜索。
            public SchoolConfiguration()
            {
                //SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            }

     

    这一次在尝试第一次查询时,调试器会当即中止并弹出异常。
  5. 取消注释并再试一次,了解之间的不一样。

总结

在本节中你看到了如何启用实体框架的链接恢复,记录发送到数据库的SQL查询命令,在下一节中你会使用Code First Migrations来将其部署该应用程序到互联网中。

做者信息

  Tom Dykstra - Tom Dykstra是微软Web平台及工具团队的高级程序员,做家。

相关文章
相关标签/搜索