Entity Framework、DBContext和using()+async是什么?

25

有一件事情困扰了我很长时间关于Entity Framework。

去年我为一个客户编写了一个大型应用程序,使用EF进行开发。在开发期间一切都运行良好。

我们在八月份发布了这个系统。但是几周后,我开始在生产服务器上看到奇怪的内存泄漏。我的ASP.NET MVC 4进程在运行几天后(8 GB)占用机器的所有资源。这不是好事。我在网上搜索并发现你应该将所有EF查询和操作包含在 using()块中,以便可以处理上下文。

一天之内,我重构了所有代码,使用了using(),这解决了我的问题,自那时起,进程的内存使用率稳定。

然而,我没有一开始就包含查询的原因是,我从Visual Studio中包含的Microsoft自己的脚手架开始我的第一个控制器和存储库,它们没有将查询装入使用中,而是将DbContext作为控制器本身的实例变量。

首先:如果处理上下文非常重要(这不奇怪,dbconnection需要关闭等等),那么Microsoft可能应该在所有示例中都这样做!

现在,我已经开始了一个新的大项目,并且将我的所有学习成果放在脑后,我一直在尝试.NET 4.5和EF 6 asyncawait的新功能。EF 6.0具有所有这些异步方法(例如SaveChangesAsyncToListAsync等)。

public Task<tblLanguage> Post(tblLanguage language)
{
    using (var langRepo = new TblLanguageRepository(new Entities()))
    {
        return langRepo.Add(RequestOrganizationTypeEnum, language);
    }
}

在类 TblLanguageRepo 中:

public async Task<tblLanguage> Add(OrganizationTypeEnum requestOrganizationTypeEnum, tblLanguage language)
{
    ...
    await Context.SaveChangesAsync();
    return langaugeDb;
}

然而,当我现在将我的语句包裹在一个 using() 块中时,我会得到异常 DbContext was disposed,在查询能够返回之前已经发生了。这是预期的行为。查询运行异步,using 块在查询之前就已经结束。但是,在使用 EF 6 的异步和等待函数时,我应该如何以正确的方式处理上下文的释放?

请指导我正确的方向。

在 EF 6 中是否需要使用 using() 呢?为什么微软自己的示例从未涉及呢?如何使用异步功能并正确地处理上下文的释放?


这与之密切相关:EF数据上下文-异步/等待和多线程 - noseratio - open to work
6个回答

24

您的代码:

public Task<tblLanguage> Post(tblLanguage language)
{
    using (var langRepo = new TblLanguageRepository(new Entities()))
    {
        return langRepo.Add(RequestOrganizationTypeEnum, language);
    }
}

在返回 Task 前处理存储库的释放。如果将代码更改为 async

public async Task<tblLanguage> Post(tblLanguage language)
{
    using (var langRepo = new TblLanguageRepository(new Entities()))
    {
        return await langRepo.Add(RequestOrganizationTypeEnum, language);
    }
}

在任务完成之前,它将会处理仓库的释放工作。实际上,当您命中await时,该方法会返回一个未完成的Task(此时请注意using块仍处于“活动”状态)。然后,当langRepo.Add任务完成时,Post方法继续执行并处置langRepo。当Post方法完成时,返回的Task也会被完成。

更多信息请参阅我的async介绍


@Steven,Add是可等待的吗?我认为它只返回一个TEntity(假设langRepo是DbSet)... - danludwig
7
在这种情况下,“Add”已经在提问者的问题中给出。它是可等待的(实际上应该称为“AddAsync”)。 - Stephen Cleary

4
我会选择“每个请求一个DbContext”的方式,并在请求内重复使用DbContext。由于所有任务都应在请求结束时完成,因此您可以安全地将其销毁。

请参见:ASP.NET MVC中每个请求一个DbContext(无需IOC容器)

其他一些优点:

  • 某些实体可能已经从以前的查询中在DbContext中实现,从而节省了一些额外的查询。
  • 您不必在代码中添加所有这些额外的using语句。

1

同意@Dirk Boer的观点,即最好的管理DbContext生命周期的方法是使用IoC容器,在http请求完成时处理上下文。但是,如果这不是一个选择,你也可以像这样做:

var dbContext = new MyDbContext();
var results = await dbContext.Set<MyEntity>.ToArrayAsync();
dbContext.Dispose();
< p > using语句只是在代码块结束时处理对象释放的一种语法糖。您可以通过手动调用.Dispose来实现相同的效果,而不需要使用using块。

想一想,如果您在using块中使用await关键字,就不应该出现对象已释放的异常:

public async Task<tblLanguage> Post(tblLanguage language)
{
    using (var langRepo = new TblLanguageRepository(new Entities()))
    {
        var returnValue = langRepo.Add(RequestOrganizationTypeEnum, language);
        await langRepo.SaveChangesAsync();
        return returnValue;
    }
}

1
如果您想保持方法同步,但希望异步保存到数据库,请不要使用using语句。就像@danludwig所说,这只是一种语法糖。您可以调用SaveChangesAsync()方法,然后在任务完成后处理上下文。一种方法是这样的:
//Save asynchronously then dispose the context after
context.SaveChangesAsync().ContinueWith(c => context.Dispose());

请注意,您传递给ContinueWith()的lambda表达式也将异步执行。

1
如果您正在使用适当的n层编程模式,则您的控制器甚至不应该知道正在进行数据库请求。 这应该全部发生在您的服务层中。
有几种方法可以做到这一点。 其中之一是为每个类创建2个构造函数,一个创建上下文,另一个接受已存在的上下文。 这样,如果您已经在服务层中,就可以传递上下文,否则如果是控制器/模型调用服务层,则可以创建新的上下文。
另一种方法是创建每个方法的内部重载并在其中接受上下文。
但是,是的,您应该将它们包装在using中。
理论上,垃圾回收应该清除这些对象而无需将其包装起来,但我并不完全信任GC。

0

在我看来,这又是一次由于使用延迟加载引起的问题。当您处理完上下文后,就不能再进行延迟加载属性了,因为处理上下文会关闭与数据库服务器的底层连接。

如果您已经激活了延迟加载,并且异常发生在using范围之后,请参见https://stackoverflow.com/a/21406579/870604


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