Entity Framework Core 服务默认的生命周期

73
在ASP.NET Core应用程序中,我可以通过依赖注入来注册DbContext,如下所示。
services.AddDbContext<Models.ShellDbContext>(options => options.UseNpgsql(connection));

了解它的生命周期是很有趣的,那么它的生命周期是什么呢?

从这里 https://github.com/aspnet/EntityFramework/blob/f33b76c0a070d08a191d67c09650f52c26e34052/src/Microsoft.EntityFrameworkCore/EntityFrameworkServiceCollectionExtensions.cs#L140 看起来像是配置为Scoped,这意味着在每个请求上创建一个DbContext实例。

因此问题的第一部分是: 这是真的吗?如果是,那么成本高吗?

第二部分是: 如果我创建了一个使用DbContext的服务,并且旨在被控制器使用,并且将具有管理DB中某些实体的API,那么应该将其注册为Scoped吗?

3个回答

95
是的,DbContext 的默认生存周期是作用域(scoped)。这是有意为之的。
实例化 DbContext 的成本非常低,并确保您不使用过多的资源。如果您具有单例生命周期的 DbContext,则所有您仅读取一次的记录都将由 DbContext 跟踪,除非您明确禁用跟踪。这将需要更多的内存使用,并且它将不断增长。 DbContext 跟踪的内容越多,性能就越低。这就是为什么你经常会看到 DbContext 仅在 using(var context = new AppDbContext()) 块中使用的原因。
然而,在 Web 应用程序中,使用 using 块是不好的,因为生命周期是由框架管理的,如果您太早处理它,则之后的调用将失败并引发异常。
如果你在另一端使用瞬态生命周期,你将失去“事务”功能。使用作用域生命周期,DbContext具有与请求一样长的事务范围。
如果需要更精细的控制,您必须使用工作单元模式(DbContext已经有点使用了)。
对于您的第二个问题:
如果创建一个服务,它的生命周期必须与范围相同或更短(即:Scoped或transient)。
如果您明确需要更长的生命周期,则应将DbContext工厂服务或工厂方法注入到您的服务中。
您可以通过类似以下内容来实现:
services.AddTransient<Func<AppDbContext>>( (provider) => new Func<MyDbContext>( () => new AppDbContext()));
services.AddSingleton<IMySingletonService, MySingletonService>();

而你的服务可能看起来像这样:

public class MySingletonService : IMySingletonService, IDisposable
{
    private readonly AppDbContext context;

    public MySingletonService(Func<AppDbContext> contextFactory)
    {
        if(contextFactory == null)
            throw new ArgumentNullException(nameof(contextFactory));

        // it creates an transient factory, make sure to dispose it in `Dispose()` method.
        // Since it's member of the MySingletonService, it's lifetime
        // is effectively bound to it. 
        context = contextFactory();
    }
}

2
在 Web 应用程序中,真的是控制器管理生命周期吗?还是 DI 在请求结束时处理按请求范围作用域的内容?我想更好地理解这个问题。 - Joe Audette
我的错,我想说的是框架。在它的CreateController方法中有一个控制器工厂(IControllerFactory,请参见https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/Controllers/DefaultControllerFactory.cs的默认实现),它创建控制器并在`ReleaseController()`方法中处理控制器及其依赖项的释放。此外,请参阅此GitHub问题以获取更多详细信息和为什么以这种方式工作:https://github.com/aspnet/Mvc/issues/3727 - Tseng
如果我有一个在构造函数中需要 DbContext 并实现 IDisposable 接口的存储库,它是否应该在自己的 dispose 方法中调用 DbContext 的 dispose 呢?我注意到 Identity 的 EF UserStore 没有在 dbcontext 上调用 dispose,但不这样做感觉不对,我正在尝试理解他们为什么不这样做。 - Joe Audette
如果您直接注入 DbContext,则不需要将其释放。请注意,在我的答案中,我传递了一个委托 Func<AppDbContext> contextFactory,其中委托在每次调用时创建工厂的新实例。 - Tseng
关于 Select 跟踪的我的看法。如果您不想跟踪实体的集合,那么可以使用 AsNoTracking() 方法。理论上来说,它应该能够节省资源并减少查询时间,但我不能百分之百确定。 - Alex Gurskiy
显示剩余3条评论

18
注意:在 EF Core 2 中,现在有一个新方法 AddDbContextPool,它创建了一个可以重复使用的上下文池。作用域仍然相同,但实例将被“重置”并返回到池中。我原以为“重置”的开销与创建新实例的开销相同,但我想这并不是这种情况。
如果使用此方法,在控制器请求 DbContext 实例时,我们将首先检查池中是否有可用实例。一旦请求处理完成,任何实例上的状态都将被重置,并将实例本身返回到池中。
这在概念上类似于 ADO.NET 提供程序中连接池的操作,并具有节省 DbContext 实例初始化成本的优点。

https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-2.0#high-performance


2
也许是 'OnModelCreating' 函数在处理大型数据库时需要花费时间? - Simon_Weaver

-6

诚然,这是我学到的经验。而且它来自于一种懒惰的KISS信仰。我避免了其他复杂性,并解决了我的问题,即过早地EF Core DbContext处置而不关心池、范围等。我只是随意地组合了一个POC,没有考虑异步返回值,从控制器链接到存储库等,所有这些都基本上返回“void”,即字面上的单数,“Task”。这导致我的DbContext成员在较低的例程中迭代,无法解释地处置底层DbContext。我所要做的就是使每个异步方法返回一个Task<任何返回>值,一切都能正常工作。EF Core不喜欢异步void返回值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接