最近在接觸DDD+micro service來開發專案,因為EF Core太適合DDD模式需要的ORM設計,所以這篇部落格是從程式碼角度去理解EF core的內部實現,希望大家能從其中學到一些心得體會去更好的寫出高質量的程式碼。
從github 上去下載ef core倉庫, 本篇程式碼的版本是基於tag v5.0.3的,如果大家在以後看見這篇部落格,可以在分支上reset 到這個tag對照這邊部落格,下載完成之後,配置根目錄下的global.json成本機已經安裝的sdk, runtime 的版本,直接build通過就可以了。下面是程式碼的目錄。
簡單的說一下,benchmark 是用來效能測試的,solution是一些自動化部署的配置檔案,src是ef core 的核心程式碼,test 是此專案的單元測試目錄,如果你想更深入瞭解每個模組的實現邏輯,從單元測試出發是一個非常不錯的選擇。
在使用者階段我們需要做的如下,程式碼來自 ef 官方文件
1 using System; 2 using System.Linq; 3 4 namespace EFGetStarted 5 { 6 internal class Program 7 { 8 private static void Main() 9 { 10 using (var db = new BloggingContext()) 11 { 12 // Note: This sample requires the database to be created before running. 13 14 // Create 15 Console.WriteLine("Inserting a new blog"); 16 db.Add(new Blog { Url = "http://blogs.msdn.com/adonet" }); 17 db.SaveChanges(); 18 19 // Read 20 Console.WriteLine("Querying for a blog"); 21 var blog = db.Blogs 22 .OrderBy(b => b.BlogId) 23 .First(); 24 25 // Update 26 Console.WriteLine("Updating the blog and adding a post"); 27 blog.Url = "https://devblogs.microsoft.com/dotnet"; 28 blog.Posts.Add( 29 new Post { Title = "Hello World", Content = "I wrote an app using EF Core!" }); 30 db.SaveChanges(); 31 32 // Delete 33 Console.WriteLine("Delete the blog"); 34 db.Remove(blog); 35 db.SaveChanges(); 36 } 37 } 38 } 39 }
首先我們先看在new DbContext的時候做了哪些的初始化的動作
1 protected DbContext() 2 : this(new DbContextOptions<DbContext>()) 3 { 4 }
這個Context建立了了自己的DbContextOptions,很明顯我們需要的一些配置像資料庫元件等資訊都是需要在DbContextOptions裡配置的,我們繼續看下去會發現在初始化的時候會建立一個字典物件,正如其名所說的一樣這是在維護一些擴充套件元件。當你想使用不同型別的資料庫是引用的library 就是在動態註冊這些元件。在本例中,我們使用InMemoryDatabse來測試。
1 protected DbContextOptions( 2 [NotNull] IReadOnlyDictionary<Type, IDbContextOptionsExtension> extensions) 3 { 4 Check.NotNull(extensions, nameof(extensions)); 5 6 _extensions = extensions; 7 }
在相應的DbContext的繼承子類重寫OnConfiguring方法就可以使用memory database,配置如下
1 protected internal override void OnConfiguring(DbContextOptionsBuilder options) 2 { 3 options.UseInMemoryDatabase(nameof(BloggingContext)); 4 }
初始化新的option好的時候,這是一個空的配置檔案,後面就是對option做的初始化的動作。
1 public DbContext([NotNull] DbContextOptions options) 2 { 3 Check.NotNull(options, nameof(options)); 4 5 if (!options.ContextType.IsAssignableFrom(GetType())) 6 { 7 throw new InvalidOperationException(CoreStrings.NonGenericOptions(GetType().ShortDisplayName())); 8 } 9 10 _options = options; 11 20 ServiceProviderCache.Instance.GetOrAdd(options, providerRequired: false) 21 .GetRequiredService<IDbSetInitializer>() 22 .InitializeSets(this); 23 24 EntityFrameworkEventSource.Log.DbContextInitializing(); 25 }
check完之後,ServiceProviderCache的單例模式去獲取內部的service provide,這個很明顯是對需要的元件進行依賴注入。我們擷取了一些核心程式碼如下。
1 public virtual IServiceProvider GetOrAdd([NotNull] IDbContextOptions options, bool providerRequired) 2 { 3 var key = options.Extensions 4 .OrderBy(e => e.GetType().Name) 5 .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.Info.GetServiceProviderHashCode()); 6 7 return _configurations.GetOrAdd(key, k => BuildServiceProvider()).ServiceProvider; 8 9 (IServiceProvider ServiceProvider, IDictionary<string, string> DebugInfo) BuildServiceProvider() 10 { 11 12 var services = new ServiceCollection(); 13 var hasProvider = ApplyServices(options, services); 14 15 var serviceProvider = services.BuildServiceProvider(); 16 17 if (hasProvider) 18 { 19 serviceProvider 20 .GetRequiredService<ISingletonOptionsInitializer>() 21 .EnsureInitialized(serviceProvider, options); 22 } 23 24 return (serviceProvider, debugInfo); 25 } 26 }
從上面可以看出,option 的註冊擴充套件元件不一樣會建立不一樣的service provide,在第13行程式碼會進行依賴注入,我們點進去之後會看到如下程式碼。
1 private static bool ApplyServices(IDbContextOptions options, ServiceCollection services) 2 { 3 var coreServicesAdded = false; 4 5 foreach (var extension in options.Extensions) 6 { 7 extension.ApplyServices(services); 8 9 if (extension.Info.IsDatabaseProvider) 10 { 11 coreServicesAdded = true; 12 } 13 } 14 15 if (coreServicesAdded) 16 { 17 return true; 18 } 19 20 new EntityFrameworkServicesBuilder(services).TryAddCoreServices(); 21 22 return false; 23 }
迴圈option獲取其中的擴充套件元件,每個元件會有自己的依賴物件注入,如果沒找到database provider,再注入當前的context的核心依賴物件注入,注入的邏輯不用看了,後面去查介面的實現物件參考這個就行了。程式碼到現在我們能看到已經獲取service provider了,接下來繼續看InitializeSets的方法。
1 public virtual void InitializeSets(DbContext context) 2 { 3 foreach (var setInfo in _setFinder.FindSets(context.GetType()).Where(p => p.Setter != null)) 4 { 5 setInfo.Setter.SetClrValue( 6 context, 7 ((IDbSetCache)context).GetOrAddSet(_setSource, setInfo.Type)); 8 } 9 }
我們記得之前宣告dbcontext 的時候我們會寫dbset 屬性,但是並沒有對其進行賦值,並且所有對錶的操作彷彿都是通過這個物件來完成的,是因為dbcontext 幫我們自動做了賦值的操作,我們找到findSets的實現邏輯。
1 public virtual IReadOnlyList<DbSetProperty> FindSets(Type contextType) 2 => _cache.GetOrAdd(contextType, FindSetsNonCached); 3 4 private static DbSetProperty[] FindSetsNonCached(Type contextType) 5 { 6 var factory = new ClrPropertySetterFactory(); 7 8 return contextType.GetRuntimeProperties() 9 .Where( 10 p => !p.IsStatic() 11 && !p.GetIndexParameters().Any() 12 && p.DeclaringType != typeof(DbContext) 13 && p.PropertyType.GetTypeInfo().IsGenericType 14 && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) 15 .OrderBy(p => p.Name) 16 .Select( 17 p => new DbSetProperty( 18 p.Name, 19 p.PropertyType.GenericTypeArguments.Single(), 20 p.SetMethod == null ? null : factory.Create(p))) 21 .ToArray(); 22 }
上面的程式碼會顯示不是靜態的,不是索引,type不是dbcontext的,是泛型的,是dbset泛型的屬性會當做dbcontext 的set來處理,然後map成DbSetProperty物件進行維護,值得注意的一點是SetMethod為factory.Create(p)返回的Func型別,這個時候物件並沒有賦值,指到在上面程式碼setInfo.Setter.SetClrValue呼叫完這個set才是真正的進行賦值,而其中的邏輯是如下所示。
1 [UsedImplicitly] 2 private static Func<DbContext, string, object> CreateSetFactory<TEntity>() 3 where TEntity : class 4 => (c, name) => new InternalDbSet<TEntity>(c, name);
其中的dbset 屬性就是一個個的InternalDbSet物件,初始化set之後dbcontext就是日誌記錄一下,這個不是我的研究重點物件,到此為止一個dbcontext物件建立成功,這個時候我們的疑問就來了,我們的資料庫配置的元件等配置是什麼時候初始化的呢,明顯現在的option 是非常乾淨的。
不用急我們現在來看一下第一個程式碼片段中的db.Add方法吧,看一下這個裡面做了啥。
1 private EntityEntry<TEntity> SetEntityState<TEntity>( 2 TEntity entity, 3 EntityState entityState) 4 where TEntity : class 5 { 6 var entry = EntryWithoutDetectChanges(entity); 7 8 SetEntityState(entry.GetInfrastructure(), entityState); 9 10 return entry; 11 }
我們會傳入一個entity物件,並且將entityState 置為add 的狀態,這個時候我們會涉及到一個重要的物件,就是DbContextDependencies.StateManager,這個就是內部的entity的狀態管理物件,我們稍後會討論這個物件。
1 private EntityEntry<TEntity> EntryWithoutDetectChanges<TEntity>(TEntity entity) 2 where TEntity : class 3 => new(DbContextDependencies.StateManager.GetOrCreateEntry(entity));
這個時候我們需要重視DbContextDependencies這個物件,也就是dbcontext的依賴物件,它會通過InternalServiceProvider物件來獲得,但是其中會有一些初始化的邏輯
1 private IServiceProvider InternalServiceProvider 2 { 3 get 4 { 5 CheckDisposed(); 6 7 if (_contextServices != null) 8 { 9 return _contextServices.InternalServiceProvider; 10 } 11 12 if (_initializing) 13 { 14 throw new InvalidOperationException(CoreStrings.RecursiveOnConfiguring); 15 } 16 17 try 18 { 19 _initializing = true; 20 21 var optionsBuilder = new DbContextOptionsBuilder(_options); 22 23 OnConfiguring(optionsBuilder); 24 25 if (_options.IsFrozen 26 && !ReferenceEquals(_options, optionsBuilder.Options)) 27 { 28 throw new InvalidOperationException(CoreStrings.PoolingOptionsModified); 29 } 30 31 var options = optionsBuilder.Options; 32 33 _serviceScope = ServiceProviderCache.Instance.GetOrAdd(options, providerRequired: true) 34 .GetRequiredService<IServiceScopeFactory>() 35 .CreateScope(); 36 37 var scopedServiceProvider = _serviceScope.ServiceProvider; 38 39 var contextServices = scopedServiceProvider.GetService<IDbContextServices>(); 40 41 contextServices.Initialize(scopedServiceProvider, options, this); 42 43 _contextServices = contextServices; 44 45 DbContextDependencies.InfrastructureLogger.ContextInitialized(this, options); 46 } 47 finally 48 { 49 _initializing = false; 50 } 51 52 return _contextServices.InternalServiceProvider; 53 } 54 }
在23行的OnConfiguring方法就是會呼叫我們配置的資料庫元件,本例中我們用的就是inmemorydatabase,我們現在撇一下這個元件中間做了啥。很簡單的就是在option中註冊了擴充套件元件InMemoryOptionsExtension,之前說過註冊了元件之後會重新生成新的server provider, 在新的server collection 重新注入memory database 元件所需要的依賴物件。在擴充套件元件的InMemoryOptionsExtension.ApplyServices 方法。這是每個擴充套件元件必須要實現的方法。現在我們知道database元件現在已經註冊進來了。繼續檢視contetx.add 方法的邏輯。
1 private IServiceProvider InternalServiceProvider 2 { 3 get 4 { 5 CheckDisposed(); 6 7 if (_contextServices != null) 8 { 9 return _contextServices.InternalServiceProvider; 10 } 11 12 if (_initializing) 13 { 14 throw new InvalidOperationException(CoreStrings.RecursiveOnConfiguring); 15 } 16 17 try 18 { 19 _initializing = true; 20 21 var optionsBuilder = new DbContextOptionsBuilder(_options); 22 23 OnConfiguring(optionsBuilder); 24 25 if (_options.IsFrozen 26 && !ReferenceEquals(_options, optionsBuilder.Options)) 27 { 28 throw new InvalidOperationException(CoreStrings.PoolingOptionsModified); 29 } 30 31 var options = optionsBuilder.Options; 32 33 _serviceScope = ServiceProviderCache.Instance.GetOrAdd(options, providerRequired: true) 34 .GetRequiredService<IServiceScopeFactory>() 35 .CreateScope(); 36 37 var scopedServiceProvider = _serviceScope.ServiceProvider; 38 39 var contextServices = scopedServiceProvider.GetService<IDbContextServices>(); 40 41 contextServices.Initialize(scopedServiceProvider, options, this); 42 43 _contextServices = contextServices; 44 45 DbContextDependencies.InfrastructureLogger.ContextInitialized(this, options); 46 } 47 finally 48 { 49 _initializing = false; 50 } 51 52 return _contextServices.InternalServiceProvider; 53 } 54 }
現在轉到上上上個程式碼片段,statemanager 需要建立一個entity,這時候會判斷這個entity存在不存在,如果不存在會在facyor 方法建立一個statemanager管理的entity,然後在statemanager更新這個entity的狀態。
1 public virtual InternalEntityEntry GetOrCreateEntry(object entity) 2 { 3 var entry = TryGetEntry(entity); 4 if (entry == null) 5 { 6 var entityType = _model.FindRuntimeEntityType(entity.GetType()); 7 if (entityType == null) 8 { 9 if (_model.IsShared(entity.GetType())) 10 { 11 throw new InvalidOperationException( 12 CoreStrings.UntrackedDependentEntity( 13 entity.GetType().ShortDisplayName(), 14 "." + nameof(EntityEntry.Reference) + "()." + nameof(ReferenceEntry.TargetEntry), 15 "." + nameof(EntityEntry.Collection) + "()." + nameof(CollectionEntry.FindEntry) + "()")); 16 } 17 18 throw new InvalidOperationException(CoreStrings.EntityTypeNotFound(entity.GetType().ShortDisplayName())); 19 } 20 21 if (entityType.FindPrimaryKey() == null) 22 { 23 throw new InvalidOperationException(CoreStrings.KeylessTypeTracked(entityType.DisplayName())); 24 } 25 26 entry = _internalEntityEntryFactory.Create(this, entityType, entity); 27 28 UpdateReferenceMaps(entry, EntityState.Detached, null); 29 } 30 31 return entry; 32 }
statemanager會有五個物件分別記錄每一個entity的不同的記錄,這五個物件分別是_detachedReferenceMap,_unchangedReferenceMap,_deletedReferenceMap,_modifiedReferenceMap,_addedReferenceMap,對應著放棄追蹤,沒有變化,刪除的,修改的,增加的各種entity 物件,如下圖所示,就是我們EF在記憶體維護的給個狀態的物件。
1 switch (state) 2 { 3 case EntityState.Detached: 4 _detachedReferenceMap ??= new Dictionary<object, InternalEntityEntry>(LegacyReferenceEqualityComparer.Instance); 5 _detachedReferenceMap[mapKey] = entry; 6 break; 7 case EntityState.Unchanged: 8 _unchangedReferenceMap ??= 9 new Dictionary<object, InternalEntityEntry>(LegacyReferenceEqualityComparer.Instance); 10 _unchangedReferenceMap[mapKey] = entry; 11 break; 12 case EntityState.Deleted: 13 _deletedReferenceMap ??= new Dictionary<object, InternalEntityEntry>(LegacyReferenceEqualityComparer.Instance); 14 _deletedReferenceMap[mapKey] = entry; 15 break; 16 case EntityState.Modified: 17 _modifiedReferenceMap ??= new Dictionary<object, InternalEntityEntry>(LegacyReferenceEqualityComparer.Instance); 18 _modifiedReferenceMap[mapKey] = entry; 19 break; 20 case EntityState.Added: 21 _addedReferenceMap ??= new Dictionary<object, InternalEntityEntry>(LegacyReferenceEqualityComparer.Instance); 22 _addedReferenceMap[mapKey] = entry; 23 break; 24 }
隨後在這個entity會進入到一個changetracking 的狀態。
好了今天寫到這個地方了,進入tracking 的狀態時會有一個graph 物件所管理,其中擁有一些圖的資料結構,這時候會對導航屬性等資訊進行處理,現在快11點了,就寫到這裡後面我會跟上後續的內容,謝謝大家的閱讀,如果有任何不理解或者指正的地方歡迎評論,最後謝謝大家。