Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I move async query into method?

The following code works

[Route("case-studies/{slug}")]
public async Task<ActionResult> Details(string slug)
{
    var item = await Db.Pages.OfType<CaseStudy>()
             .WithSlug(slug)
             .FirstOrDefaultAsync();

    if (item == null)
    {
        return HttpNotFound();
    }

    var related = await Db.Pages.OfType<CaseStudy>()
           .Where(r => r.Client == item.Client && r.Id != item.Id)
           .Where(r => !r.IsArchived)
           .Include(r => r.Media)
           .Take(3)
           .Project()
           .To<RelatedPageModel>()
           .ToListAsync();

    var archived = await Db.Pages.OfType<CaseStudy>()
            .Where(r => r.Client == item.Client && r.Id != item.Id)
            .Where(r => r.IsArchived)
            .Take(3)
            .Project()
            .To<RelatedPageModel>()
            .ToListAsync();

    ViewData.Model = new DetailPageModel<CaseStudy>()
    {
        Item = item,
        RelatedItems = related,
        ArchivedItems = archived
    };

    return View();
}

However when I try to refactor the async method calls as follows

[Route("case-studies/{slug}")]
public async Task<ActionResult> Details(string slug)
{
    var item = await Db.Pages.OfType<CaseStudy>()
             .WithSlug(slug)
             .FirstOrDefaultAsync();

    if (item == null)
    {
        return HttpNotFound();
    }       

    var related = await GetRelatedCaseStudies(item, false);
    var archived = await GetRelatedCaseStudies(item, true);

    ViewData.Model = new DetailPageModel<CaseStudy>()
    {
        Item = item,
        RelatedItems = related,
        ArchivedItems = archived
    };

    return View();
}


private Task<List<RelatedPageModel>> GetRelatedCaseStudies(CaseStudy casestudy, bool archived)
{
    return Db.Pages.OfType<CaseStudy>()
            .Where(r => r.Client == casestudy.Client && r.Id != casestudy.Id)
            .Where(x => x.IsArchived == archived)
            .Include(r => r.Media)
            .Take(3)
            .Project().To<RelatedPageModel>()
            .ToListAsync();
}

It fails giving me the following error

A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.

Why is this? How can I make this work?

Update:

Db is declared in the base controller as follows

private WebSiteDb db;

protected WebSiteDb Db
{
    get
    {
        LazyInitializer.EnsureInitialized(ref db, () => new WebSiteDb());

        return db;
    }
}

WebSiteDb extends DbContext as follows

   [DbConfigurationType(typeof(DbConfig))]
    public class WebSiteDb : DbContext
    {
        static WebSiteDb() {
            Database.SetInitializer<WebSiteDb>(new WebSiteDbInitializer());
        }
        public IDbSet<Page> Pages { get; set; }
        public IDbSet<Media> Media { get; set; }
        ...some missing sets

        public WebSiteDb() : base("MyDatabaseName") { }
     }

If I await inside the method the error is thrown from inside the method instead

WithSlug() is as follows

public static IQueryable<T> WithSlug<T>(this IQueryable<T> pages, string slug) where T : Page
        {
            return pages.Where(p => p.Slug == slug);
        }
like image 611
Tom Avatar asked Feb 21 '14 16:02

Tom


1 Answers

Try your code with the latest EF 6.1.0 Beta. The current EF6 definition of thread safety is a bit vague:

Thread Safety

While thread safety would make async more useful it is an orthogonal feature. It is unclear that we could ever implement support for it in the most general case, given that EF interacts with a graph composed of user code to maintain state and there aren't easy ways to ensure that this code is also thread safe.

For the moment, EF will detect if the developer attempts to execute two async operations at one time and throw.

It doesn't look like your code executes more than two async operations at the same time, but in ASP.NET a thread switch may and does take place between await continuations. In theory, this scenario should still be supported by EF6. However, to eliminate a possibility of an EF6 bug caused by the lack of thread affinity in ASP.NET, your could try my ThreadWithAffinityContext from the related question, like this:

public async Task<ActionResult> Details(string slug)
{
    Func<Task<ActionResult>> doAsync = async () =>
    {
        var item = await Db.Pages.OfType<CaseStudy>()
                 .WithSlug(slug)
                 .FirstOrDefaultAsync();

        if (item == null)
        {
            return HttpNotFound();
        }

        var related = await Db.Pages.OfType<CaseStudy>()
               .Where(r => r.Client == item.Client && r.Id != item.Id)
               .Where(r => !r.IsArchived)
               .Include(r => r.Media)
               .Take(3)
               .Project()
               .To<RelatedPageModel>()
               .ToListAsync();

        var archived = await Db.Pages.OfType<CaseStudy>()
                .Where(r => r.Client == item.Client && r.Id != item.Id)
                .Where(r => r.IsArchived)
                .Take(3)
                .Project()
                .To<RelatedPageModel>()
                .ToListAsync();

        ViewData.Model = new DetailPageModel<CaseStudy>()
        {
            Item = item,
            RelatedItems = related,
            ArchivedItems = archived
        };

        return View();
    };

    using (var staThread = new Noseratio.ThreadAffinity.ThreadWithAffinityContext(
        staThread: false, pumpMessages: false))
    {
        return await staThread.Run(() => doAsync(), CancellationToken.None);
    }
}

Note, this is not a production solution, but it might help to spot a bug in EF6. If there's a bug, you may consider using another helper class, ThreadAffinityTaskScheduler, until the bug is fixed in the future EF releases. ThreadAffinityTaskScheduler runs a pool of ThreadWithAffinityContext threads and thus should scale better than the above code. The linked question contains an example of use.

like image 185
noseratio Avatar answered Oct 22 '22 06:10

noseratio