關於日志
屬性日志
DbContext.Database.Log 屬性被設置為一個委托,該委托能接受帶有一個字符串參數的任何方法,最主要的是,通過設置它到 TextWriter 的 Write 方法將能應用於任何的TextWriter,通過上下文自動生成的所有SQL語句將被記錄到Writer中。
例如,如下代碼將記錄SQL在控制台上:
using (var ctx = new EntityDbContext()) { ctx.Database.Log = Console.WriteLine; }
【注意】上下文中的日志被設置到 Console.WriteLine ;則其所有SQL代碼都將會輸出在控制台上。
下面我們進行一些簡單的查詢、修改利用日志屬性來演示在控制台上進行輸出(依然利用前一篇文章所給出個三給類,如若不知其關系,請參考前一篇文章):
using (var ctx = new EntityDbContext()) { ctx.Database.Log = Console.WriteLine;
或者
ctx.Database.log = s => Console.WriteLine(s); var stu = ctx.Set<Student>().First(p => p.Name == "xpy0928"); stu.Grades.First().Fraction = 90; stu.Grades.Add(new Grade() { Fraction = 40 }); ctx.SaveChangesAsync().Wait(); }
則在控制台打印如下SQL代碼:

日志記錄內容
(1)各種SQL命令
查詢語句。例如:Linq查詢、原始SQL查詢
作為SaveChanges的一部分的插入(inserted)、修改(update)以及刪除(delete)
由延遲加載自動生成的關聯查詢
(2)參數
(3)是否正在被異步執行的命令
(4)當命令開始執行時,時間戳的顯示
(5)無論命令是否被成功完成,還是通過拋出異常而失敗或者是通過異步被取消
(6)一些顯示結果的值
(7)執行命令所需要的時間
輸出日志
依上述,將日志輸出在控制台是so easy,像這種輸出在控制台中只能作為一些小demo,所以應用性不廣,如果你要是在window form中或者是Web應用程序中的話,完全可以將日志輸出在內存、文件等中。下面演示輸出到文件中,其余不予演示。
using (var ctx = new EntityDbContext()) {
var sw = new StreamWriter(@"d:\Data.log") { AutoFlush = true };
ctx.Database.Log = s => { sw.Write(s); }; }
結果文件中輸出如下:

那么問題來了,如果我們需要將輸出的內容進行格式化,那么我們應該怎樣通過一種簡單的方式怎來做呢?
稍等,容我一一講來。
日志結果
默認的日志記錄器記錄sql語句文本,參數以及在命令發到數據庫之前帶着時間戳的“Executeing”(通過上述文本可知)。一個“Completed”(完成的)包含總的時間被記錄在執行命令的下面。
【注意】異步命令中的“Completed”是直到異步任務執行完成、失敗或者被取消才被記錄下來,當然,它因不同的命令和是否成功執行而產生不同的信息。
執行成功
成功完成輸出的是如上述文件中的 執行-- 已在 0 毫秒內完成,結果為: SqlDataReader
執行失敗
通過異常來告知失敗,輸出中包含了失敗的信息。
執行取消
異步命令中的任務被取消會通過異常來告知結果是失敗的。因為當試圖去取消時,底層的ADO.NET會這樣操作,而EF是基於ADO.NET的。
日志格式化
在Database.log屬性下我們要充分使用 DatabaseLogFormatter 對象,這個對象有效結合了 IDbCommandInterceptor 來實現,通過接受一個字符串的委托和上下文。這意味着在DatabaseLogFormatter上截取方法之前和通過EF執行命令之后被調用,這些DatabaseLogFormatter方法獲取有格式的日志並將其傳遞給一個委托代理。
通過從DatabaseLogFormatter上派生出一個類並且適當的重寫其方法就能實現有格式的日志輸出。重寫最常用的方法有以下三者:
LogCommand
在命令被執行之前重寫此方法來進行更改。通過默認的LogCommand來調用LogParameter的每個參數。當然你也可以進行重寫或者處理參數不同。
LogResult
重寫此方法來更改原有執行命令后記錄下來的結果。
LogParameter
重寫此方法來更改其格式和參數所記錄的內容。
例如,在每條命令被發送到數據庫之前,我們想僅僅通過單行來進行記錄。這就需要重寫兩個方法:
(1)重寫 LogCommand 來得到SQL單行的格式
(2)重寫 LogResult 不需要做任何動作。(當執行時讓其執行可重寫的,因為這樣在性能上可能會稍微高效一點,但是在功能還是等同的)
依據上述,下面我們來寫出代碼
public class SingleLineFormatter : DatabaseLogFormatter { public SingleLineFormatter(DbContext ctx, Action<string> action) : base(ctx, action) { } public override void LogCommand<TResult>(System.Data.Common.DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext) { Write( string.Format("DbContext '{0}' is Executing Command '{1}' '{2}'", Context.GetType().Name, command.CommandText.Replace(Environment.NewLine,""), Environment.NewLine));
base.LogCommand<TResult>(command, interceptionContext); } public override void LogResult<TResult>(System.Data.Common.DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext) { base.LogResult<TResult>(command, interceptionContext); } }
上述日志輸出會調用 Write 方法,此方法將輸出發送到配置的寫入委托。
【注意】上述代碼只是通過一種簡單的方式除去換行符,如果是在復雜的SQL語句中,上述方式可能會沒有效果。
設置DatabaseLogFormatter
一旦DatabaseLogFormatter的派生類被創建,那么你需要將其注冊到EF中,否則也無法進行格式化輸出。通過使用Code-Based Configuration,這也就是說,創建一個新的繼承自DbConfiguration的類與DbContext上下文類所在同一個程序集,然后會在創建的新類的構造函數中調用 SetDatabaseLogFormatter 方法,該方法接受的參數就是格式化類中構造函數中的兩個參數。
public class DbContextConfiguration : DbConfiguration { public DbContextConfiguration() { SetDatabaseLogFormatter( (context, action) => new SingleLineFormatter(context, action)); } }
基於上我們新創建的 SingleLineFormatter 會應用在設置使用Database.log的任何時刻,所以你會看到如下結果:

