EF Core 5 中的 DbContextFactory
Intro
使用過 EF Core 大多都會遇到這樣一個場景,希望能夠並行查詢,但是如果使用同一個 DbContext 例項進行並行操作的時候就會遇到一個 InvalidOperationException
的異常,在 EF Core 2.x/3.x 版本中, EF Core DbContext 的生命週期預設是 Scoped
,如果要並行查詢,需要建立多個 Scope,在子 Scope 中建立 DbContext 來進行操作,EF Core 5 中的 DbContextFactory
可以用來簡化這樣的操作,且看下文示例
DbContextFactory
DbContextFactory
就如同它的名字一樣,就是一個 DbContext
的工廠,就是用來建立 DbContext
的
IDbContextFactory
介面定義如下,Github 原始碼 https://github.com/dotnet/efcore/blob/v5.0.0/src/EFCore/IDbContextFactory.cs
public interface IDbContextFactory<out TContext> where TContext : DbContext
{
/// <summary>
/// <para>
/// Creates a new <see cref="DbContext" /> instance.
/// </para>
/// <para>
/// The caller is responsible for disposing the context; it will not be disposed by the dependency injection container.
/// </para>
/// </summary>
/// <returns> A new context instance. </returns>
TContext CreateDbContext();
}
需要注意的是,如果使用 DbContextFactory
來建立 DbContext
,需要自己來釋放 DbContext
,需要自己使用 using
或者 Dispose
來釋放資源
另外 DbContextFactory
生命週期不同於 DbContext
,預設的生命週期的 Singleton
,也正是因為這樣使得我們可以簡化並行查詢的程式碼,可以參考
Sample
來看一個實際的示例,這是一個並行操作插入100條記錄的簡單示例,看一下如何使用 DbContextFactory
進行並行操作
var services = new ServiceCollection();
services.AddDbContextFactory<TestDbContext>(options =>
{
options.UseInMemoryDatabase("Tests")
;
});
using var provider = services.BuildServiceProvider();
var contextFactory = provider.GetRequiredService<IDbContextFactory<TestDbContext>>();
Enumerable.Range(1, 100)
.Select(async i =>
{
using (var dbContext = contextFactory.CreateDbContext())
{
dbContext.Posts.Add(new Post() { Id = i + 101, Author = $"author_{i}", Title = $"title_{i}" });
return await dbContext.SaveChangesAsync();
}
})
.WhenAll()
.Wait();
using var context = contextFactory.CreateDbContext();
Console.WriteLine(context.Posts.Count());
實現原始碼
EF Core 的 DbContextFactory
的實現不算複雜,一起來看一下,首先看一下 DbContextFactory
的實現:
public class DbContextFactory<TContext> : IDbContextFactory<TContext> where TContext : DbContext
{
private readonly IServiceProvider _serviceProvider;
private readonly DbContextOptions<TContext> _options;
private readonly Func<IServiceProvider, DbContextOptions<TContext>, TContext> _factory;
public DbContextFactory(
[NotNull] IServiceProvider serviceProvider,
[NotNull] DbContextOptions<TContext> options,
[NotNull] IDbContextFactorySource<TContext> factorySource)
{
Check.NotNull(serviceProvider, nameof(serviceProvider));
Check.NotNull(options, nameof(options));
Check.NotNull(factorySource, nameof(factorySource));
_serviceProvider = serviceProvider;
_options = options;
_factory = factorySource.Factory;
}
public virtual TContext CreateDbContext()
=> _factory(_serviceProvider, _options);
}
可以看到 DbContextFactory
的實現裡用到了一個 IDbContextFactorySource
,再來看一下 DbContextFactorySource
的實現,實現如下:
public class DbContextFactorySource<TContext> : IDbContextFactorySource<TContext> where TContext : DbContext
{
public DbContextFactorySource()
=> Factory = CreateActivator();
public virtual Func<IServiceProvider, DbContextOptions<TContext>, TContext> Factory { get; }
private static Func<IServiceProvider, DbContextOptions<TContext>, TContext> CreateActivator()
{
var constructors
= typeof(TContext).GetTypeInfo().DeclaredConstructors
.Where(c => !c.IsStatic && c.IsPublic)
.ToArray();
if (constructors.Length == 1)
{
var parameters = constructors[0].GetParameters();
if (parameters.Length == 1)
{
var isGeneric = parameters[0].ParameterType == typeof(DbContextOptions<TContext>);
if (isGeneric
|| parameters[0].ParameterType == typeof(DbContextOptions))
{
var optionsParam = Expression.Parameter(typeof(DbContextOptions<TContext>), "options");
var providerParam = Expression.Parameter(typeof(IServiceProvider), "provider");
return Expression.Lambda<Func<IServiceProvider, DbContextOptions<TContext>, TContext>>(
Expression.New(
constructors[0],
isGeneric
? optionsParam
: (Expression)Expression.Convert(optionsParam, typeof(DbContextOptions))),
providerParam, optionsParam)
.Compile();
}
}
}
var factory = ActivatorUtilities.CreateFactory(typeof(TContext), new Type[0]);
return (p, _) => (TContext)factory(p, null);
}
}
從上面的原始碼中可以看得出來, DbContextFactory
把工廠拆成了兩部分,DbContextFactorySource
提供一個工廠方法,提供一個委託來建立 DbContext
,而 DbContextFactory
則利用 DbContextFactorySource
提供的工廠方法來建立 DbContext
.
More
DbContextFactory
可以使得在並行操作得時候會更加方便一些,但是注意要自己控制好 DbContext
生命週期,防止記憶體洩漏。
對於 EF Core DbContextFactory
的實現,不得不說這樣的實現靈活性更強一些,但是又感覺有一些多餘,想要擴充套件 DbContextFactory
的實現,直接重寫一個 DbContextFactory
的實現服務註冊的時候注入就可以了,你覺得呢~~
Reference
- https://github.com/dotnet/efcore/blob/v5.0.0/src/EFCore/IDbContextFactory.cs
- https://github.com/dotnet/efcore/blob/v5.0.0/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs#L607
- https://github.com/dotnet/efcore/blob/v5.0.0/src/EFCore/Internal/DbContextFactory.cs
- https://github.com/dotnet/efcore/blob/v5.0.0/src/EFCore/Internal/DbContextFactorySource.cs
- https://github.com/WeihanLi/SamplesInPractice/blob/master/EF5Samples/DbContextFactoryTest.cs