8

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);
        }
Tom
  • 12,591
  • 13
  • 72
  • 112

3 Answers3

2

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.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
0

The below test code works just fine. Show the implementation of .WithSlug( slug )

class Program
{
    static void Main( string[] args )
    {
        using( var db = new TestEntities() )
        {
            db.EntityBs.Add( new EntityB()
                {
                    EntityBId = 78
                } );

            db.SaveChanges();

            Task.WaitAll( Test( db ) );
        }

        var input = Console.ReadLine();
    }

    static Task<List<EntityB>> GetEntityBsAsync( TestEntities db )
    {
        return db.EntityBs.ToListAsync();
    }

    static async Task Test( TestEntities db )
    {
        var a0 = await GetEntityBsAsync( db );
        var a1 = await GetEntityBsAsync( db );
    }
}
Moho
  • 15,457
  • 1
  • 30
  • 31
  • 1
    strange. Comment out all `IQueryable` method calls other than `.ToListAsync` and see if that works. if it does, start adding the other method calls back in until it breaks. – Moho Feb 21 '14 at 17:04
  • your a0 and a1 will be filled with a Task> type object and not the List that you are expecting, also since it does the exact same query both times, the second time it hits it will recognize that it already executed this request on the context and will return the previous results. – Solmead Feb 26 '15 at 20:15
-2

Your missing the async await in the function, it should be:

private async Task<List<RelatedPageModel>> GetRelatedCaseStudies(CaseStudy casestudy, bool archived)
{
return await 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();
}

Without those it will run both calls in separate threads at the same time, causing two hits to the context at the same time.

Solmead
  • 4,158
  • 2
  • 26
  • 30
  • 1
    This is entirely equivalent to the OP's code. Nothing about it is different, other than that you're needlessly spinning up a state machine that you use none of the functionality of. – Servy Feb 26 '15 at 20:19