1

I'm debugging some code and noticed that occasionally, when it accesses the HttpContext.Current it is null and resorts to a fallback that was put in to handle that. The code is an async Web API method that calls down through the application layers and ultimately executes an async Entity Framework 6 query. (Code below) None of the layers in between do anything other than await [method call] - no .ConfigureAwait(false) or anything else.

The project has a System.Data.Entity.Infrastructure.IDbConnectionInterceptor which sets up the SQL session (for SQL RLS) in the Opened method. It uses an injected dependency which, in this case, gets the an ID it needs from the HttpContext.Current.Items collection. When I'm debugging, 95% of the time it works every time, but once in a while I found that HttpContext.Current and SynchronizationContext.Current are both null.

Looking at the Call Stack window, I can see they are arriving at the IDbConnectionInterceptor.Opened method in different ways. The successful version leads back down to my calling code in the Web API controller, but the version where it is null, leads back down to native code. I thought well maybe when it's not null, it's not even executing on a different thread, but Open does execute on a different thread from the original in both cases. My project is targeting .NET Framework 4.8 and referencing the Microsoft.AspNet.WebApi v5.2.3 nuget package. It has <httpRuntime targetFramework="4.7.2" /> under <system.web> in the config file (which I'm just now noticing does not match the 4.8 of the framework). My understanding is that as of .NET Framework 4.5, the context should flow across async calls so it seems like something is preventing that, or somehow Opened is getting queued on a thread that's not using the async/await model. So can someone help me understand the call stack of the failed request, why it might be different from the one that succeeds, and hopefully how that might explain the missing context?

Web API method:

    [HttpGet]
    [Infrastructure.Filters.AjaxOnly]
    [Route("event/month/list/{year}")]
    public async Task<IHttpActionResult> GetRoster___EventMonthItems(int year)
    {
        try
        {
            HttpContext.Current.SetCallContextFacilityID();    //This extension method sets the mentioned fallback value for when HttpContext.Current is null
            List<RosterDayListItem> data = await _roster___Mapper.GetRoster___EventMonthItems(year);
            return Ok(data);
        }
        catch (Exception ex)
        {
            Logging.DefaultLogger.Error(ex, "An error occurred while loading report categories.");
            return BadRequest(ex.Message);
        }
    }

EF6 Query

    public async Task<List<Roster___EventListItem>> GetRoster___EventListItems(int year, int month)
    {
        using (var dbContextScope = _dbContextFactory.Create())
        {
            var context = GetContext(dbContextScope);

            var result = await context.DropInEvents
                .Where(w => w.EventDate.Year == year && w.EventDate.Month == month && w.IsDeleted == false)
                .Select(d => new Roster___EventListItem
                {
                    ID = d.ID,
                    EventDate = d.EventDate,
                    EventTime = d.StartTime,
                    Year = d.EventDate.Year
                })
                .OrderBy(f => f.EventDate).ThenBy(f => f.EventTime)
                .ThenByDescending(f => f.EventDate)
                .ToListAsync();

            return result;
        }
    }

Successful call stack:

Successful Call Stack

Call Stack with null contexts: Call Stack with null contexts


Update

Grasping at straws but after thinking about it for a while, it seemed like maybe something inside EF 6 is perhaps queueing the call to IDbConnectionInterceptor.Opened on a thread in a way that loses the SynchronizationContext. So I went looking through the EF source following my successful stack trace and it looks like the call to Opened is initiated here in InternalDispatcher.DispatchAsync<TTarget, TInterceptionContext> line 257. I'm not sure how it would explain the intermittency of my problem, but might it have something to do with Task.ContinueWith that is being used here? Interestingly I found this other question related to both Task.ContinueWith that method and a SynchronizationContext being lost. Then i found this question where the answer says it will continue with a ThreadPool thread which will not have an associated SyncrhonizationContext unless one is explicitly specified. So this sounds like what I came looking for, but I'm not sure whether the TaskContinuationOptions.ExecuteSynchronously option used changes anything, and if this is the culprit, I don't yet understand why my HttpContext is available most of the time.

xr280xr
  • 12,621
  • 7
  • 81
  • 125
  • I don't see any `Task.ContinueWith`, but don't mix it with `async` code. It won't post back to the captured `SynchronizationContext`. – Paulo Morgado Feb 13 '21 at 19:14
  • @PauloMorgado the `Task.ContinueWith` is in the linked Entity Framework code line 139. So it sounds like they’ve caused the IDbContextInterceptor to run in it’s own context. Intentional or bug? – xr280xr Feb 13 '21 at 19:58
  • Can you provide a [How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example)? – Paulo Morgado Feb 14 '21 at 17:47
  • @PauloMorgado I have a VS project zipped, but not sure how to share it. I can use DropBox or Drive but won't be maintaining it there long-term. – xr280xr Feb 17 '21 at 05:44
  • Please, change the images to text and provide a simple repro of the problem. – Paulo Morgado Feb 17 '21 at 10:21
  • @PauloMorgado Here is a link to a reproducible example. It takes some patience for it to occur. When testing if the project unzips and runs correctly, I clicked the button while the Seed.sql file was still executing and experienced the exception 5 times. Maybe luck. https://drive.google.com/file/d/17fFYnk3lHKKZB3ql6kBRSyeIRLJkzr6c/view?usp=sharing. – xr280xr Mar 13 '21 at 01:58

0 Answers0