万字长文,带你彻底理解EF Core5的运行机制,让你成为团队中的EF Core专家
在EF Core 5中,有很多方式可以窺察工作流程中發生的事情,并與該信息進行交互。這些功能點包括日志記錄,攔截,事件處理程序和一些超酷的最新出現的調試功能。EF團隊甚至從Entity Framework的第一個版本中恢復了一些有用的舊的功能。本博文帶你更深入地研究訪問EF Core 5的一些元數據和其有趣的使用方式。
1、將EF的ToTraceString移植為EF Core的ToQueryString
這是回憶殺。
在Entity Framework的第一個迭代版本中,沒有內置的日志記錄。但是有ObjectQuery.ToTraceString(),這是一種運行時方法,可以動態計算LINQ或Entity SQL查詢的SQL,盡管這不是一個很好的日志記錄方法,但它畢竟可以輸出SQL,即使在今天,也有一些有用的場景。
直到最新版本EF Core 5,該功能才成為EF Core的一部分,并且已重命名為ToQueryString()。
如果要查看實體類People的簡單查詢所生成的SQL,只需將ToQueryString附加到查詢中即可。不涉及LINQ執行方法。
換句話說,將查詢本身與執行方法分開,僅僅針對查詢。
var sqlFromQuery=context.People.ToQueryString();ToQueryString的一個有趣用例是在調試時查看其結果,不必等到運行方法即可檢查日志中的SQL。例如我可以構建查詢,捕獲字符串,然后執行查詢。
private static void GetAllPeople() {using var context = new PeopleContext();var query = context.People.Where(p=>p.FirstName=="Julie");var sqlFromQuery = query.ToQueryString();var people = query.ToList(); }然后在調試時,可以看到sqlFromQuery變量的預期SQL。當然您不需要將此代碼嵌入生產代碼中。實際上,也非常不建議這樣做,因為當EF Core進行SQL編制過程時,它很容易影響性能。
您應該可以在調試器中調用ToQueryString,如圖所示。
在調用ToQueryString之前,查詢變量已經作為DbQuery進行了評估,因此可以正常工作。
調試上下文并在調試器中直接顯示DbSet,例如在調試器中成功運行context.People.ToQueryString(),但是您不能直接評估LINQ表達式。換句話說,如果要調試上下文變量,然后在調試器中使用Where方法,它將失敗。這并不是什么新鮮事物,也不是ToQueryString的限制。
關于ToQueryString的最后一個要點是對它的評估基于最簡單的執行:ToList。使用諸如FirstOrDefault之類的LINQ執行查詢會影響SQL的呈現方式,因此,在使用FirstOrDefault執行查詢時,ToQueryString呈現的SQL與發送給數據庫的SQL不同。這種情況下需要 EF Core日志記錄來打印準確的Sql,而不是還執拗于ToQueryString。
我發現在集成測試場景下,ToQueryString特別有用。如果您需要編寫測試,測試的成功取決于生成的SQL表達式的某些部分,那么ToQueryString是比日志記錄更簡單的路徑。使用日志記錄時,您必須將日志捕獲到文本編寫器中,然后讀取該文本。盡管使用InMemory提供程序可能很誘人,但請記住,InMemory提供程序不會生成SQL。您需要為真實數據庫使用提供程序,數據庫不需要存在即可使用ToQueryString。EF Core在內存中才能確定SQL。
這是一個演示測試示例,旨在證明EF Core編寫的智能SQL比我編寫的更為智能。請注意,我在測試項目中引用了Microsoft.EntityFrameworkCore.Sqlite提供程序。如您所知,EF和EF Core總是投影與實體屬性相關的列。它不寫SELECT *。
[TestMethod] public void SQLDoesNotContainSelectStar() {var builder = new DbContextOptionsBuilder();builder.UseSqlite("Data Source=testdb.db");using var context = new PeopleContext(builder.Options);var sql=context.People.ToQueryString();Assert.IsFalse(sql.ToUpper().Contains("SELECT *")); }如果您使用攔截器來執行軟刪除,并且使用全局查詢過濾器來始終過濾出這些行。例如,這是我DbContext OnModelBuildling方法中的一個查詢過濾器,它告訴EF Core過濾掉IsDeleted屬性為true的Person行。
modelBuilder.Entity<Person>().HasQueryFilter(p => !p.IsDeleted);有了這個,我可以編寫與上面類似的測試,但是將斷言更改為以下內容,以確保我不會破壞全局查詢過濾器邏輯。
Assert.IsTrue(sql.ToUpper().Contains("WHERE NOT (\"p\".\"IsDeleted\")"));2、從EF Core記錄詳細信息
共有三種方法可以利用EF Core的日志管道。
2.1、 簡單的日志記錄
可以與.NET的日志記錄API結合使用,所有的繁重辛苦的工作都是在后臺進行的。您可以使用LogTo方法輕松配置DbContext,將.NET日志記錄輸出。
嗯,我就想看著你,就這樣子,簡簡單單。
EF Core將輸出很多事件。分為以下類,這些類從DbCloggerCategory派生。
變更追蹤,ChangeTracking
數據庫命令,Database.Command
數據庫連接,Database.Connection
數據庫事務,Database.Transaction
數據庫,Database
基礎設施,Infrastructure
移居,Migrations
模型驗證,Model.Validation
模型,Model
詢問,Query
腳手架,Scaffolding
更新,Update
您可以使用這些類別將輸出篩選為要記錄的信息類型。
LogTo的一個參數指定目標為控制臺窗口、文件或調試窗口。
然后,第二個參數允許您通過.NET LogLevel以及您感興趣的任何DLoggerCategoy進行篩選。
此示例配置DbContext將日志輸出到控制臺,并過濾掉所有DbLoggerCategory類型LogLevel.Information組。
optionsBuilder.UseSqlServer(myConnectionString) .LogTo(Console.WriteLine,LogLevel.Information);下面一個LogTo方法添加了第三個參數-DbLoggerCatetory數組(僅包含一個數組),以便僅對EF Core的數據庫命令進行進一步過濾。
與LogTo方法一起,我添加了EnableSensitiveDataLogging方法以在SQL中顯示傳入參數。這將捕獲所有發送到數據庫的SQL:查詢,更新,原始SQL甚至通過遷移發送的更改。
.LogTo(Console.WriteLine, LogLevel.Information,new[]{DbLoggerCategory.Database.Command.Name}, ) .EnableSensitiveDataLogging();上面包含IsDeleted屬性的“Person”類型也具有FirstName和LastName屬性。這是添加新的Person對象后調用SaveChanges的日志。
info: 1/4/2021 17:56:09.935RelationalEventId.CommandExecuted[20101](Microsoft.EntityFrameworkCore.Database.Command)Executed DbCommand (22ms) [Parameters=[ @p0='Julie' (Size = 4000), @p1='False', @p2='Lerman' (Size = 4000)],CommandType='Text',CommandTimeout='30']SET NOCOUNT ON;INSERT INTO [People] ([FirstName],[IsDeleted], [LastName])VALUES (@p0, @p1, @p2);SELECT [Id]FROM [People]WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();日志記錄顯示信息類型、EventId、以及請求的記錄器類別的詳細信息。接下來,日志名稱,執行時間和參數列表。由于啟用了敏感數據記錄,因此將顯示參數。最后,它列出了發送到數據庫的SQL。
LogTo使EF Core輸出基本日志記錄變得容易。您可以在DbContext,ASP.NET Core應用程序的啟動文件或ASP.NET Core應用程序的應用程序配置文件中對其進行配置。
注意頂部的EventId。您甚至可以定義日志記錄以使用這些ID過濾特定事件。您還可以過濾出特定的日志類別,并且可以控制格式。在https://docs.microsoft.com/zh-cn/ef/core/logging-events-diagnostics/simple-logging上查看有關這些各種功能的更多詳細信息的文檔。
簡單日志記錄是記錄EF Core的高級方法,是的,高級的就是簡單的,這就是計算機世界的定義!
您也可以通過直接與Microsoft.Extensions.Logging一起,以對EF Core的日志方式進行更多控制。檢查EF Core文檔,以獲取更多有關使用此更高級用法的詳細信息:https://docs.microsoft.com/zh-cn/ef/core/logging-events-diagnostics/extensions-logging。
2.2、響應EF Core 事件
EF Core 2.1在EF Core管道中引入了.NET事件。開始只有兩個:ChangeTracker.Tracked(在DbContext開始跟蹤實體時引發)和ChangeTracker.StateChanged(在已跟蹤的實體的狀態改變時引發)。
后來,看久了生情,事件的家族又迎來了幾個小家伙......
有了基本邏輯,團隊向EF Core 5添加三個事件:SaveChangesFailed、SaveChanges和SaveChangesAsync。
當上下文將要保存更改時,將引發DbContext.SavingChanges。
在兩個保存更改方法中的任何一個成功完成之后,將引發DbContext.SavedChanges。
DbContext.SaveChangesFailed用于捕獲和檢查故障。
能夠分離此邏輯,而不是全部填充到SaveChanges方法的中,這是一個很好的選擇。
可以使用這些事件來記錄未跟蹤的備用信息,您甚至可以使用事件來發出記錄器無法跟蹤的備用信息。
如果要使用影子屬性跟蹤審核數據,則可以在構造SQL并將其發送到數據庫之前,使用SavingChanges事件更新這些屬性。
例如,我將應用程序設置為向每個實體添加UserId陰影屬性(不包括那些屬性包和擁有的實體)。當用戶登錄時,我的應用程序有一個名為Globals.CurrentUserId的靜態變量。此外,在我的DbContext類中,我創建了一個名為SetUserId的私有方法,該方法將我的shadow屬性(存在的地方)的值設置為CurrentUserId。
private void SetUserId(object sender, SavingChangesEventArgs e) {foreach (var entry in ChangeTracker.Entries().Where(entry => entry.Metadata.GetProperty("UserId") != null)){entry.Property("UserId").CurrentValue = Globals.CurrentUserId;} }最后,我可以將SetUserId方法連接到DbContext的構造函數中的SavingChanges事件:
public PeopleContext() {SavingChanges += SetUserId; }現在,每當我調用SaveChanges時,UserId就會與其他數據一起持久保存到表中。
這是一些日志數據:
Executed DbCommand (29ms) [Parameters=[@p0='Julie' (Size = 4000), @p1='False', @p2='Lerman' (Size = 4000), @p3='101'], CommandType='Text', CommandTimeout='30']SET NOCOUNT ON; INSERT INTO [People] ([FirstName],[IsDeleted], [LastName], [UserId]) VALUES (@p0, @p1, @p2, @p3); SELECT [Id] FROM [People] WHERE @@ROWCOUNT = 1AND [Id] = scope_identity();這只是利用這些事件的一種簡單方法。
2.3、使用事件計數器訪問指標
EF Core 5利用了.NET Core 3.0中.NET引入的一項很酷的功能-dotnet-counters(https://docs.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters)。計數器是一個全局命令行工具。您可以使用dotnet CLI安裝此工具。
dotnet tool install --global dotnet-counters
安裝完成后,您可以告訴它監視在dotnet環境中運行的進程。您需要提供正在運行的.NET應用程序的進程ID
System.Diagnostics.Process.GetCurrentProcess().Id在Visual Studio中,我無法簡單地在調試器中調試此值。調試器只會告訴您“此表達式會產生副作用,因此不會被評估。” 因此,我將其嵌入到我的代碼中并獲得了值313131。
在擁有ID且應用仍在運行的情況下,然后可以觸發計數器,開始監視來自Microsoft.EntityFramework命名空間的事件。如下:
dotnet counters monitor Microsoft.EntityFrameworkCore -p 313131然后,當您遍歷應用程序時,計數器將顯示EF Core統計信息的特定列表,如圖所示,然后在應用程序執行其功能時更新計數。
我僅監視了一個小型演示應用程序,因此計數并不是很好看,但是您可以看到我正在運行一個DbContext實例(Active DbContexts),我已經運行了三個查詢,并利用了查詢緩存(因為我運行了其中一些查詢不止一次),并兩次調用SaveChanges。
這看起來像您的代碼分析工具,但是當針對更密集的解決方案運行時,它肯定會更有用。EF團隊建議您在文檔中仔細閱讀dotnet-counters功能,以便正確使用EF Core。
3、攔截EF Core的數據——攔截器
EF Core的攔截器是一項功能,該功能始于EF6,并在EF Core 3中重新引入。EF Core 5中引入了SaveChanges的新攔截器。
由于此功能已經存在很長時間了(盡管它對于EF Core是相當新的),因此應該有很多文章介紹。即使這樣,我還是覺得很神奇。
共有三種不同的攔截器類來攔截命令,連接和事務,以及用于SaveChanges的新攔截器類。每個類都有自己相關的虛擬方法(和相關對象)。例如,DbCommandInterceptor公開了ReaderExecuting和ReaderExecutingAsync,它們在命令即將發送到數據庫時被觸發。
public override InterceptionResult<DbDataReader>ReaderExecuting(DbCommand command,CommandEventData eventData,InterceptionResult<DbDataReader> result){//例如,webmote支持你干點啥?return result;}它的參數之一是DbCommand,其CommandText屬性保存SQL。
如果要修改SQL,添加查詢提示或其他任務,則可以更改命令,然后使用新CommandText值的命令將繼續進行。
從數據庫返回任何結果數據時,將觸發ReaderExecuted / Async方法。
public override DbDataReader ReaderExecuted(DbCommand command,CommandExecutedEventData eventData,DbDataReader result){return base.ReaderExecuted(command, eventData, result);}例如,在這里您可以捕獲DbDataReader,并對該數據進行某些處理,然后再繼續執行EF Core實現。一個示例是記錄一些記錄器無法捕獲的內容,例如:
4、查詢攔截
EF Core 公開的?DbCommandInterceptor攔截器提供查詢攔截功能,查詢攔截是在數據庫上執行查詢之前插入邏輯,或者在查詢執行之后(以及控制返回到調用代碼之前)立即插入邏輯的能力。
此功能在現實世界中有多種使用案例:
延長具有某些特征的命令的超時
查詢失敗并記錄異常時診斷信息
當讀取到內存的行數超過特定閾值時,記錄警告
一個小例子:
public class TestQueryInterceptor : DbCommandInterceptor {// runs before a query is executedpublic override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result){command.CommandText += " OPTION (OPTIMIZE FOR UNKNOWN)"; command.CommandTimeout = 12345; return result;}// runs after a query is excutedpublic override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result){if (this.ShouldChangeResult(command, out var changedResult)){return changedResult;}return result;} }注意:?大多數方法都有同步和異步版本。令人討厭的是,異步查詢僅觸發異步方法(反之亦然),因此在編寫攔截器時必須覆蓋兩者。
安裝攔截器是很簡單的。
public class SampleDbContext : DbContext {protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlite(@"Data Source=Sample.db;").AddInterceptors(new TestQueryInterceptor (), new SampleInterceptor2());} }通過返回InterceptionResult<T>.SuppressWithResult()禁止執行。重要的是要注意,DbCommandInterceptor安裝的其他所有組件仍將執行,并且可以通過上的HasResult屬性檢查其他攔截器是否已禁止執行result。
public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result) {if (this.ShouldSuppressExecution(command)){return InterceptionResult.SuppressWithResult<object>(null);}return result; }方法中引發的異常從技術上將阻止執行,不要利用這個事實,將異常用于控制流幾乎總是很糟糕的設計。
你可以攔截如下清單的操作:
| CommandCreating | 在創建命令之前(注意:一切都是命令,因此它將攔截所有查詢) |
| CommandCreated | 創建命令之后但執行之前 |
| CommandFailed[Async] | 在執行過程中命令失敗并出現異常后 |
| ReaderExecuting[Async] | 在執行“查詢”命令之前 |
| ReaderExecuted[Async] | 執行“查詢”命令后 |
| NonQueryExecuting[Async] | 在執行“非查詢”命令之前(注意:非查詢的一個示例是 ExecuteSqlRaw |
| NonQueryExecuted[Async] | 執行“非查詢”命令后 |
| ScalarExecuting [Async] | 在執行“標量”命令之前(注意:“標量”是存儲過程的同義詞) |
| ScalarExecuted [Async] | 執行“標量”命令后 |
| DataReaderDispose | 執行命令后 |
這是一個耗時命令攔截
public class MyDBCommandInterceptor: DbCommandInterceptor {public static ConcurrentDictionary CommandStartTimes = new ConcurrentDictionary();public static ConcurrentDictionary CommandDurations = new ConcurrentDictionary();public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) {CommandStartTimes.TryAdd(command, DateTime.Now);base.NonQueryExecuting(command, interceptionContext);}public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) {CommandStartTimes.TryAdd(command, DateTime.Now);base.ReaderExecuting(command, interceptionContext);}public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) {CommandStartTimes.TryAdd(command, DateTime.Now);base.ScalarExecuting(command, interceptionContext);}public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) {base.NonQueryExecuted(command, interceptionContext);AccumulateTime(command);}public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) {base.ReaderExecuted(command, interceptionContext);AccumulateTime(command);}public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) {base.ScalarExecuted(command, interceptionContext);AccumulateTime(command);}private void AccumulateTime(DbCommand command) {if (CommandStartTimes.TryRemove(command, outvar commandStartTime)) {var commandDuration = DateTime.Now - commandStartTime;CommandDurations.AddOrUpdate(command.CommandText, commandDuration, (_, accumulated) => commandDuration + accumulated);}} }有關使用EF Core文檔中的攔截器的大量指導,請訪問https://docs.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors。
5、EF Core 5中的Sleeper功能:調試視圖
就是?ChangeTracker.DebugView和Model.DebugView。
DebugViews輸出格式正確的字符串,其中有ChangeTracker的狀態或模型中的元數據的信息。DebugView提供了一個漂亮的文檔,您可以捕獲和打印該文檔,并真正了解其幕后情況。
我在調試器上花費了大量時間,以探索有關變更跟蹤器了解的內容或EF Core如何解釋我所描述的模型的各種詳細信息。能夠以這種文本格式讀取此信息,甚至將其保存在文件中,因此您無需反復調試即可收集詳細信息,這是EF Core 5的一項神奇功能。
確保您了解DebugViews是撰寫本文的目的。
在DbContext.ChangeTracker.DebugView中,您將找到ShortView和LongView屬性。例如,這里是我剛查詢一個Person對象時的視圖,而我的上下文僅包含一個人。
Person {Id: 1} Unchanged這是最常用的信息-在我的上下文中,只有一個未更改的Person的ID為1。
LongView提供了有關被跟蹤實體的更多詳細信息。
Person {Id: 1} UnchangedId: 1 PKFirstName: 'Julie'IsDeleted: 'False'LastName: 'Lerman'UserId: 101Addresses: []如果要在跟蹤的Person上對其進行編輯并強制上下文檢測更改,則LongView除了將狀態顯示為Modified之外,還對LastName屬性所做的更改進行記錄。
Person {Id: 1} ModifiedId: 1 PKFirstName: 'Julie'IsDeleted: 'False'LastName: 'Lermantov' ModifiedOriginally 'Lerman'UserId: 101Addresses: []您可以在此視圖中看到一個Addresses屬性。實際上,使用導航,“人”和“地址”之間存在多對多關系。EF Core在運行時推斷內存中的PersonAddress實體,以便將關系數據持久化到聯接表中。
當我在其“地址”集合中創建一個具有一個地址的人的圖形時,您可以在ShortView中看到一個“人”,一個地址和一個推斷的PersonAddress對象。長視圖顯示了這些對象的屬性。
AddressPerson (Dictionary<string, object>){AddressesId: 1, ResidentsId: 1} Unchanged FK{AddressesId: 1} FK {ResidentsId: 1} Address {Id: 1} Unchanged Person {Id: 1} Modified我喜歡這些調試視圖,這些視圖可以在調試時幫助我發現被跟蹤對象的狀態和關系,無論我是在解決問題還是在學習它的工作方式。
讓我們轉到Model.DebugViews看看您可以從中學到什么。
首先,我應該闡明我的模型。使用Visual Studio中的EF Core Power Tools擴展來可視化模型。
DbContext.Model.DebugView也具有ShortView和LongView。它們都包含很多信息。
您可以看到屬性,主鍵和外鍵,索引以及級聯刪除規則,多對多關系,甚至指定了它使用跳過導航。還描述了繼承。您可以從這份文件中學到很多東西。
清單1:數據模型的Model.DebugView.ShortView
Model.DebugView.LongView包含更多詳細信息,它們描述了注釋,數據庫映射等。您可以從LongView中學到更多,但并不是每個人都希望看到這種細節,如果您需要,它就在那里。
清單2:在Model.DebugView的LongView中描述的Person實體
6、利用
有關EF Core,您對幕后情況了解得越多,對該工具的掌控力就越大。
了解EF Core如何解釋您的類和映射,你可以控制這些模型并按照您希望的方式持久保存數據,也可以根據需要修改SQL甚至結果。學習如何利用本文中介紹的各種調試,日志記錄,偵聽和事件處理方法,希望能幫助您成為團隊中的EF Core專家。
當然,不用多久,你就會升職加薪、當上總經理、出任CEO、迎娶白富美、走上人生巔峰,想想是不是還有點小激動?
總結
以上是生活随笔為你收集整理的万字长文,带你彻底理解EF Core5的运行机制,让你成为团队中的EF Core专家的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Polly-故障处理和弹性应对很有一手
- 下一篇: C#使用线程窗口调试多线程程序