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:
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.