共用方式為


ASP.NET Web Form 連線恢復功能與命令攔截

Erik Reitan

在本教學課程中,您將修改 Wingtip Toys 範例應用程式,以支援連線復原和命令攔截。 藉由啟用連線復原功能,Wingtip Toys 範例應用程式會在雲端環境發生暫時性錯誤時自動重試數據呼叫。 此外,藉由實作命令攔截,Wingtip Toys 範例應用程式會擷取傳送至資料庫的所有 SQL 查詢,以便記錄或變更它們。

注意

本 Web Form 教學課程是以 Tom Dykstra 的下列 MVC 教學課程為基礎:
ASP.NET MVC 應用程式中的 Entity Framework 連線復原和命令攔截

您將學到什麼:

  • 如何提供連線復原功能。
  • 如何實作命令攔截。

必要條件

開始之前,請確定您的電腦已安裝下列軟體:

連線恢復

當您考慮將應用程式部署至 Windows Azure 時,其中一個選項是將資料庫部署至雲端資料庫服務 Windows Azure SQL 資料庫。 當您連線到雲端資料庫服務時,暫時性連線錯誤通常比 Web 伺服器和資料庫伺服器直接連線在同一個數據中心時更頻繁。 即使雲端 Web 伺服器和雲端資料庫服務裝載在相同的數據中心,它們之間仍有更多的網路連線可能會有問題,例如負載平衡器。

此外,雲端服務通常會由其他用戶共用,這表示其回應能力可能會受到其影響。 而您對資料庫的存取可能會受限於節流。 節流表示當您嘗試存取它的頻率比服務等級協定 (SLA) 中允許的頻率更高時,資料庫服務會擲回例外狀況。

當您存取雲端服務時,許多或大部分的連線問題都是暫時性的,也就是說,它們會在短時間內自行解決。 因此,當您嘗試資料庫作業並取得通常是暫時性的錯誤類型時,您可以在短暫等候后再次嘗試作業,而且作業可能會成功。 如果您藉由自動再試一次處理暫時性錯誤,讓大部分錯誤對客戶看不見,您可以為使用者提供更好的體驗。 Entity Framework 6 中的連線復原功能會將重試失敗 SQL 查詢的程序自動化。

必須針對特定資料庫服務適當地設定連線恢復功能:

  1. 它必須知道哪些例外狀況可能是暫時性的。 您想要重試網路連線暫時遺失所造成的錯誤,而不是程式錯誤所造成的錯誤。例如。
  2. 它必須等候失敗作業重試之間的適當時間量。 您可以在批處理重試之間等候的時間比使用者正在等候回應的在線網頁還要長。
  3. 在放棄之前,它必須重試適當的次數。 您可能想要在線上應用程式中的批次處理中重試更多次。

您可以針對 Entity Framework 提供者所 環境支援 的任何資料庫手動設定這些設定。

您只需要在元件中建立衍生自 類別的DbConfiguration類別,並在該類別中設定 SQL 資料庫 執行策略,在 Entity Framework 中則是重試原則的另一個詞彙。

實作聯機復原

  1. 在 Visual Studio 中下載並開啟 WingtipToys 範例 Web Form 應用程式。

  2. WingtipToys 應用程式的 Logic 資料夾中,新增名為 WingtipToysConfiguration.cs 的類別檔案。

  3. 將現有的程式碼取代為下列程式碼:

    using System.Data.Entity;
    using System.Data.Entity.SqlServer;
     
    namespace WingtipToys.Logic
    {
        public class WingtipToysConfiguration : DbConfiguration
        {
            public WingtipToysConfiguration()
            {
              SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            }
        }
    }
    

