EF6与MVC5系列(4):在MVC应用程序中使用弹性连接和命令拦截
本节教程是可选的,如果你跳过本教程,那么需要你在后面的教程中做些调整。
目前为止,网站已经可以在本地运行访问,但是为了更多的人浏览网站,会把网站数据库部署到数据库服务器上。
在这节教程中,将学习到EF6的两个特性:弹性连接和命令拦截。这两个特性在网站部署到云环境中时是很有价值的。弹性连接(connection resiliency):发生暂态误差时自动重试。命令拦截(command interception):捕获所有发送到服务器的sql查询,用于记录和修改。
启用弹性连接
当你把网站部署在Windows Azure中,例如部署在 Windows Azure SQL数据库中,这种情况下发生暂态误差的频率要比访问你本地数据库高些。即使云服务器和云数据服务在同一个数据中心,也会出现一些网络连接错误,比如负载均衡。
云服务是被用户共享的,这意味着用户操作会影响云服务的响应能力。并且你访问数据的时候可能会受到限流(Throttling)。Throttling就是当访问频率高于你数据库的服务水平协议((Service Level Agreement (SLA)))时,数据库抛出异常。
许多或者大多数访问云服务时发生的链接问题都是暂时的,因为数据库会在短时间内自动处理这些问题。所以当我们访问数据库失败并且抛出这种异常时,通常会过些时间再重试就可以访问成功。为了获得更好的用户体验,如果自动重试解决这些问题时,让用户看不到这些错误。这里就用到了EF6的弹性连接特性。
弹性连接特性必须适当地配置为一个特定的数据库服务:
- 它必须要知道什么样的异常是暂时的。你需要重试的是在网络连接过程中出现的暂时错误。而不是代码问题所造成的bug。
- 它必须在每次失败之后重新连接中,需要等待一定时间。
- 它必须尝试若干次数之后才放弃。
这些特性可以手动配置在任何支持EF6的数据库环境中。Windows Azure SQL Database已经将这些配置好。
启用弹性连接的方法,在项目中创建一个类继承自 DbConfiguration类。并且在这个类中设置SQL数据库的执行策略(execution strategy),在EF中又被称为重试策略(retry policy)。
1.在DAL文件夹中,添加SchoolConfiguration.cs类。
2.在SchoolConfiguration.cs类中代码:
1 using System.Data.Entity; 2 using System.Data.Entity.SqlServer; 3 4 namespace ContosoUniversity.DAL 5 { 6 7 public class SchoolConfiguration:DbConfiguration 8 { 9 public SchoolConfiguration() 10 { 11 SetExecutionStrategy("System.Data.SqlClient",()=>new SqlAzureExecutionStrategy()); 12 DbInterception.Add(new SchoolInterceptorTransientErrors()); 13 DbInterception.Add(new SchoolInterceptorLogging()); 14 } 15 } 16 }
EF会自动运行上面继承自DbConfiguration的代码,你可以用DbConfiguration
类在代码中做配置,或者可以在Web.config文件中配置。详情查看: EntityFramework Code-Based Configuration.
3.在StudentController中添加引用: System.Data.Entity.Infrastructure
.
using System.Data.Entity.Infrastructure;
4.将StudentController中所有catch代码段中的DataException
异常改为RetryLimitExceededException
异常
catch (RetryLimitExceededException) { ModelState.AddModelError("", "创建失败!"); }
现在使用了重试策略( retry policy),这样所有的暂态误差抛出的异常将会包含在RetryLimitExceededException异常中。而不是像DataException
那样给出一个“"try again”友好消息。更多信息查看: Entity Framework Connection Resiliency / Retry Logic.
启用命令拦截
启用了重试策略,那么怎么测试它是否如我们预期那样?强制发生暂态误差是不容易的,尤其当项目运行在本地时。也不容易把它整合到自动化单元测试中。为了测试弹性连接,我们需要阻断EF发送到SQL Server数据库的sql查询,然后用暂态误差异常替代SQL Server响应。
在云应用中也可以使用查询拦截: log the latency and success or failure of all calls to external servicesEF提供了 dedicated logging API便于记录。但是这节教程中我们直接使用EF6的拦截特性(interception feature)用于模拟和记录暂态误差。
创建用于记录的接口和类
日志或者记录的最好方法是编写接口,而不是采用硬编码的方式调用System.Diagnostics.Trace或者某个日志类。这样即使发现不需要如此记录之后也便于更改日志。因此本节中我们将创建用于记录的接口和它的实现类。
1.在项目中创建 Logging文件夹。
2.在 Logging文件夹中创建接口 ILogger.cs。
1 using System; 2 3 namespace ContosoUniversity.Logging 4 { 5 public interface ILogger 6 { 7 void Information(string message); 8 void Information(string fmt,params object[] vars); 9 void Information(Exception exception, string fmt,params object[] vars); 10 void Warning(string message); 11 void Warning(string fmt,params object[] vars); 12 void Warning(Exception exception, string fmt, params object[] vars); 13 void Error(string message); 14 void Error(string fmt, params object[] vars); 15 void Error(Exception exception, string fmt, params object[] vars); 16 void TraceApi(string componetName,string method,TimeSpan timespan); 17 void TraceApi(string componetName, string method, TimeSpan timespan, string propertity); 18 void TraceApi(string componetName, string method, TimeSpan timespan,string fmt,params object[]vars); 19 20 } 21 }
接口定义了三个跟踪级别表示日志的相对重要性。还有一个 TraceApi方法是用于提供外部服务调用(例如sql查询)的延迟信息。使用重载这样的话堆栈跟踪和内部异常的异常信息都可以记下来,这样不要调用logging中的每个方法。
TraceApi方法可以跟踪调用其他服务产生的延迟(例如sql查询)
3.在Logging文件夹中添加Logger.cs类实现ILogger接口。
1 using System; 2 using System.Diagnostics; 3 using System.Text; 4 5 namespace ContosoUniversity.Logging 6 { 7 public class Logger:ILogger 8 { 9 public void Information(string message) 10 { 11 Trace.TraceInformation(message); 12 } 13 14 public void Information(string fmt, params object[] vars) 15 { 16 Trace.TraceInformation(fmt,vars); 17 } 18 19 public void Information(Exception exception, string fmt, params object[] vars) 20 { 21 Trace.TraceInformation(FormatExceptionMessage(exception,fmt,vars)); 22 } 23 24 public void Warning(string message) 25 { 26 Trace.TraceWarning(message); 27 } 28 29 public void Warning(string fmt, params object[] vars) 30 { 31 Trace.TraceWarning(fmt,vars); 32 } 33 34 public void Warning(Exception exception, string fmt, params object[] vars) 35 { 36 Trace.TraceWarning(FormatExceptionMessage(exception, fmt, vars)); 37 } 38 39 public void Error(string message) 40 { 41 Trace.TraceError(message); 42 } 43 44 public void Error(string fmt, params object[] vars) 45 { 46 Trace.TraceError(fmt,vars); 47 } 48 49 public void Error(Exception exception, string fmt, params object[] vars) 50 { 51 Trace.TraceError(FormatExceptionMessage(exception, fmt, vars)); 52 } 53 54 public void TraceApi(string componetName, string method, TimeSpan timespan) 55 { 56 TraceApi(componetName,method,timespan,""); 57 } 58 59 public void TraceApi(string componetName, string method, TimeSpan timespan, string fmt, params object[] vars) 60 { 61 TraceApi(componetName,method,timespan,string.Format(fmt,vars)); 62 } 63 64 public void TraceApi(string componetName, string method, TimeSpan timespan, string propertity) 65 { 66 string message = string.Concat("Component",componetName,";Method:",method,";Timespan:",timespan.ToString(),";Properties:",propertity); 67 Trace.TraceInformation(message); 68 } 69 private static string FormatExceptionMessage(Exception exception,string fmt,object[] vars) 70 { 71 var sb = new StringBuilder(); 72 sb.Append(string.Format(fmt,vars)); 73 sb.Append("Exception:"); 74 sb.Append(exception.ToString()); 75 return sb.ToString(); 76 } 77 } 78 }
使用 System.Diagnostics跟踪。 System.Diagnostics是.NET内置特性。它更加容易生成和使用跟踪信息。它之中有很多监听器将跟踪内容写到文件或者Azure中的云端文件系统。更多信息请查看:Troubleshooting Azure Web Sites in Visual Studio。而在本节中,我们会将这些信息记录在“输出”窗口中。
在实际项目中,你可能除了System.Diagnostics还要考虑到跟踪包,在不同的跟踪机制下相互转换比较容易。
创建拦截器类
接下来创建一个类,EF每次发送sql到数据库的时候都会调用这个类,一个类用于模拟暂态误差,一个类用于记录。这些拦截器类必须继承自DbCommandInterceptor
类。在拦截器类中重写调用查询时执行的方法。这些方法可以检查或者记录要发送到数据库中的查询语句,而你可以在这些语句被发送到数据库之前修改他们,或者不像数据库中传递查询语句,而是向EF返回你自己的东西
1.创建拦截类用于记录每次想数据发送的sql查询, 在DAL文件夹中创建类:SchoolInterceptorLogging.cs
1 using System.Data.Entity.Infrastructure.Interception; 2 using ContosoUniversity.Logging; 3 using System.Diagnostics; 4 5 namespace ContosoUniversity.DAL 6 { 7 public class SchoolInterceptorLogging:DbCommandInterceptor 8 { 9 private ILogger logger = new Logger(); 10 private readonly Stopwatch stopwatch = new Stopwatch(); 11 public override void ScalarExecuting(System.Data.Common.DbCommand command, DbCommandInterceptionContext<object> interceptionContext) 12 { 13 base.ScalarExecuting(command, interceptionContext); 14 stopwatch.Restart(); 15 } 16 public override void ScalarExecuted(System.Data.Common.DbCommand command, DbCommandInterceptionContext<object> interceptionContext) 17 { 18 stopwatch.Stop(); 19 if (interceptionContext.Exception !=null) 20 { 21 logger.Error(interceptionContext.Exception, "执行命令时发生错误:{0}", command.CommandText); 22 } 23 else 24 { 25 logger.TraceApi("SQL Database", "SchoolInterceptor.ScalarExecuted", stopwatch.Elapsed, "Command:{0}", command.CommandText); 26 } 27 base.ScalarExecuted(command,interceptionContext); 28 } 29 public override void NonQueryExecuting(System.Data.Common.DbCommand command, DbCommandInterceptionContext<int> interceptionContext) 30 { 31 base.NonQueryExecuting(command, interceptionContext); 32 stopwatch.Restart(); 33 } 34 public override void NonQueryExecuted(System.Data.Common.DbCommand command, DbCommandInterceptionContext<int> interceptionContext) 35 { 36 stopwatch.Stop(); 37 if (interceptionContext.Exception!=null) 38 { 39 logger.Error(interceptionContext.Exception, "执行命令时发生错误:{0}", command.CommandText); 40 } 41 else 42 { 43 logger.TraceApi("SQL Database", "SchoolInterceptor.NonQueryExecuted",stopwatch.Elapsed,"Command:{0}",command.CommandText); 44 } 45 base.NonQueryExecuted(command, interceptionContext); 46 } 47 public override void ReaderExecuting(System.Data.Common.DbCommand command, DbCommandInterceptionContext<System.Data.Common.DbDataReader> interceptionContext) 48 { 49 base.ReaderExecuting(command, interceptionContext); 50 stopwatch.Restart(); 51 } 52 public override void ReaderExecuted(System.Data.Common.DbCommand command, DbCommandInterceptionContext<System.Data.Common.DbDataReader> interceptionContext) 53 { 54 stopwatch.Stop(); 55 if (interceptionContext.Exception!=null) 56 { 57 logger.Error(interceptionContext.Exception,"执行命令时发生错误:{0}",command.CommandText); 58 } 59 else 60 { 61 logger.TraceApi("SQL Database", "SchoolInterceptor.ReaderExecuted", stopwatch.Elapsed, "Command:{0}", command.CommandText); 62 } 63 base.ReaderExecuted(command, interceptionContext); 64 } 65 } 66 }
对于成功的查询或者命令。这个类会输出一个包含延迟信息的信息日志。对于异常,它就会创建一个错误日志。
2.为了在页面的搜索框中输入Throw时,让拦截类生成一个虚拟的暂态误差异常。我们在DAL文件夹中创建类文件:SchoolInterceptorTransientErrors.cs
1 using System; 2 using System.Data.Entity.Infrastructure.Interception; 3 using System.Linq; 4 using ContosoUniversity.Logging; 5 using System.Data.SqlClient; 6 using System.Reflection; 7 8 namespace ContosoUniversity.DAL 9 { 10 public class SchoolInterceptorTransientErrors:DbCommandInterceptor 11 { 12 private int counter = 0; 13 private ILogger logger = new Logger(); 14 public override void ReaderExecuting(System.Data.Common.DbCommand command, DbCommandInterceptionContext<System.Data.Common.DbDataReader> interceptionContext) 15 { 16 bool throwTransientError = false; 17 if (command.Parameters.Count>0 && command.Parameters[0].Value.ToString()=="%Throw%") 18 { 19 throwTransientError = true; 20 command.Parameters[0].Value = "%an%"; 21 command.Parameters[1].Value = "%an%"; 22 } 23 if (throwTransientError && counter<4) 24 { 25 logger.Information("返回暂态错误命令:{0}",command.CommandText); 26 counter++; 27 interceptionContext.Exception = CreateDummySqlException(); 28 } 29 } 30 private SqlException CreateDummySqlException() 31 { 32 var sqlErrorNumber = 20; 33 var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single(); 34 var sqlError = sqlErrorCtor.Invoke(new object[]{sqlErrorNumber,(byte)0,(byte)0,"","","",1}); 35 36 var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection),true); 37 var addMethod = typeof(SqlErrorCollection).GetMethod("Add",BindingFlags.Instance | BindingFlags.NonPublic); 38 addMethod.Invoke(errorCollection,new[]{sqlError}); 39 40 var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c=>c.GetParameters().Count()==4).Single(); 41 var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy",errorCollection,null,Guid.NewGuid() }); 42 return sqlException; 43 } 44 } 45 }
上述代码重写了 ReaderExecuting
方法,查询的时候调用这个方法会返回数据中的多行。如果你想对其他类型的查询测试弹性连接,也可以重写NonQueryExecuting
和ScalarExecuting方法。
当运行Student页面在输入框中输入Throw时,代码会创建一个虚拟的暂态误差,错误编号为20.编号20通常被定义为暂时性的。其他目前被定义为暂时性的错误编码有:64, 233, 10053, 10054, 10060, 10928, 10929, 40197, 40501, abd 40613。但这些是新版本的SQL 数据库的变化。
代码对EF返回一个异常而不是执行查询,返回查询结果。这个暂态异常会被返回四次,之后代码将会正常的处理数据查询。
因为每一次都被记录下来,所以在成功之前将会看到4次EF执行查询,唯一看到的不同就是查询的时候页面会加载变慢。
EF重试查询的次数是可配置的。代码定义四次。是因为SQL数据库执行策略默认为4次。如果要修改执行策略,那么代码中就要修改定义生成都少次暂态误差。这样抛出的异常信息也要做修改。
在搜索框中输入的值将会存贮在 command.Parameters[0]
和
command.Parameters[1]
中(一个存贮姓,一个存储名)。当程序发现%Throw%时,就会用“an”替代%Throw%。所以就会返回带an的学生信息。
这只是在程序UI中输入内容测试弹性连接。你可以对于所有的查询或者修改生成暂态误差。正如后面解释到的DbInterception.Add方法
3.在Global.asax文件中添加如下声明:
using ContosoUniversity.DAL; using System.Data.Entity.Infrastructure.Interception;
2.修改 Application_Star
方法:
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()); }
当EF发送sql语句到数据库的时候上述代码就会调用拦截器。注意,我们将模拟暂态误差和日志写在了不同的类中。你可以启用或者禁用它们。
你可以在你代码的任何地方通过 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方法。否则将得到多个拦截器实例。
拦截器按照登记顺序执行( DbInterception.Add
方法被调用的顺序)。顺序可能取决于你拦截器中的内容。例如。一个拦截器可能修改它从CommandText属性中获取到的SQL命令,若它确实修改了SQL命令,下一个拦截器就会使用修改后的sql命令而不是之前的sql命令。
通过在页面输入不同值产生暂态错误。除此之外,你可以使拦截器一直说暂态序列异常而不用检查特定的参数值。你可以在想生成暂态错误的时候添加拦截器,但是要在数据库初始化之后再使用拦截器。换言之,在开始生成暂态错误之前至少要对你所操纵的实体集做一次数据库操作,例如查询操作。在数据库初始化完成过程中EF会执行一些列的查询,这些查询并不是在事务中执行。因此初始化中产生的错误会让数据库上下文状态不一致。
测试日志和弹性连接
1.F5运行项目,单击Student菜单。
2.在vs中打开输出窗口,查看跟踪的输出。
在输出窗口中可以看到发送到数据库的sql语句。
3.在Student页面中,输入Throw搜索。
这时你将会看到浏览器页面会查询几秒,直到EF返回数据到页面。查看vs的输出窗口,会看到相同的查询尝试了5次。前四次返回的是暂态误差异常。
以下是我本地的输出窗口返回的异常信息:
下面是查询成功的信息:
4.为了查看不同执行策略的结果,注释掉SchoolConfiguration.cs中的 SetExecutionStrategy
行。再次在运行项目,在搜索框中输入Throw。会看到如下异常
5.取消SchoolConfiguration.cs中对SetExecutionStrategy的注释。