EFCore 5 中的 DbContextFactory

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 源碼 //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,也正是因為這樣使得我們可以簡化並行查詢的程式碼,可以參考

//github.com/dotnet/efcore/blob/v5.0.0/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs#L607

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

Tags: