教學課程:在 ASP.NET MVC 應用程式中使用實體框架的連接彈性和命令攔截
到目前為止,該應用程式已在您的開發電腦上的 IIS Express 中本地運行。 要使真正的應用程式可供其他人透過 Internet 使用,您必須將其部署到 Web 託管供應商,並且必須將資料庫部署到資料庫伺服器。
在本教學課程中,您將學習如何使用連接彈性和命令攔截。 它們是 Entity Framework 6 的兩個重要功能,在部署到雲端環境時特別有價值:連接彈性 (針對暫時性錯誤自動重試) 和命令攔截 (捕獲發送到資料庫的所有 SQL 查詢以便記錄或更改它們)。
此連接彈性和命令攔截教學課程是可選的。 如果您跳過本教學課程,則後續教學課程中將需要進行一些細微的調整。
在本教學課程中,您已:
- 啟用連線彈性
- 啟用指令攔截
- 測試新設定
必要條件
啟用連線彈性
當您將應用程式部署到 Windows Azure 時,您會將資料庫部署到 Windows Azure SQL 資料庫 (一種雲端資料庫服務)。 當您連接到雲端資料庫服務時,瞬時連接錯誤通常比當您的 Web 伺服器和資料庫伺服器在同一資料中心直接連接在一起時更常見。 即使雲端 Web 伺服器和雲端資料庫服務託管在同一資料中心,它們之間的更多網路連線也可能會出現問題,例如負載平衡器。
此外,雲端服務通常由其他用戶共享,這意味著其回應能力可能會受到其他用戶的影響。 您對資料庫的存取可能會受到限制。 限制意味著當您嘗試比服務等級協定 (SLA) 允許的頻率更頻繁地存取資料庫服務時,資料庫服務會引發例外狀況。
當您存取雲端服務時,許多或大多數連線問題都是暫時的,也就是說,它們會在短時間內自行解決。 因此,當您嘗試資料庫操作並遇到通常是暫時性的錯誤時,您可以在短暫等待後再次嘗試該操作,並且該操作可能會成功。 如果您透過自動重試來處理暫時性錯誤,從而使大多數錯誤對客戶不可見,則可以為使用者提供更好的體驗。 Entity Framework 6 中的連線彈性功能可自動執行重試失敗的 SQL 查詢的過程。
必須針對特定資料庫服務適當設定連線彈性功能:
- 它必須知道哪些例外狀況可能是暫時的。 例如,您想要重試網路連線暫時遺失所引起的錯誤,而不是由程式錯誤引起的錯誤。
- 它必須在重試失敗操作之間等待適當的時間。 與使用者等待回應的線上網頁相比,批次重試之間的等待時間可能更長。
- 它必須在放棄之前重試適當的次數。 您可能希望在批次過程中比在線上應用程式中重試更多次。
您可以為實體框架提供者支援的任何資料庫環境手動設定這些設置,但通常已經為您設定了通常適用於使用 Windows Azure SQL 資料庫的線上應用程式的預設值,並且這些是您將實施的設定用於Contoso 大學申請。
要啟用連接彈性,您所要做的就是在程式集中建立一個衍生自 DbConfiguration 類別的類別,並在該類別中設定 SQL 資料庫執行策略,這在 EF 中是重試原則的另一個術語。
在 DAL 資料夾中,新增一個名為 SchoolConfiguration.cs 的類別檔案。
使用下列程式碼取代範本程式碼:
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 基於程式碼的設定。在 StudentController.cs 中,新增一
using
語句 forSystem.Data.Entity.Infrastructure
。using System.Data.Entity.Infrastructure;
更改所有捕獲
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
例外狀況中。
有關更多信息,請參閱實體框架連接彈性/重試邏輯。
啟用指令攔截
既然您已經打開了重試原則,那麼如何測試以驗證它是否按預期工作? 強制發生瞬態錯誤並不容易,尤其是當您在本地運行時,並且將實際的瞬態錯誤整合到自動化單元測試中尤其困難。 若要測試連線彈性功能,您需要一種方法來攔截實體框架傳送至 SQL Server 的查詢,並將 SQL Server 回應取代為通常是瞬態的例外類型。
您也可以使用查詢攔截來實現雲端應用程式的最佳實踐: 記錄對外部服務 (例如資料庫服務) 的所有呼叫的延遲和成功或失敗。 EF6 提供了專用的日誌記錄 API,可以更輕鬆地進行日誌記錄,但在本教學課程的這一部分中,您將學習如何直接使用實體框架的攔截功能,用於日誌記錄和模擬瞬態錯誤。
建立日誌記錄介面和類
日誌記錄的最佳做法是使用介面來進行記錄,而不是對 System.Diagnostics.Trace 或日誌記錄類別進行硬編碼呼叫。 如果您以後需要的話,這可以讓您更輕鬆地更改日誌記錄機制。 因此,在本節中,您將建立日誌記錄介面和一個類別來實現它。/p>
在專案中建立一個資料夾並將其命名為 Logging。
在 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 資料庫) 的延遲。
在 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
類別衍生。 您可以在其中編寫方法重寫,當查詢即將執行時會自動呼叫這些方法。 在這些方法中,您可以檢查或記錄傳送到資料庫的查詢,並且可以在將查詢傳送到資料庫之前變更查詢,或自行將某些內容傳回實體框架,甚至無需將查詢傳遞到資料庫。
若要建立記錄傳送至資料庫的每個 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); } } }
對於成功的查詢或命令,此程式碼會寫入包含延遲資訊的資訊日誌。 對於例外狀況,它會建立錯誤日誌。
若要建立當您在搜尋框中輸入「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
方法,該方法是為可傳回多行資料的查詢呼叫的。 如果您想檢查其他類型查詢的連接彈性,您也可以重寫NonQueryExecuting
和ScalarExecuting
方法,就像日誌記錄攔截器所做的那樣。當您執行「Student」頁面並輸入「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 方法的註解中所述。
在 Global.asax 中加入以下
using
語句:using ContosoUniversity.DAL; using System.Data.Entity.Infrastructure.Interception;
將突出顯示的行新增至
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 中輸入不同的值來引發瞬態錯誤。 作為替代方案,您可以編寫攔截器程式碼以始終產生瞬態例外狀況序列,而不檢查特定參數值。 然後,只有當您想要產生暫時性錯誤時才可以新增攔截器。 但是,如果您這樣做,請在資料庫初始化完成之前新增攔截器。 換句話說,在開始產生暫時性錯誤之前,至少執行一項資料庫操作,例如對實體集之一進行查詢。 實體框架在資料庫初始化期間執行多個查詢,並且它們不是在交易中執行,因此初始化期間的錯誤可能會導致上下文進入不一致的狀態。
測試新設定
按 F5 以偵錯模式執行應用程序,然後按一下學生標籤。
查看 Visual Studio 輸出視窗以查看追蹤輸出。 您可能需要向上捲動瀏覽一些 JavaScript 錯誤才能看到記錄器寫入的日誌。
請注意,您可以看到傳送到資料庫的實際 SQL 查詢。 您將看到實體框架啟動時執行的一些初始查詢和命令,檢查資料庫版本和遷移歷史記錄表 (您將在下一個教學課程中了解遷移)。 您會看到一個分頁查詢,以找出有多少學生,最後您會看到獲取學生資料的查詢。
在學生頁面中,輸入「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:
您沒有記錄參數的值,但您可以這樣做。 如果您想查看參數值,您可以編寫日誌程式碼以從攔截器方法中取得的
DbCommand
物件的Parameters
屬性中取得參數值。請注意,除非停止並重新啟動應用程序,否則無法重複此測試。 如果您希望能夠在應用程式的單次運行中多次測試連接彈性,您可以編寫程式碼來重置
SchoolInterceptorTransientErrors
。若要查看執行策略 (重試原則) 產生的差異,請
SetExecutionStrategy
註解掉 SchoolConfiguration.cs 中的行,再次在偵錯模式下執行 Students 頁面,然後再次搜尋「Throw」。這次,當偵錯器第一次嘗試執行查詢時,它會立即停止在第一個產生的例外狀況上。
取消 SchoolConfiguration.cs 中 SetExecutionStrategy 行的註解。
取得程式碼
其他資源
可以在 ASP.NET 資料存取 - 推薦資源中找到其他實體框架資源的連結。
下一步
在本教學課程中,您已:
- 啟用連線彈性
- 啟用指令攔截
- 測試了新設定
請繼續閱讀下一篇文章,以了解 Code First 遷移和 Azure 部署。