Search code examples
datetime.net-6.0.net-core-3.1

_dateData is null inside an awaited asynch Task


I am stepping through a .NET Core 3.1 service and the code is blowing up with a null exception on the following line:

DateTime maxOwnerValidDate = DateTime.UtcNow;

The exception is:

System.NullReferenceException: 'Object reference not set to an instance of an object.'

maxOwnerValidDate._dateData was null.

After researching _dateData, I found that this is something introduced by .NET 6, 7. I did recently update the environment on this machine to .NET 6. Interestingly, the above statement does not throw any exceptions if placed elsewhere in the code. It is only failing when the statement exists inside of an asynch Task.

At this point, I am not interested in upgrading this .NET Core project to .NET6/7 (yet). I'm just debugging something else.

Does anyone have any idea what I'm doing wrong? How can I get the code to continue to function here without rolling back the .NET6 install or working from another environment?

The inner workings of the new DateTime object and the new private ulong _dateData property and why it would be null inside a new DateTime object declared inside an asnych Task are so far beyond my skill set that I have nothing to lean on.

VS break on exception screen cap

ADDITIONS:

I have found that it is in fact related to pushing through the code by manually moving the step pointer in Visual Studio. In some cases there is no impact. Here there is a very repeatable, very specific, very odd failure.

Sample code to recreate:

public async Task ProcessWorkflow(List<Object> steps)
{
    foreach (Object step in steps)
    {
        bool pushingForward = true; //<<< Breakpoint 1 here <<<
        DateTime maxOwnerValidDate = DateTime.UtcNow; //Manually push through to here and step forward (F10)
        bool success = await ProcessWorkflowSteps(step); //Success: maxOwnerValidDate populated, no exception thrown.
    }
}

public async Task<bool> ProcessWorkflowSteps(Object step)
{
    List<int> someStuff = new List<int>() { 1, 2, 4, 5 };
    bool thisShouldRun = false;
    if (thisShouldRun)  //<<< Breakpoint 2 here <<<
    {                   //Manually push into if clause here and step forward (F10)
        DateTime maxOwnerValidDate = DateTime.UtcNow; // Step forward here (F10). Exception thrown here.
    // For some reason, if the next line is not present, the above statement succeeds without exception.
    List<int> Ids = someStuff.Where(stuff => maxOwnerValidDate <= DateTime.UtcNow).ToList();
        await Task.Delay(1000);
    }
    return true;
}

Solution

  • Neither maxOwnerValidDate(its struct) nor _dateData(its primitive) can be null. So you misinterpreting something. Btw _dateData field exists since NET Framework 2.0.

    Based on updated info you skipping closure object initialization (it captures state for lambdas or async functions).

    Here is decompiled code sharpLab

    List<int> list = new List<int>();
    list.Add(1);
    list.Add(2);
    list.Add(4);
    list.Add(5);
    <someStuff>5__1 = list;
    <thisShouldRun>5__2 = false;
    if (!<thisShouldRun>5__2) //<<< Breakpoint 2 here <<<
    {
        goto IL_0105;
    }
    <>8__3 = new <>c__DisplayClass0_0(); // <<< ! this skipped !
    <>8__3.maxOwnerValidDate = DateTime.UtcNow;  // Step forward here (F10). Exception thrown here.
    <Ids>5__4 = Enumerable.ToList(Enumerable.Where(<someStuff>5__1, new Func<int, bool>(<>8__3.<<Main>$>b__1)));
    awaiter = Task.Delay(1000).GetAwaiter();
    

    In your first example maxOwnerValidDate is not captured, so it's not promoted to closure.