這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這裏是第四篇:MVC程序中實體框架的鏈接恢復和命令攔截程序員
譯文版權全部,謝絕全文轉載——但您能夠在您的網站上添加到該教程的連接。sql
到目前爲止,應用程序已經能夠在您本地機器上正常地運行。但若是您想將它發佈在互聯網上以便更多的人來使用,您須要將程序部署到WEB服務器並將數據庫部署到數據庫服務器。數據庫
在本教程中,您將學習在將實體框架部署到雲環境時很是有價值的兩個特色:鏈接回覆(瞬時錯誤的自動重試)和命令攔截(捕捉全部發送到數據庫的SQL查詢語句,以便將它們記錄在日誌中或更改它們)。編程
注意:本節教程是可選的。若是您跳過本節,咱們會在後續的教程中作一些細微的調整。windows
當您將應用程序部署到Windows Azure時,您會將數據庫部署到Windows Azure SQL數據庫中——一個雲數據庫服務。和您將Web服務器和數據庫直接鏈接在同一個數據中心相比,鏈接一個雲數據庫服務更容易遇到瞬時鏈接錯誤。即便雲Web服務器和雲數據庫服務器在同一數據中心機房中,它們之間在出現大量數據鏈接時也很容易出現各類問題,好比負載均衡。瀏覽器
另外,雲服務器一般是由其餘用戶共享的,這意味着可能會受到其它用戶的影響。您對數據庫的存取權限可能受到限制,當您嘗試頻繁的訪問數據庫服務器時也可能遇到基於SLA的帶寬限制。大多數鏈接問題都是在您鏈接到雲服務器時瞬時發生的,它們會嘗試在短期內自動解決問題。因此當您嘗試鏈接數據庫並遇到一個錯誤,該錯誤極可能是瞬時的,當您重複嘗試後可能該錯誤就再也不存在。您可使用自動瞬時錯誤重試來提升您的客戶體驗。實體框架6中的鏈接恢復能自動對錯誤的SQL查詢進行重試。服務器
鏈接恢復功能只能針對特定的數據庫服務進行正確的配置後纔可用:網絡
您能夠爲任何實體框架提供程序支持的數據庫環境來手動配置這些設定,但實體框架已經爲使用Windows Azure SQL數據庫的在線應用程序作了缺省配置。接下來咱們將在Contoso大學中實施這些配置。mvc
若是要啓用鏈接恢復,您須要在您的程序集中建立一個從DbConfiguration派生的類,該類將用來配置SQL數據庫執行策略,其中包含鏈接恢復的重試策略。
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。
using System.Data.Entity.Infrastructure;
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 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)的每次調用的延遲時間。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 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 }
對於成功查詢的命令,這段代碼將相關信息及延時信息寫入日誌中,對於異常,它將建立錯誤日誌。
在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來對鏈接恢復進行測試的方法。您也能夠針對更新來編寫代碼生成瞬時錯誤。
using ContosoUniversity.DAL; using System.Data.Entity.Infrastructure.Interception;
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屬性,而下一個攔截器獲取到的會是更改過的該屬性。
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中的錯誤計數器。public SchoolConfiguration() { //SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy()); }
這一次在嘗試第一次查詢時,調試器會當即中止並彈出異常。
在本節中你看到了如何啓用實體框架的鏈接恢復,記錄發送到數據庫的SQL查詢命令,在下一節中你會使用Code First Migrations來將其部署該應用程序到互聯網中。
Tom Dykstra - Tom Dykstra是微軟Web平臺及工具團隊的高級程序員,做家。