教學課程:在 ASP.NET MVC 應用程式中搭配 Entity Framework 使用連線復原和命令攔截
到目前為止,應用程式已在開發電腦上的本機IIS Express中執行。 若要讓其他人能夠透過網際網路使用實際應用程式,您必須將其部署至 Web 主控提供者,而且您必須將資料庫部署至資料庫伺服器。
在本教學課程中,您將瞭解如何使用連線復原和命令攔截。 這是 Entity Framework 6 的兩個重要功能,當您部署至雲端環境時特別重要:連線復原 (暫時性錯誤的自動重試) 和命令攔截 (攔截傳送至資料庫的所有 SQL 查詢,以便記錄或變更它們) 。
此連線復原和命令攔截教學課程是選擇性的。 如果您略過本教學課程,後續教學課程中必須進行一些次要調整。
在本教學課程中,您:
- 啟用連線復原
- 啟用命令攔截
- 測試新設定
必要條件
啟用連線復原
當您將應用程式部署至 Windows Azure 時,您會將資料庫部署至 Windows Azure SQL Database,這是雲端資料庫服務。 當您連線到雲端資料庫服務時,暫時性連線錯誤通常比當您的 Web 服務器和資料庫伺服器直接連線在相同的資料中心時更頻繁。 即使雲端 Web 服務器和雲端資料庫服務裝載于相同的資料中心,它們之間仍有更多網路連線可能會有問題,例如負載平衡器。
此外,雲端服務通常會由其他使用者共用,這表示其回應能力可能會受到其影響。 而且您對資料庫的存取可能會受到節流限制。 節流表示當您嘗試存取資料庫服務的頻率比服務等級協定 (SLA) 允許的頻率更高時,資料庫服務會擲回例外狀況。
當您存取雲端服務時,許多或大部分的連線問題都是暫時性的,也就是說,它們會在短時間內自行解決。 因此,當您嘗試資料庫作業並取得通常是暫時性的錯誤類型時,您可以在短暫等候後再次嘗試作業,而且作業可能會成功。 如果您藉由自動再次嘗試處理暫時性錯誤,讓大部分使用者都能看到暫時性錯誤,您可以為使用者提供更好的體驗。 Entity Framework 6 中的連線復原功能會將重試失敗 SQL 查詢的程式自動化。
必須針對特定資料庫服務適當地設定連線復原功能:
- 它必須知道哪些例外狀況可能是暫時性的。 您想要重試因網路連線暫時遺失所造成的錯誤,而不是程式錯誤所造成的錯誤,例如。
- 它必須等候失敗作業重試之間的適當時間量。 您可以在重試批次程式之間等候的時間比使用者正在等候回應的線上網頁還長。
- 它必須重試適當的次數,才能放棄。 您可能會想要在線上應用程式中的批次處理中重試更多次。
您可以針對 Entity Framework 提供者所支援的任何資料庫環境手動設定這些設定,但對於使用 Windows Azure SQL Database 的線上應用程式而言,這些預設值通常很適合您設定,而這些是您將針對 Contoso University 應用程式實作的設定。
您只需要啟用連線復原功能,就會在衍生自DbConfiguration類別的元件中建立類別,並在該類別中設定SQL Database執行策略,而 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()); } } }
Entity Framework 會自動執行它在衍生自
DbConfiguration
的類別中找到的程式碼。 您可以使用DbConfiguration
類別,在程式碼中執行設定工作,否則會在 Web.config 檔案中執行。 如需詳細資訊,請參閱 EntityFramework Code-Based Configuration。在 StudentController.cs 中,新增
using
的System.Data.Entity.Infrastructure
語句。using System.Data.Entity.Infrastructure;
變更攔截例外狀況的所有
catch
區塊DataException
,使其改為攔截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 連線復原/重試邏輯。
啟用命令攔截
現在您已開啟重試原則,接下來該如何測試以確認它是否如預期般運作? 強制發生暫時性錯誤並不容易,特別是當您在本機執行時,而且將實際暫時性錯誤整合到自動化單元測試中會特別困難。 若要測試連線復原功能,您需要一種方式來攔截 Entity Framework 傳送給SQL Server的查詢,並將SQL Server回應取代為通常是暫時性的例外狀況類型。
您也可以使用查詢攔截來實作雲端應用程式的最佳做法: 記錄所有外部服務的延遲和成功或失敗 ,例如資料庫服務。 EF6 提供 專用的記錄 API ,可讓您更輕鬆地進行記錄,但在本教學課程的本節中,您將瞭解如何直接使用 Entity Framework 的 攔截功能 來記錄和模擬暫時性錯誤。
建立記錄介面和類別
記錄的最佳做法是使用介面,而不是硬式編碼對 System.Diagnostics.Trace 或記錄類別進行編碼。 這可讓您在稍後需要執行此動作時,更輕鬆地變更記錄機制。 因此,在本節中,您將建立記錄介面和類別來實作它。/p>
在專案中建立資料夾,並將其命名為 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 Database。
在 [記錄 ] 資料夾中,建立名為 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 介面可讓您在決定這麼做時,切換至不同的追蹤機制相當容易。
建立攔截器類別
接下來,您將建立 Entity Framework 每次將查詢傳送至資料庫時都會呼叫的類別,一個用來模擬暫時性錯誤,另一個用來執行記錄。 這些攔截器類別必須衍生自 DbCommandInterceptor
類別。 在它們中,您會撰寫在即將執行查詢時自動呼叫的方法覆寫。 在這些方法中,您可以檢查或記錄傳送至資料庫的查詢,而且您可以在查詢傳送至資料庫之前變更查詢,或自行將查詢傳回 Entity Framework,而不需要將查詢傳遞至資料庫。
若要建立攔截器類別,以記錄傳送至資料庫的每個 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 Database例外狀況,這是通常為暫時性的類型。 目前辨識為暫時性的其他錯誤號碼為 64、233、10053、10054、10060、10928、10929、40197、40501 和 40613,但這些在新版本的 SQL Database中可能會變更。
程式碼會將例外狀況傳回 Entity Framework,而不是執行查詢並傳回查詢結果。 暫時性例外狀況會傳回四次,然後程式碼會還原為將查詢傳遞至資料庫的一般程式。
由於所有專案都會記錄,因此您將能夠看到 Entity Framework 會在最後成功之前嘗試執行查詢四次,而應用程式的唯一差異在於,轉譯含有查詢結果的頁面需要較長的時間。
Entity Framework 將重試的可設定次數;程式碼會指定四次,因為這是SQL Database執行原則的預設值。 如果您變更執行原則,您也會變更此處的程式碼,指定產生暫時性錯誤次數。 您也可以變更程式碼以產生更多例外狀況,讓 Entity Framework 擲回
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()); }
當 Entity Framework 將查詢傳送至資料庫時,這幾行程式碼會導致您的攔截器程式碼執行。 請注意,因為您已為暫時性錯誤模擬和記錄建立個別的攔截器類別,因此您可以獨立啟用和停用它們。
您可以在程式碼中的任何位置使用
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 中輸入不同的值來造成暫時性錯誤。 或者,您可以撰寫攔截器程式碼,一律產生暫時性例外狀況序列,而不檢查特定參數值。 然後,只有在您想要產生暫時性錯誤時,才能新增攔截器。 不過,如果您這麼做,在資料庫初始化完成之前,請勿新增攔截器。 換句話說,在您開始產生暫時性錯誤之前,請先執行至少一個資料庫作業,例如其中一個實體集上的查詢。 Entity Framework 會在資料庫初始化期間執行數個查詢,而且它們不會在交易中執行,因此在初始化期間發生錯誤可能會導致內容進入不一致的狀態。
測試新設定
按 F5 以偵錯模式執行應用程式,然後按一下 [ 學生] 索引標籤。
查看 Visual Studio [輸出 ] 視窗以查看追蹤輸出。 您可能必須捲動一些 JavaScript 錯誤,才能到達記錄器所寫入的記錄。
請注意,您可以看到傳送至資料庫的實際 SQL 查詢。 您會看到 Entity Framework 開始執行的一些初始查詢和命令,並檢查資料庫版本和移轉歷程記錄資料表, (您將在下一個教學課程) 中瞭解移轉。 您會看到分頁的查詢,以瞭解有多少學生存在,最後您會看到取得學生資料的查詢。
在 [ 學生] 頁面中,輸入 「Throw」 作為搜尋字串,然後按一下 [ 搜尋]。
您會發現,當 Entity Framework 重試查詢數次時,瀏覽器似乎停止回應數秒。 第一次重試會非常快速發生,然後在每次額外重試之前先等候。 此程式在每次重試之前等候較長的時間稱為 指數輪詢。
當頁面顯示時,顯示其名稱中有 「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
錯誤計數器。若要查看執行策略 (重試原則) 的差異,請將SchoolConfiguration.cs中的行批註化
SetExecutionStrategy
、再次以偵錯模式執行 Students 頁面,然後再次搜尋 「擲回」。這次偵錯工具會在第一次嘗試執行查詢時,立即停止第一次產生的例外狀況。
取消批註SchoolConfiguration.cs中的SetExecutionStrategy行。
取得程式碼
其他資源
您可以在 ASP.NET 資料存取 - 建議的資源中找到其他 Entity Framework 資源的連結。
後續步驟
在本教學課程中,您:
- 已啟用連線恢復功能
- 已啟用命令攔截
- 測試新的組態
請前進到下一篇文章,以瞭解 Code First 移轉和 Azure 部署。
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應