[渣譯文] 使用 MVC 5 的 EF6 Code First 入門 系列:MVC程序中實體框架的鏈接恢復和命令攔截

這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這裏是第四篇:MVC程序中實體框架的鏈接恢復和命令攔截程序員

原文:Connection Resiliency and Command Interception with the Entity Framework in an ASP.NET MVC Applicationweb

譯文版權全部,謝絕全文轉載——但您能夠在您的網站上添加到該教程的連接。sql

到目前爲止,應用程序已經能夠在您本地機器上正常地運行。但若是您想將它發佈在互聯網上以便更多的人來使用,您須要將程序部署到WEB服務器並將數據庫部署到數據庫服務器。數據庫

在本教程中,您將學習在將實體框架部署到雲環境時很是有價值的兩個特色:鏈接回覆(瞬時錯誤的自動重試)和命令攔截(捕捉全部發送到數據庫的SQL查詢語句,以便將它們記錄在日誌中或更改它們)。編程

注意:本節教程是可選的。若是您跳過本節,咱們會在後續的教程中作一些細微的調整。windows

啓用鏈接恢復

當您將應用程序部署到Windows Azure時,您會將數據庫部署到Windows Azure SQL數據庫中——一個雲數據庫服務。和您將Web服務器和數據庫直接鏈接在同一個數據中心相比,鏈接一個雲數據庫服務更容易遇到瞬時鏈接錯誤。即便雲Web服務器和雲數據庫服務器在同一數據中心機房中,它們之間在出現大量數據鏈接時也很容易出現各類問題,好比負載均衡。瀏覽器

另外,雲服務器一般是由其餘用戶共享的,這意味着可能會受到其它用戶的影響。您對數據庫的存取權限可能受到限制,當您嘗試頻繁的訪問數據庫服務器時也可能遇到基於SLA的帶寬限制。大多數鏈接問題都是在您鏈接到雲服務器時瞬時發生的,它們會嘗試在短期內自動解決問題。因此當您嘗試鏈接數據庫並遇到一個錯誤,該錯誤極可能是瞬時的,當您重複嘗試後可能該錯誤就再也不存在。您可使用自動瞬時錯誤重試來提升您的客戶體驗。實體框架6中的鏈接恢復能自動對錯誤的SQL查詢進行重試。服務器

鏈接恢復功能只能針對特定的數據庫服務進行正確的配置後纔可用:網絡

  • 必須知道那些異常有多是暫時的,您想要重試因爲網絡鏈接而形成的錯誤,而不是編程Bug帶來的。
  • 在失敗操做的間隔中必須等待適當的時間。批量重試時在線用戶可能會須要等待較長時間纔可以得到響應。
  • 須要設置一個適當的重試次數。在一個在線的應用程序中,您可能會進行屢次重試。

您能夠爲任何實體框架提供程序支持的數據庫環境來手動配置這些設定,但實體框架已經爲使用Windows Azure SQL數據庫的在線應用程序作了缺省配置。接下來咱們將在Contoso大學中實施這些配置。mvc

若是要啓用鏈接恢復,您須要在您的程序集中建立一個從DbConfiguration派生的類,該類將用來配置SQL數據庫執行策略,其中包含鏈接恢復的重試策略。

  1. 在DAL文件夾中,添加一個名爲SchoolConfiguration.cs的新類。
  2. 使用如下代碼替換類中的:
     1 using System.Data.Entity;
     2 using System.Data.Entity.SqlServer;
     3 
     4 namespace ContosoUniversity.DAL
     5 {
     6     public class SchoolConfiguration : DbConfiguration
     7     {
     8         public SchoolConfiguration()
     9         {
    10             SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
    11         }
    12     }
    13 }

    實體框架會自動運行從DbConfiguration類派生的類中找到的代碼,你一樣也可使用Dbconfiguration類來在web.config中進行配置,詳細信息請參閱EntityFramework Code-Based Configuration

  3. 在學生控制器中,添加引用:
    using System.Data.Entity.Infrastructure;
  4. 更改全部捕獲DataException的異常代碼塊,使用RetryLimitExcededException:
    catch (RetryLimitExceededException)
                {
                    ModelState.AddModelError("", "保存數據時出現錯誤。請重試,若是問題依舊存在請聯繫系統管理員。");
                }

    在以前,你使用了DataException。這樣會嘗試找出可能包含瞬時錯誤的異常,而後返回給用戶一個友好的重試提示消息,但如今你已經開啓自動重試策略,屢次重試仍然失敗的錯誤將被包裝在RetryLimitExceededException異常中返回。

有關詳細信息,請參閱Entity Framework Connection Resiliency / Retry Logic

啓用命令攔截

如今你已經打開了重試策略,但你如何進行測試已驗證它是否像預期的那樣正常工做?強迫發出一個瞬時錯誤並不容易,尤爲是您正在本地運行的時候。並且瞬時錯誤也難以融入自動化的單元測試中。若是要測試鏈接恢復功能,您須要一種能夠攔截實體框架發送到SQL數據庫查詢的方法並替代SQL數據庫返回響應。

你也能夠在一個雲應用程序上按照最佳作法:log the latency and success or failure of all calls to external services來實現查詢攔截。實體框架6提供了一個dedicated logging API使它易於記錄。但在本教程中,您將學習如何直接使用實體框架的interception feature(攔截功能),包括日誌記錄和模擬瞬時錯誤。

建立一個日誌記錄接口和類

best practice for logging是經過接口而不是使用硬編碼調用System.Diagnostice.Trace或日誌記錄類。這樣可使得之後在須要時更容易地更改日誌記錄機制。因此在本節中,咱們將建立一個接口並實現它。

  1. 在項目中建立一個文件夾並命名爲Logging。
  2. 在Logging文件夾中,建立一個名爲ILogger.cs的接口類,使用下面的代碼替換自動生成的:
     1 using System;
     2 
     3 
     4 namespace ContosoUniversity.Logging
     5 {
     6     public interface ILogger
     7     {
     8         void Information(string message);
     9         void Information(string fmt, params object[] vars);
    10         void Information(Exception exception, string fmt, params object[] vars);
    11 
    12         void Warning(string message);
    13         void Warning(string fmt, params object[] vars);
    14         void Warning(Exception exception, string fmt, params object[] vars);
    15 
    16         void Error(string message);
    17         void Error(string fmt, params object[] vars);
    18         void Error(Exception exception, string fmt, params object[] vars);
    19 
    20         void TraceApi(string componentName, string method, TimeSpan timespan);
    21         void TraceApi(string componentName, string method, TimeSpan timespan, string properties);
    22         void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars);
    23 
    24 
    25     }
    26 }

    該接口提供了三個跟蹤級別用來指示日誌的相對重要性,而且設計爲能夠提供外部服務調用(例如數據庫查詢)的延遲信息。日誌方法提供了可讓你傳遞異常的重載。這樣異常信息能夠包含在棧中而且內部異常可以可靠地被該接口實現的類記錄下來,而不是依靠從應用程序的每一個日誌方法來調用並記錄。

    TraceAPI方法使您可以跟蹤到外部服務(例如SQL Server)的每次調用的延遲時間。
  3. 在Logging文件夾中,建立一個名爲Logger.cs的類,使用下面的代碼替換自動生成的:
     1 using System;
     2 using System.Diagnostics;
     3 using System.Text;
     4 
     5 namespace ContosoUniversity.Logging
     6 {
     7     public class Logger : ILogger
     8     {
     9 
    10         public void Information(string message)
    11         {
    12             Trace.TraceInformation(message);
    13         }
    14 
    15         public void Information(string fmt, params object[] vars)
    16         {
    17             Trace.TraceInformation(fmt, vars);
    18         }
    19 
    20         public void Information(Exception exception, string fmt, params object[] vars)
    21         {
    22             Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars));
    23         }
    24 
    25         public void Warning(string message)
    26         {
    27             Trace.TraceWarning(message);
    28         }
    29 
    30         public void Warning(string fmt, params object[] vars)
    31         {
    32             Trace.TraceWarning(fmt, vars);
    33         }
    34 
    35         public void Warning(Exception exception, string fmt, params object[] vars)
    36         {
    37             throw new NotImplementedException();
    38         }
    39 
    40         public void Error(string message)
    41         {
    42             Trace.TraceError(message);
    43         }
    44 
    45         public void Error(string fmt, params object[] vars)
    46         {
    47             Trace.TraceError(fmt, vars);
    48         }
    49 
    50         public void Error(Exception exception, string fmt, params object[] vars)
    51         {
    52             Trace.TraceError(FormatExceptionMessage(exception, fmt, vars));
    53         }
    54 
    55 
    56 
    57         public void TraceApi(string componentName, string method, TimeSpan timespan)
    58         {
    59             TraceApi(componentName, method, timespan, "");
    60         }
    61 
    62         public void TraceApi(string componentName, string method, TimeSpan timespan, string properties)
    63         {
    64             string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties);
    65             Trace.TraceInformation(message);
    66         }
    67 
    68         public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars)
    69         {
    70             TraceApi(componentName, method, timespan, string.Format(fmt, vars));
    71         }
    72         private string FormatExceptionMessage(Exception exception, string fmt, object[] vars)
    73         {
    74             var sb = new StringBuilder();
    75             sb.Append(string.Format(fmt, vars));
    76             sb.Append(" Exception: ");
    77             sb.Append(exception.ToString());
    78             return sb.ToString();
    79         }
    80     }
    81 }

    咱們使用了System.Diagnostics來進行跟蹤。這是.Net的使它易於生成並使用跟蹤信息的一個內置功能。你可使用System.Diagnostics的多種偵聽器來進行跟蹤並寫入日誌文件。例如,將它們存入blob storage或存儲在Windows Azure。在 Troubleshooting Windows Azure Web Sites in Visual Studio中你能夠找到更多選項及相關信息。在本教程中您將只在VS輸出窗口看到日誌。

    在生產環境中您可能想要使用跟蹤包而非System.Diagnostics,而且而當你須要時,ILogger接口可以使它相對容易地切換到不一樣的跟蹤機制下。

