EF Core 原始碼分析

NeilHu發表於2021-03-13

最近在接觸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點了,就寫到這裡後面我會跟上後續的內容,謝謝大家的閱讀,如果有任何不理解或者指正的地方歡迎評論,最後謝謝大家。

相關文章