接下來我們將看 IDbCommandInterceptor 實現直接來控制命令的攔截,並通過集成使用NLog的例子而不使用Database.log。請繼續往下看
低級構造塊監聽
接口監聽
監聽代碼實際上是監聽接口的概念。這些接口來繼承自 IDbInterceptor 並且當EF有所行為時其定義的方法會被調用。其意義是在每個對象被截獲時都有一個接口。例如,當EF調用ExecuteNonQuery, ExecuteScalar, ExecuteReader等相關的方法時在IDbInterceptor上定義的方法將會被調用。同樣,當每個操作完成這些方法也會被調用,上述中的DatabaseLogFormatter類就實現了這個接口來記錄命令。
接口存在的意義
(1)為了記錄SQL命令
(2)為了支持並實現一些其他特性
處理結果
泛型 DbCommandInterceptionContext<> 類里面包含幾個屬性稱之為Result, OriginalResult, Exception, and OriginalException。這些方法會被設置為空通過在操作被執行之前調用的攔截方法。如果操作被成功執行,那么 Result and OriginalResult會被設置成操作的結果。這些值會被在操作執行完后的監聽方法所監控。同理,如果操作拋出異常,那么Exception and OriginalException將會被設置。
禁止執行
如果一個監聽者在命令快執行完之前設置了Result屬性的話,那么EF實際上是不會試圖去執行該命令,但是代替的是僅僅是使用結果集。換言之,這個監聽者會禁止命令的執行,但是EF會繼續,就好像命令已經被執行了。
發生上述禁止執行的示例可能是經過包裝的批處理命令,這個監聽者會將其儲存以便日后將用於批處理但是會裝到EF中一如往常的執行該命令。注意監聽者需要更多這來實現批處理。
執行后改變結果
如果一個監聽者在命令執行完后設置了Result屬性,那么EF將會使用改變后的結果而不是通過此操作返回實際的結果。同樣,如果一個監聽者在命令執行完后設置了Exception屬性,那么EF將拋出設置的異常,就好像此操作已經拋出了這個異常一樣。
一個監聽者也可以設置Exception屬性為空來表明沒有異常應該被拋出。這其實是非常有意義的,如果執行操作失敗但是監聽者希望EF繼續運行就好像操作成功執行了一樣。
OriginalResult 和OriginalException
在EF執行完一個操作后,如果該操作未失敗將設置Result和OriginalResult屬性,或者如果執行失敗拋出異常那么將設置Exception和OrigianlException。
在實際執行完一個操作之后, OriginalResult和OriginalException會被EF設置為只讀的。這些屬性不會被監聽者所設置。這也就意味着,任何監聽者能區分Exception(異常)和Result(結果),那些會通過一些其他的相對於操作被執行時發生的真實的異常(Exception)和結果(Result)的監聽者所設置。
注冊監聽者
一旦創建了一個實現了一個或者多個監聽接口的類需要通過 DbInterception 注冊到EF中。例如:
DbInterception.Add(new NLogCommandInterceptor());
監聽者也能夠使用基於代碼的配置機制(Code-Based DbConfiguration )被注冊在應用程序域中。
下面我們將基於以上描述通過使用 IDbCommandInterceptor ,來講述一個實例: Log To NLog (通過NLog進行日志記錄)(關於NLog請看這里:NLog Tutorial)
記錄如下日志:
(1)執行非異步的命令作為Warning(警告)
(2)執行拋出的異常作為Error(嚴重錯誤)
public class NLogCommandInterceptor : IDbCommandInterceptor { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public void NonQueryExecuting( DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { LogIfNonAsync(command, interceptionContext); } public void NonQueryExecuted( DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { LogIfError(command, interceptionContext); } public void ReaderExecuting( DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { LogIfNonAsync(command, interceptionContext); } public void ReaderExecuted( DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { LogIfError(command, interceptionContext); } public void ScalarExecuting( DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { LogIfNonAsync(command, interceptionContext); } public void ScalarExecuted( DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { LogIfError(command, interceptionContext); } private void LogIfNonAsync<TResult>( DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext) { if (!interceptionContext.IsAsync) { Logger.Warn("Non-async command used: {0}", command.CommandText); } } private void LogIfError<TResult>( DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext) { if (interceptionContext.Exception != null) { Logger.Error("Command {0} failed with exception {1}", command.CommandText, interceptionContext.Exception); } } }
結果如下:

【注意】上述NLog得安裝如下程序包

接下來在NLog.config中rules和target節點下進行簡單的配置輸出日志即可,如下:
<rules>
<logger name="ConsoleApplication1.NLogCommandInterceptor" minlevel="Info" writeTo="f"></logger>
/*name為命名空間+實現IDbCommandInterceptor接口的監控類名稱,f為寫到下面target中的name*/
</rules>
<targets>
<target name="f" xsi:type="File" fileName="c:\temp\log.txt"/>
</targets>
監聽中的Dispatch方法
除了在 DbInterception 中注冊方法外,它也提供了Dispatch方法,此方法允許代碼不是分配到監聽的通知的一部分,這是以上已經提到的機制,允許提供者讓監聽者知道在EF的控制下,一條命令正在被執行。不過對於應用程序開發者去使用Dispatch API這是比較少見的。下面了解下這個方法如何去操作。
DbInterception.Dispatch.Command.NonQueryAsync(myCommand, new DbCommandInterceptionContext());
以上代碼做了以下五件事:
(1)確保被設置到監聽上下文中的命令是否是 IsAsync 的
(2)調用所有注冊在 IDbCommandInterceptors 中的 NonQueryExecuting 方法
(3)除非如以上所說這些 NonQueryExecuting 方法之一被設置了Result屬性,否則調用 ExecuteNonQueryAsync
(4)在異步任務中設置了延續,使得注冊在 IDbCommandInterceptors 上的所有 NonQueryExecuted 方法都被調用
(5)使得任務結果中包含了可能已經被監聽集合中的方法之一改變了的正確結果
打開日志無需重新編譯(EF 6.1)
上述我們是通過手動操作來進行日志的輸出,如果你嫌麻煩,大可在配置文件(web.config或App.config)中的 EntityFramework 節點下進行相關配置來完成日志輸出工作。
(1)輸出到控制台
<interceptors> <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework"/> </interceptors>
(2)輸出到文件
<interceptors> <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework"> <parameters> <parameter value="c:\temp\log.txt"/> </parameters> </interceptor> </interceptors>
如下結果:
【注意】上述默認情況下,當應用程序每次啟動會重新生成一個新的log.text,則此前的將被覆蓋。如果該文件總是存在的話,為了追加到到日志文件中,可以進行如下操作:
<interceptors> <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework"> <parameters> <parameter value="c:\temp\log.txt"/> <parameter value="true" type="System.Boolean"/> </parameters> </interceptor> </interceptors>
上述就是通過注冊監聽集合下的interceptor(監聽者)來進行日志輸出。
IDbConfigurationInterceptor
在EF6.1中引入了此接口,它是一個當應用程序啟動時允許代碼檢測或者修改EF configuration的監聽接口。通過這個我們能夠實現一個關於 DatabaseLogger 的簡單版本
public class ExampleDatabaseLogger : IDbConfigurationInterceptor { public void Loaded( DbConfigurationLoadedEventArgs loadedEventArgs, DbConfigurationInterceptionContext interceptionContext) { var formatterFactory = loadedEventArgs .DependencyResolver .GetService<Func<DbContext, Action<string>, DatabaseLogFormatter>>(); var formatter = formatterFactory(null, Console.Write); DbInterception.Add(formatter); } }
我們來分析下上述代碼:
(1)第一行要求EF去注冊 DatabaseLogFormatter 工廠,上述我們只是新建了一個 DatabaseLogFormatter 來代替,當然如果應用程序在此行自定義了格式化 ,那么將通過用此自定義的而不會是默認的
(2)第一行實際上是沒有獲得DatabaseLogFormatter,它只是獲得一個來創建DatabaseLogFormatter實例的一個工廠,第二行調用工廠來獲得一個實際意義上的formatter實例。因為我們要記錄所有上下文實例,所以為空也是允許的。工廠的第二個參數是對於控制台輸出流的委托,因此我們只需將指針指向Console.Write()方法即可。同理如果我們要將日志記錄到文件中,則需要通過StreanWrite的實例來實現,上述已經演示。
(3)第三行將DatabaseLogFormatter作為一個監聽者來進行注冊,在EF中如何就監聽者來進行日志記錄,上述已經描述。