Entity Framework 會自動執行它在衍生自 DbConfiguration的類別中找到的程序代碼。 您可以使用 類別DbConfiguration,在 Web.config 檔案中執行的程式代碼中執行設定工作。 如需詳細資訊,請參閱 EntityFramework 程式代碼型組態

  1. [邏輯 ] 資料夾中,開啟 AddProducts.cs 檔案。

  2. using為 新增 語句System.Data.Entity.Infrastructure,如以黃色醒目提示:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using WingtipToys.Models;
    using System.Data.Entity.Infrastructure;
    
  3. catch將區塊新增至 AddProduct 方法,RetryLimitExceededException以便將 記錄為以黃色反白顯示:

    public bool AddProduct(string ProductName, string ProductDesc, string ProductPrice, string ProductCategory, string ProductImagePath)
    {
        var myProduct = new Product();
        myProduct.ProductName = ProductName;
        myProduct.Description = ProductDesc;
        myProduct.UnitPrice = Convert.ToDouble(ProductPrice);
        myProduct.ImagePath = ProductImagePath;
        myProduct.CategoryID = Convert.ToInt32(ProductCategory);
    
        using (ProductContext _db = new ProductContext())
        {
            // Add product to DB.
            _db.Products.Add(myProduct);
            try
            {
                _db.SaveChanges();
            }
            catch (RetryLimitExceededException ex)
            {
                // Log the RetryLimitExceededException.
                WingtipToys.Logic.ExceptionUtility.LogException(ex, "Error: RetryLimitExceededException -> RemoveProductButton_Click in AdminPage.aspx.cs");
            }
        }
        // Success.
        return true;
    }
    

藉由新增 RetryLimitExceededException 例外狀況,您可以提供更好的記錄,或向用戶顯示錯誤訊息,讓他們可以選擇再次嘗試程式。 藉由擷 RetryLimitExceededException 取例外狀況,可能為暫時性的唯一錯誤已經嘗試過且失敗數次。 傳回的實際例外狀況將會包裝在例外狀況中 RetryLimitExceededException 。 此外,您也新增了一般 catch 區塊。 如需例外狀況的詳細資訊 RetryLimitExceededException ,請參閱 Entity Framework Connection Resiliency / Retry Logic

命令攔截

既然您已開啟重試原則,如何測試以確認它是否如預期般運作? 強制發生暫時性錯誤並不容易,特別是當您在本機執行時,將實際的暫時性錯誤整合到自動化單元測試中會特別困難。 若要測試連線恢復功能,您需要攔截 Entity Framework 傳送給 SQL Server 的查詢,並將 SQL Server 回應取代為通常是暫時性的例外狀況類型。

您也可以使用查詢攔截來實作雲端應用程式的最佳做法:記錄所有外部服務呼叫的延遲和成功或失敗,例如資料庫服務。

在本教學課程的本節中,您將使用 Entity Framework 的 攔截功能 來記錄和模擬暫時性錯誤。

建立記錄介面和類別

記錄的最佳做法是使用 interface 或記錄類別的硬式編碼呼叫 System.Diagnostics.Trace 來執行這項作業。 這可讓您稍後更輕鬆地變更記錄機制,如果您曾經需要這麼做。 因此,在本節中,您將建立記錄介面和類別來實作它。

