使用 Async Await 模式的 EF 核心已经抛出打开的 DataReader 错误

Posted

技术标签:

【中文标题】使用 Async Await 模式的 EF 核心已经抛出打开的 DataReader 错误【英文标题】:EF core using Async Await pattern throws already an open DataReader error 【发布时间】:2021-11-19 13:10:31 【问题描述】:

所以,错误基本上是Entity Framework,There is already an open DataReader associated with this Connection which must be closed first - Stack Overflow中提到的

在这种情况下,我根据解释了解发生了什么以及错误原因。

然后我最近在应用http://www.asyncfixer.com/建议的最佳实践后遇到了同样的错误

代码是一个自定义本地化程序,它使用 SQL 数据库而不是资源文件作为存储。简而言之,如果它尚未缓存在内存中,它会提供来自 db 的字符串。此外,如果一个字符串不在 db 中,它也会将它添加到 db 中。

有问题的代码片段如下(2 种方法)。要发生错误,必须对与异步等待语法相关的 cmets 中的两种方法进行 3 处更改

我想通过这些更改了解有关此异常的内部原理

方法一

public async Task<LocalizedString> GetResourceAsync(string resourceKey, CultureInfo culture, string location = "shared")


    var tenant = ((MultiTenantContextAccessor<AppTenant>)_serviceProvider.GetRequiredService(typeof(IMultiTenantContextAccessor<AppTenant>))).MultiTenantContext.TenantInfo;

    if (string.IsNullOrWhiteSpace(resourceKey))
        throw new ArgumentNullException(nameof(resourceKey));

    var cacheKey = $"location.resourceKey";
    if (!_cacheProvider.TryGetValue(tenant.Id, cacheKey, culture, out var value))
    
        using (var scope = _serviceProvider.GetScopedService(out T context))
        
            var item = context 
                .Set<LocalizationResource>()
                .SelectMany(r => r.Translations)
                .Where(t => t.Resource.ResourceKey == resourceKey
                            && t.Resource.Module == location
                            && t.Language == culture.Name)
                .Select(p => new
                
                    p.LocalizedValue
                )
                .SingleOrDefault();

//change 1) change above to use **await context** ... **SingleOrDefaultAsync()**


            if (item == null)
                if (_settings.CreateMissingTranslationsIfNotFound)
                    await AddMissingResourceKeysAsync(resourceKey, location); //AddMissingResourceKeys(resourceKey);

            value = item?.LocalizedValue ?? string.Empty;

            if (string.IsNullOrWhiteSpace(value))
                switch (_settings.FallBackBehavior)
                
                    case FallBackBehaviorEnum.KeyName:
                        value = resourceKey;
                        break;

                    case FallBackBehaviorEnum.DefaultCulture:
                        if (culture.Name != DefaultCulture.Name)
                            return await GetResourceAsync(resourceKey, DefaultCulture,location);
                        break;
                

        

        _cacheProvider.Set(tenant.Id, cacheKey, culture, value);
    

    return new LocalizedString(resourceKey, value!);

方法二

private async Task AddMissingResourceKeysAsync(string resourceKey, string location="shared")

////Change 2): remove async from method signature to 
////private Task AddMissingResourceKeysAsync(string resourceKey, string location="shared")


    var modificationDate = DateTime.UtcNow;

    //resourceKey = $"location.resourceKey";

    using var scope = _serviceProvider.GetScopedService(out T context);
    var resource = context
        .Set<LocalizationResource>()
        .Include(t => t.Translations)
        .SingleOrDefault(r => r.ResourceKey == resourceKey && r.Module==location);

    if (resource == null)
    
        resource = new LocalizationResource
        
            Module = location,
            ResourceKey = resourceKey,
            //Modified = modificationDate,
            Translations = new List<LocalizationResourceTranslation>()
        ;

        context.Add(resource);
    

    _requestLocalizationSettings.SupportedCultures.ToList()
        .ForEach(culture =>
        
            if (resource.Translations.All(t => t.Language != culture.Name))
            
                //resource.Modified = modificationDate;
                resource.Translations.Add(new LocalizationResourceTranslation
                
                    Language = culture.Name
                    //, Modified = modificationDate
                );
            
        );

    await context.SaveChangesAsync();
////change 3) change above to return context.SaveChangesAsync();

一旦完成上述 3 项更改,它就会始终抛出此异常,如果我删除这些更改,它会正常工作。我想知道发生此异常的幕后可能发生了什么

【问题讨论】:

至少显示调用堆栈。还有其他功能会产生问题。 为简洁起见,我避免放置调用堆栈。 Callstack 正是指向我提到的这些行。方法 1 调用方法 2 将缺少的资源添加到 db ......所以基本上这两种方法都在读取/写入 db ......所以很有可能这就是原因......但不确定为什么添加 async/await 更改会引发例外。 你可能会觉得这很有趣:Eliding Async and Await. 【参考方案1】:

更改 #1 无关紧要,因为它只是修复了Misuse - Longrunning Operations Under async Methods:

我们注意到开发人员在异步方法下使用了一些可能长时间运行的操作,即使 .NET 或第三方库中存在这些方法的相应异步版本。

或者简单来说,在异步方法内部使用库异步方法(如果存在)。

更改#2 和#3 不正确。看起来您正在尝试关注Misuse - Unnecessary async Methods:

有些异步方法不需要使用 async/await。添加 async 修饰符是有代价的:编译器会在每个异步方法中生成一些代码。

但是,您的方法需要 async/await,因为它包含一个一次性作用域,该作用域必须保留到作用域服务(在本例中为 db 上下文)的异步操作完成为止。否则,范围退出,数据库上下文被释放,没有人能说如果SaveChangesAsync 在释放时处于未决状态会发生什么 - 你可能会得到有问题的错误,或者任何其他错误,这只是出乎意料的,行为是未定义。

【讨论】:

以上是关于使用 Async Await 模式的 EF 核心已经抛出打开的 DataReader 错误的主要内容,如果未能解决你的问题,请参考以下文章

Swift 并行编程现状和展望 - async/await 和参与者模式

在 web api 控制器(.net 核心)中使用 async/await 或任务

使用 async/await 时,Firebase Cloud Firestore 查询返回 Promise <pending> 而不是已完成

在猫鼬模式子文档中使用 async/await [重复]

在非 UI 线程上运行 async/await 方法[关闭]

Async和Await 异步方法