Entity Framework异步问题 - 上下文还是查询?

8

我有一个异步问题,涉及到以下查询。我有一个单例上下文,并尝试执行以下查询:

var query = await (from parent in Context.ParentTable
                   join child in Context.ChildTable
                   on parent.ID equals child.ID
                   into allResult
                   from ResultValue in allResult.DefaultIfEmpty()
                   where ResultValue.TenantId == tenantId
                   select new Result
                   {
                      Code = parent.Code,
                      Type = parent.Type,
                      ID = ResultValue == null ? 0 : ResultValue.Id
                   }).ToListAsync();

我的单例上下文看起来像这样:

public class BaseRepository
{
    private readonly IConfigurationContextFactory configurationContextFactory;

    private IConfigurationContext context;

    protected IConfigurationContext Context
    {
        get
        {
            return context ?? (context = configurationContextFactory.Context);
        }
    }

    public BaseRepository(IConfigurationContextFactory configurationContextFactory)
    {
        this.configurationContextFactory = configurationContextFactory;
    }
}

配置上下文工厂返回的Context如下:
private ConfigurationContext Get()
{
    logger.WriteEntrySync(LogLevel.Information,
                          null != context ? "Config Context: Using existing context." : "Config Context: Wiil create new context.");
    return context ?? (context = new ConfigurationContext(connectionString));
}

在这个问题中,我遇到了以下错误:
一个异步操作还未完成,就开始在该上下文(context)上执行第二个操作。使用“await”确保任何异步操作在调用此上下文的另一个方法之前已经完成。任何实例成员都不能保证是线程安全的。

4
EF上下文作为单例是大忌。 - haim770
2个回答

22

我有一个单例上下文

这是你的问题。DbContext不是线程安全的,并且设计为一次只执行一个查询。由于你正在共享你的DbContext,你可能在尝试同时调用另一个查询,而这在DbContext术语中是不“合法”的。

你甚至可以在ToListAsync的注释中看到这一点:

不支持在同一上下文实例上进行多个活动操作。使用“await”确保在调用此上下文上的另一个方法之前完成任何异步操作。

你应该做的是不要重复使用全局单例的上下文,而是每次想要查询数据库时创建一个新的上下文。

编辑:

不要通过你的工厂方法获取单个Context,而是为每个查询分配一个新的上下文:

using (var context = new ConfigurationContext(connectionString))
{
    var query = await (from feature in context.Features
                join featureFlag in context.FeatureFlags
                on feature.FeatureId equals featureFlag.FeatureId
                into allFeatures
                from featureFlagValue in allFeatures.DefaultIfEmpty()
                where featureFlagValue.TenantId == tenantId
                select new BusinessEntities.FeatureFlag
                {
                   Code = feature.Code,
                   Type = feature.Type,
                   FeatureFlagId = featureFlagValue == null ? 0 : featureFlagValue.FeatureFlagId
                }).ToListAsync();
}

谢谢@yuval。 我已经更新了我的问题。 我不确定在linq join查询中应该如何创建新的上下文实例。还是整个linq join查询将在单个上下文中进行? 仍然有点困惑。 - Rusty
@Rusty 使用一个示例编辑了我的答案。 - Yuval Itzchakov
我应该使用上下文工厂,之前我使用了“using”语句。那么有没有一种方法可以使用上下文工厂,但它应该通过属性返回一个新的上下文? - Rusty
@Rusty,您可以每次调用工厂方法时返回一个新的实例,但我并不认为这样做有什么意义。 - Yuval Itzchakov
你需要为每个工作单位创建一个新的实例,而不是每次查询时都要创建。@Rusty,你可以将你的工厂属性更改为一个名为CreateContext的方法。 - Guillaume
@Guillaume,你能给我提供一些参考资料吗,告诉我如何做到这一点吗? - Rusty

10

DbContext应该只在一个业务事务(工作单元)中存在,不多也不少。 业务事务通常是请求、页面或表单。DbContext不是线程安全的,它保持内部实体缓存并跟踪更改,因此您不能在多个请求之间共享它。

你真的需要阅读Entity Framework文档:使用DbContext工作

我不会为每个仓库创建一个实例,因为业务事务可以操纵多个仓库。

您可以在您的仓库中注入上下文:

public class BaseRepository
{
    private IConfigurationContext context;
    public BaseRepository(IConfigurationContext context)
    {
        this.context = context;
    }
    //...
}

更改您的工厂,使其每次创建一个实例:

public interface IConfigurationContextFactory
{
    IConfigurationContext CreateContext();
}

// ...
public ConfigurationContext CreateContext()
{
    return new ConfigurationContext(connectionString);
}

然后配置您的依赖项解析器,以便每个工作单元注入一个IConfigurationContext。假设您正在使用Unity开发ASP.NET应用程序。

container.RegisterType<IConfigurationContext>(
    //Instance per http request (unit of work)
    new PerRequestLifetimeManager(),
    //Create using factory
    new InjectionFactory(c => c.Resolve<IConfigurationContextFactory>.CreateContext()));

在必要的时候,不要忘记在业务操作结束时调用SaveChangesAsync:当操作成功且修改需要持久化时。


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