1

So, the error is basically what is mentioned in Entity Framework,There is already an open DataReader associated with this Connection which must be closed first - Stack Overflow

In that context, I understand what's going on and the reason for error based on the explanation.

And then I recently encountered the same error after applying best practice suggested by http://www.asyncfixer.com/

The code is a custom localizer that uses SQL Database as it's store instead of resource files. In brief, it provides a string from db if it's not already cached in memory. In addition, if a string is not in db it adds it to db as well.

The code pieces in question are below (2 methods). For the error to happen the 3 changes must be made to both the methods as in comments related to async await syntax

I would like to know a bit about the internals for this exception with these changes

Method 1

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!);
}

Method 2

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();
}

Once above 3 changes are done it consistently throws this exception and if I remove these changes it works fine.. I would like to know what may be happening behind the scenes for this exception to happen

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
user2058413
  • 691
  • 3
  • 10
  • 28
  • Show at least callstack. Also there are other functions which can produce problem. – Svyatoslav Danyliv Nov 19 '21 at 15:26
  • For brevity, I avoided putting the callstack. Callstack exactly points to these lines I have mentioned. Method 1 calls Method 2 to add missing resources to db ... so basically both the methods are reading/writing the db... so high chance that is what the cause is.. but not sure why adding async/await changes throws an exception. – user2058413 Nov 20 '21 at 02:58
  • 1
    You may find this interesting: [Eliding Async and Await](https://blog.stephencleary.com/2016/12/eliding-async-await.html). – Theodor Zoulias Nov 20 '21 at 09:05

1 Answers1

2

Change #1 is irrelevant, since it's just fixing the Misuse - Longrunning Operations Under async Methods:

We noticed that developers use some potentially long running operations under async methods even though there are corresponding asynchronous versions of these methods in .NET or third-party libraries.

or in simple words, inside async method use library async methods when exist.

The change #2 and #3 though are incorrect. Looks like you are trying to follow the Misuse - Unnecessary async Methods:

There are some async methods where there is no need to use async/await. Adding the async modifier comes at a price: the compiler generates some code in every async method.

However, your method needs async/await, because it contains a disposable scope which has to be kept until the async operation of the scoped service (in this case db context) completes. Otherwise the scope exits, the db context is disposed, and no one can say what happens in case SaveChangesAsync is pending at the time of disposal - you can get the error in question, or any other, it's simply unexpected and the behavior is undefined.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343