建立攔截器類

接下來您將建立幾個類,這些類在實體框架在每次查詢數據庫時都會被調用。其中一個模擬瞬時錯誤而另外一個進行日誌記錄。這些攔截器類必須從DbCommandInterceptor類派生。你須要重寫方法使得查詢執行時會自動調用。在這些方法中您能夠檢查或記錄被髮往數據庫中的查詢,而且能夠再查詢發送到數據庫以前對它們進行修改,甚至不將它們發送到數據庫進行查詢而直接返回結果給實體框架。

  1. 在DAL文件夾中建立一個名爲SchoolInterceptorLogging.cs的類,並使用下面的代碼替換自動生成的:
     1 using System;
     2 using System.Data.Common;
     3 using System.Data.Entity;
     4 using System.Data.Entity.Infrastructure.Interception;
     5 using System.Data.Entity.SqlServer;
     6 using System.Data.SqlClient;
     7 using System.Diagnostics;
     8 using System.Reflection;
     9 using System.Linq;
    10 using ContosoUniversity.Logging;
    11 
    12 namespace ContosoUniversity.DAL
    13 {
    14     public class SchoolInterceptorLogging : DbCommandInterceptor
    15     {
    16         private ILogger _logger = new Logger();
    17         private readonly Stopwatch _stopwatch = new Stopwatch();
    18 
    19         public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    20         {
    21             base.ScalarExecuting(command, interceptionContext);
    22             _stopwatch.Restart();
    23         }
    24 
    25         public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    26         {
    27             _stopwatch.Stop();
    28             if (interceptionContext.Exception != null)
    29             {
    30                 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
    31             }
    32             else
    33             {
    34                 _logger.TraceApi("SQL Database", "SchoolInterceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
    35             }
    36             base.ScalarExecuted(command, interceptionContext);
    37         }
    38         public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    39         {
    40             base.NonQueryExecuting(command, interceptionContext);
    41             _stopwatch.Restart();
    42         }
    43 
    44         public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    45         {
    46             _stopwatch.Stop();
    47             if (interceptionContext.Exception != null)
    48             {
    49                 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
    50             }
    51             else
    52             {
    53                 _logger.TraceApi("SQL Database", "SchoolInterceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
    54             }
    55             base.NonQueryExecuted(command, interceptionContext);
    56         }
    57 
    58         public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    59         {
    60             base.ReaderExecuting(command, interceptionContext);
    61             _stopwatch.Restart();
    62         }
    63         public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    64         {
    65             _stopwatch.Stop();
    66             if (interceptionContext.Exception != null)
    67             {
    68                 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
    69             }
    70             else
    71             {
    72                 _logger.TraceApi("SQL Database", "SchoolInterceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
    73             }
    74             base.ReaderExecuted(command, interceptionContext);
    75         }
    76     }
    77 }

    對於成功查詢的命令,這段代碼將相關信息及延時信息寫入日誌中,對於異常,它將建立錯誤日誌。

  2. 在DAL文件夾中建立一個名爲SchoolInterceptorTransientErrors.cs的類,該類在當您輸入"Throw"到搜索框並進行查詢時生成虛擬的瞬時錯誤。使用如下代碼替換自動生成的:

     1 using System;
     2 using System.Data.Common;
     3 using System.Data.Entity;
     4 using System.Data.Entity.Infrastructure.Interception;
     5 using System.Data.Entity.SqlServer;
     6 using System.Data.SqlClient;
     7 using System.Diagnostics;
     8 using System.Reflection;
     9 using System.Linq;
    10 using ContosoUniversity.Logging;
    11 
    12 namespace ContosoUniversity.DAL
    13 {
    14     public class SchoolInterceptorTransientErrors : DbCommandInterceptor
    15     {
    16         private int _counter = 0;
    17         private ILogger _logger = new Logger();
    18 
    19         public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    20         {
    21             bool throwTransientErrors = false;
    22             if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "Throw")
    23             {
    24                 throwTransientErrors = true;
    25                 command.Parameters[0].Value = "an";
    26                 command.Parameters[1].Value = "an";
    27             }
    28 
    29             if (throwTransientErrors && _counter < 4)
    30             {
    31                 _logger.Information("Returning transient error for command: {0}", command.CommandText);
    32                 _counter++;
    33                 interceptionContext.Exception = CreateDummySqlException();
    34             }
    35         }
    36 
    37         private SqlException CreateDummySqlException()
    38         {
    39             // The instance of SQL Server you attempted to connect to does not support encryption
    40             var sqlErrorNumber = 20;
    41 
    42             var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single();
    43             var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 });
    44 
    45             var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true);
    46             var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic);
    47             addMethod.Invoke(errorCollection, new[] { sqlError });
    48 
    49             var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single();
    50             var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() });
    51 
    52             return sqlException;
    53         }
    54     }
    55 }

    這段代碼僅重寫了用來返回多行查詢結果數據的ReaderExcuting方法。若是你想要檢查其餘類型的鏈接恢復,你能夠重寫如NonQueryExecuting和ScalarExecuting方法就像在日誌攔截器中所作的那樣。
    當您運行學生頁面並輸入"Throw"做爲搜索字符串時,代碼將建立一個虛擬的SQL數據庫錯誤數20,被當作瞬時錯誤類型。目前公認的瞬時錯誤號碼有64,233,10053,10060,10928,10929,40197,40501及40613等,你能夠檢查新版的SQL 數據庫來確認這些信息。

    這段代碼返回異常給實體框架而不是運行查詢並返回查詢結果。瞬時異常將返回4次而後代碼將正常運行並將查詢結果返回。
    因爲咱們有所有的日誌記錄,你能夠看到實體框架進行了4次查詢才執行成功,而在應用程序中,惟一的區別是呈現頁面所花費的事件變長了。
    實體框架的重試次數是能夠配置的,在本代碼中咱們設定了4,由於這是SQL數據庫執行策略的缺省值。若是您更改執行策略,你一樣須要更改現有的代碼來指定生成瞬時錯誤的次數。您一樣能夠更改代碼來生成更多的異常來引起實體框架的RetryLimitExceededException異常。
    您在搜索框中輸入的值將保存在command.Parameters[0]和command.Parameters[1]中(一個用於姓而另外一個用於名)。當發現輸入值爲"Throw"時,參數被替換爲"an"從而查詢到一些學生並返回。
    這僅僅只是一種經過應用程序的UI來對鏈接恢復進行測試的方法。您也能夠針對更新來編寫代碼生成瞬時錯誤。

  3. 在Global.asax,添加下面的using語句:
    using ContosoUniversity.DAL;
    using System.Data.Entity.Infrastructure.Interception;
  4. 將高亮的行添加到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方法添加攔截器,並不必定要在Applicetion_Start中來作。另外一個選擇是將這段代碼放進以前你建立執行策略的DbConfiguration類中。
    public class SchoolConfiguration : DbConfiguration
    {
        public SchoolConfiguration()
        {
            SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            DbInterception.Add(new SchoolInterceptorTransientErrors());
            DbInterception.Add(new SchoolInterceptorLogging());
        }
    }

    無論你在何處放置這些代碼,要當心不要超過一次。對於相同的攔截器執行DbInterception.Add可能會使你獲得額外的攔截器實例。例如,若是添加兩第二天志記錄攔截器,您將看到查詢被記錄在兩個日誌中。
    攔截器是按照Add方法的註冊順序執行的。根據你所要進行的操做,順序可能很重要。例如,第一個攔截器可能會更改CommandText屬性,而下一個攔截器獲取到的會是更改過的該屬性。

    您已經編寫完成了模擬瞬時錯誤的代碼,如今能夠在用戶界面經過輸入一個不一樣的值來進行測試了。做爲替代方法,您能夠在攔截器中編寫不檢查特定參數值而直接生成瞬時錯誤的代碼,記得僅僅當你想要測試瞬時錯誤時才添加攔截器。