根據上述程式,您已在 Visual Studio 中下載並開啟 WingtipToys 範例應用程式。

  1. WingtipToys 專案中建立資料夾,並將其命名為 Logging

  2. [記錄 ] 資料夾中,建立名為 ILogger.cs 的類別檔案,並以下列程式代碼取代預設程序代碼:

    using System;
     
    namespace WingtipToys.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 資料庫。

  3. [記錄 ] 資料夾中,建立名為 Logger.cs 的類別檔案,並以下列程式代碼取代預設程序代碼:

    using System;
    using System.Diagnostics;
    using System.Text;
     
    namespace WingtipToys.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)
        {
          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 追蹤、將記錄寫入檔案,例如,或將它們寫入 Windows Azure 中的 Blob 記憶體。 如需詳細資訊, 請參閱Visual Studio中針對Windows Azure 網站進行疑難解答中的一些選項,以及其他資源的連結。 在本教學課程中,您只會查看 Visual Studio 輸出 視窗中的記錄。

在生產應用程式中,您可能想要考慮使用 以外的 System.Diagnostics追蹤架構,而 ILogger 如果您決定這麼做,介面可讓您相對輕鬆地切換到不同的追蹤機制。

建立攔截器類別

接下來,您將建立 Entity Framework 會在每次將查詢傳送至資料庫時呼叫的類別,一個用來模擬暫時性錯誤,另一個用來執行記錄。 這些攔截器類別必須衍生自 DbCommandInterceptor 類別。 在它們中,您會撰寫方法覆寫,這些覆寫會在查詢即將執行時自動呼叫。 在這些方法中,您可以檢查或記錄要傳送至資料庫的查詢,而且您可以在查詢傳送至資料庫之前變更查詢,或自行將某些專案傳回 Entity Framework,甚至不需要將查詢傳遞至資料庫。

  1. 若要建立攔截器類別,它會在傳送至資料庫之前記錄每個SQL查詢,請在Logic資料夾中建立名為 InterceptorLogging.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 WingtipToys.Logging;
    
    namespace WingtipToys.Logic
    {
      public class InterceptorLogging : 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", "Interceptor.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", "Interceptor.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", "Interceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
          }
          base.ReaderExecuted(command, interceptionContext);
        }
      }
    }
    

    針對成功的查詢或命令,此程式代碼會撰寫具有延遲資訊的信息記錄檔。 針對例外狀況,它會建立錯誤記錄檔。

  2. 若要建立攔截器類別,當您在名為 AdminPage.aspx 頁面的 [名稱] 文本框中輸入 “Throw” 時,將會產生虛擬暫時性錯誤的攔截器類別,請在Logic資料夾中建立名為 InterceptorTransientErrors.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 WingtipToys.Logging;
     
    namespace WingtipToys.Logic
    {
      public class InterceptorTransientErrors : 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 = "TransientErrorExample";
            command.Parameters[1].Value = "TransientErrorExample";
          }
     
          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 方法,這個方法會針對可傳回多個數據列的查詢呼叫。 如果您想要檢查其他類型的查詢的連線復原能力,您也可以覆寫 NonQueryExecutingScalarExecuting 方法,就像記錄攔截器所做的一樣。

    稍後,您將以「系統管理員」身分登入,然後選取 頂端導覽列上的 [管理員 ] 連結。 然後,在 [AdminPage.aspx] 頁面上,您將新增名為 “Throw” 的產品。 程式代碼會針對錯誤號碼 20 建立虛擬 SQL 資料庫 例外狀況,這是已知通常是暫時性的類型。 目前辨識為暫時性的其他錯誤號碼為 64、233、10053、10054、10060、10928、10929、40197、40501 和 40613,但這些錯誤號碼在新版本 SQL 資料庫 可能會變更。 產品會重新命名為 「TransientErrorExample」,您可以在InterceptorTransientErrors.cs檔案的程式碼中遵循。

    程序代碼會將例外狀況傳回 Entity Framework,而不是執行查詢並傳回結果。 暫時性例外狀況會傳 回四 次,然後程式代碼會還原為將查詢傳遞至資料庫的一般程式。

    由於記錄了所有專案,因此您將可以看到 Entity Framework 在最後成功之前嘗試執行查詢四次,而應用程式的唯一差異在於轉譯具有查詢結果的頁面需要較長的時間。

    Entity Framework 會重試的次數可設定;程序代碼會指定四次,因為這是 SQL 資料庫 執行原則的預設值。 如果您變更執行原則,您也會變更此處的程式代碼,以指定產生暫時性錯誤多少次。 您也可以變更程式代碼以產生更多例外狀況,讓 Entity Framework 擲回例外狀況 RetryLimitExceededException

  3. 在 Global.asax,新增下列 using 語句:

    using System.Data.Entity.Infrastructure.Interception;
    
  4. 然後,將反白顯示的幾行新增至 Application_Start 方法:

    void Application_Start(object sender, EventArgs e)
    {
        // Code that runs on application startup
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    
        // Initialize the product database.
        Database.SetInitializer(new ProductDatabaseInitializer());
    
        // Create administrator role and user.
        RoleActions roleActions = new RoleActions();
        roleActions.createAdmin();
    
        // Add Routes.
        RegisterRoutes(RouteTable.Routes);
    
        // Logging.
        DbInterception.Add(new InterceptorTransientErrors());
        DbInterception.Add(new InterceptorLogging());
      
    }
    

當 Entity Framework 將查詢傳送至資料庫時,這幾行程式代碼會導致攔截器程式代碼執行。 請注意,因為您已為暫時性錯誤模擬和記錄建立個別攔截器類別,因此您可以獨立啟用和停用它們。

