共用方式為


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. Logic 資料夾中,開啟 AddProducts.cs 檔案。

  2. System.Data.Entity.Infrastructure 新增 using 陳述式,如黃色醒目提示所示:

    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 連線恢復/重試邏輯

命令攔截

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

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

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

建立記錄介面和類別

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

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

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

  2. Logging 資料夾中,建立一個名為 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);
    
        }
    }
    

    介面提供三個追蹤層級來指出記錄的相對重要性,以及一個用來提供外部服務呼叫的延遲資訊,例如資料庫查詢。 Logging 方法具有多載,允許您傳入例外狀況。 如此一來,包含堆疊追蹤和內部例外狀況的例外狀況資訊就會由實作介面的類別可靠地記錄,而不是依賴在整個應用程式的每個記錄方法呼叫中完成的例外狀況。

    TraceApi 方法可讓您追蹤每次呼叫外部服務的延遲,例如 SQL 資料庫。

  3. Logging 資料夾中,建立一個名為 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->輸出找到輸出視窗。 您可能必須捲動記錄器所寫入的數個其他記錄。

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

  7. 若要查看執行策略 (重試原則) 產生的差異,請標記註解 Logic 資料夾中 WingtipToysConfiguration.cs 檔案中的 SetExecutionStrategy 行,再次在偵錯模式下執行管理員頁面,然後再次新增名為「Throw」的產品。

    這一次偵錯工具會在第一次嘗試執行查詢時,立即停止第一次產生的例外狀況。
    偵錯 - 檢視詳細資料

  8. 取消註解 WingtipToysConfiguration.cs 檔案中的 SetExecutionStrategy 行。

摘要

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

後續步驟

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