測試日誌記錄和鏈接恢復

  1. 按下F5在調試模式下運行該程序,而後點擊學生選項卡。
  2. 檢查VS輸出窗口,查看跟蹤輸出,您可能要向上滾動窗口內容。
    您能夠看到實際被髮送到數據庫的SQL查詢。
  3. 在學生索引頁中,輸入"Throw"進行查詢。

    你會注意到瀏覽器會掛起幾秒鐘,顯然實體框架正在進行重試查詢。第一次重試發生速度很快,而後每次重試查詢都會增長一點等待事件。

    當頁面執行完成後,檢查輸出窗口,你會看到相同的查詢嘗試了5次,前4次都返回了一個瞬時錯誤異常。對於每一個瞬時錯誤,你在日誌中看到異常的信息。

    返回學生數據的查詢是參數化的:
    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 (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstMidName])) AS int)) > 0)
            )  AS [Project1]
        )  AS [Project1]
        WHERE [Project1].[row_number] > 0
        ORDER BY [Project1].[LastName] ASC

    你沒有在日誌中記錄值的參數,固然你也能夠選擇記錄。你能夠在攔截器的方法中經過從DbCommand對象的參數屬性中獲取到屬性值。

    請注意您不能重複該測試,除非你中止整個應用程序並從新啓動它。若是你想要可以在單個應用程序的運行中進行屢次測試,您能夠編寫代碼來重置SchoolInterceptorTransientErrors中的錯誤計數器。
  4. 要查看執行策略的區別,註釋掉SchoolConfiguration.cs,而後關閉應用程序並從新啓動調試,運行學生索引頁面並輸入"Throw"進行搜索。
            public SchoolConfiguration()
            {
                //SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            }

     

    這一次在嘗試第一次查詢時,調試器會當即中止並彈出異常。
  5. 取消註釋並再試一次,瞭解之間的不一樣。

總結

在本節中你看到了如何啓用實體框架的鏈接恢復,記錄發送到數據庫的SQL查詢命令,在下一節中你會使用Code First Migrations來將其部署該應用程序到互聯網中。

做者信息

  Tom Dykstra - Tom Dykstra是微軟Web平臺及工具團隊的高級程序員,做家。

相關文章
相關標籤/搜索