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 時,有一個選項是將資料庫部署至WindowsAzure SQL Database,這是雲端資料庫服務。 當您連線到雲端資料庫服務時,暫時性連線錯誤通常比當您的 Web 服務器和資料庫伺服器直接連線在相同的資料中心時更頻繁。 即使雲端 Web 服務器和雲端資料庫服務裝載于相同的資料中心,它們之間仍有更多網路連線可能會有問題,例如負載平衡器。

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

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

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

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

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

您只需要啟用連線復原功能,就會在衍生自 DbConfiguration 類別的元件中建立類別,並在該類別中設定SQL Database執行策略,而 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 Code-Based Configuration

  1. Logic 資料夾中,開啟 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 連線復原/重試邏輯

命令攔截

現在您已開啟重試原則,接下來該如何測試以確認它是否如預期般運作? 強制發生暫時性錯誤並不容易,特別是當您在本機執行時,而且將實際暫時性錯誤整合到自動化單元測試中會特別困難。 若要測試連線復原功能,您需要一種方式來攔截 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 Database。

  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 Database例外狀況,這是已知為暫時性的類型。 目前辨識為暫時性的其他錯誤號碼為 64、233、10053、10054、10060、10928、10929、40197、40501 和 40613,但這些在新版本的 SQL Database中可能會變更。 產品將會重新命名為 「TransientErrorExample」,您可以在 InterceptorTransientErrors.cs 檔案的程式碼中追蹤。

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

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

    Entity Framework 會重試的可設定次數;程式碼會指定四次,因為這是SQL Database執行原則的預設值。 如果您變更執行原則,您也會變更此處的程式碼,以指定產生暫時性錯誤的次數。 您也可以變更程式碼以產生更多例外狀況,讓 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」 作為密碼登入「管理員」。

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

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

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

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

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

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

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

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

  8. 取消批 SetExecutionStrategyWingtipToysConfiguration.cs 檔案中的這一行。

總結

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

後續步驟

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