您可以在程式代碼中的任何位置使用 DbInterception.Add 方法新增攔截器;它不一定位於 方法中 Application_Start 。 如果您未在 方法中 Application_Start 新增攔截器,另一個選項是更新或新增名為 WingtipToysConfiguration.cs 的類別,並將上述程式代碼放在 類別建 WingtipToysConfiguration 構函式的結尾。

無論您在何處放置此程式碼,請小心不要針對相同的攔截器執行 DbInterception.Add 多次,否則您會取得其他攔截器實例。 例如,如果您新增記錄攔截器兩次,您會看到每個 SQL 查詢的兩個記錄。

攔截器會依註冊順序執行(呼叫 方法的順序 DbInterception.Add )。 順序可能很重要,取決於您在攔截器中執行的動作。 例如,攔截器可能會變更它在 屬性中取得的 CommandText SQL 命令。 如果它確實變更 SQL 命令,下一個攔截器會取得變更的 SQL 命令,而不是原始 SQL 命令。

您已撰寫暫時性錯誤模擬程式代碼,讓您可以在UI中輸入不同的值來造成暫時性錯誤。 或者,您可以撰寫攔截器程式代碼來一律產生暫時性例外狀況序列,而不檢查特定參數值。 然後,只有當您想要產生暫時性錯誤時,您才能新增攔截器。 不過,如果您這樣做,在資料庫初始化完成之前,請勿新增攔截器。 換句話說,在您開始產生暫時性錯誤之前,請至少執行一個資料庫作業,例如其中一個實體集上的查詢。 Entity Framework 會在資料庫初始化期間執行數個查詢,而且它們不會在交易中執行,因此初始化期間的錯誤可能會導致內容進入不一致的狀態。

測試記錄和聯機復原能力

  1. 在 Visual Studio 中,按 F5 以偵錯模式執行應用程式,然後使用 “Pa$$word” 作為密碼登入“Admin”。

  2. 從頂端的導覽列選取 [系統管理員 ]。

  3. 輸入名為 「Throw」 的新產品,其中包含適當的描述、價格和圖像檔。

  4. 按 [ 新增產品] 按鈕。
    您會發現,當 Entity Framework 重試查詢數次時,瀏覽器似乎停止響應數秒。 第一次重試會非常快速地進行,然後等候會在每個額外的重試之前增加。 此程式在每次重試之前等候的時間較長,稱為 指數輪詢

  5. 等候頁面不再嘗試載入。

  6. 停止專案並查看 Visual Studio [輸出 ] 視窗,以查看追蹤輸出。 您可以選擇 [偵錯 -> Windows ->Output] 來尋找 [輸出] 視窗。 您可能必須捲動記錄器所寫入的數個其他記錄。

    請注意,您可以看到傳送至資料庫的實際 SQL 查詢。 您會看到 Entity Framework 開始執行的一些初始查詢和命令,並檢查資料庫版本和移轉歷程記錄數據表。
    輸出視窗
    請注意,除非您停止應用程式並重新啟動它,否則您無法重複此測試。 如果您想要能夠在應用程式的單一執行中多次測試連線復原能力,您可以撰寫程式代碼來重設 中的 InterceptorTransientErrors 錯誤計數器。

  7. 若要查看執行策略 (重試原則) 所做的差異,請將 Logic 資料夾中的 WingtipToysConfiguration.cs 檔案中的行批注化SetExecutionStrategy、再次以偵錯模式執行 [管理] 頁面,然後再次新增名為 “Throw” 的產品。

    這一次調試程式會在第一次嘗試執行查詢時,立即停止第一次產生的例外狀況。
    偵錯 - 檢視詳細數據

  8. 取消SetExecutionStrategy批注WingtipToysConfiguration.cs檔案中的行。

摘要

在本教學課程中,您已瞭解如何修改 Web Form 範例應用程式,以支援連線復原和命令攔截。

後續步驟

檢閱 ASP.NET Web Form中的連線復原和命令攔截之後,請檢閱 ASP.NET 4.5 中的異步方法 ASP.NET Web Form主題。 本主題將教導您使用 Visual Studio 建置異步 ASP.NET Web Form應用程式的基本概念。