2

I have an ASP Core 3.1 web project where I want to add EntityFramework Core to.
I've created a db context, model class with database operations, and I've injected this into my main class (an Azure Bot).

However, when I try to insert a record into the database, it always fails with the error

System.Threading.Tasks.TaskCanceledException: 'A task was canceled.'

This is my startup.cs:

services.AddDbContext<IVRContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection"),
                optionBuilder => optionBuilder.EnableRetryOnFailure()
            )
        );
        services.AddTransient<IVRCallModel>();

This is the function in my IVRModel that I'm calling:

 public async Task InsertCallAsync(IVRCall call)
    {
        try
        {
            await _ivrContext.Calls.AddAsync(call);
            await _ivrContext.SaveChangesAsync();
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message, ex);
        }
    }

This is how I call it:

private async Task NotificationProcessor_OnNotificationReceivedAsync(NotificationEventArgs args)
{
    this.GraphLogger.CorrelationId = args.ScenarioId;
    if (args.ResourceData is Call call)
    {
        if (call.Direction != CallDirection.Outgoing && call.ToneInfo == null)
        {
            if (args.ChangeType == ChangeType.Created && call.State == CallState.Incoming)
            {
                await SaveCall(call.Id, call.CallChainId, "Incoming");
                
                .... code removed 
            }
        }
    }
}

private async Task SaveCall(string callId, string callChainId, string callState, string redirectCallId = null)
{
    IVRCall newCall = new IVRCall();
    newCall.Id = callId;
    newCall.CallChainId = callChainId;
    newCall.TimeStamp = DateTime.Now.ToString("dd-MM-yyyy HH:mm:ss");
    newCall.State = callState;
    newCall.RedirectCallId = redirectCallId; 
    await _ivrCallModel.InsertCallAsync(newCall);
}

Edit: The 'original' NoticicationProcessor_OnNotificationReceived function, which calls the async method. (From a Microsoft sample project)

private void NotificationProcessor_OnNotificationReceived(NotificationEventArgs args)
{
    _ = NotificationProcessor_OnNotificationReceivedAsync(args).ForgetAndLogExceptionAsync(this.GraphLogger, $"Error processing notification {args.Notification.ResourceUrl} with scenario {args.ScenarioId}");
}
CJ Scholten
  • 623
  • 2
  • 13
  • 27
  • `throw new Exception(ex.Message, ex);` Why?! You've just wrapped a meaningful exception type in the base `Exception` type for no reason. Just remove the `try..catch` block, and let the exception propagate as normal. Or at least re-throw the original exception. – Richard Deeming Dec 10 '21 at 13:58
  • Can you share the stack trace of the exception? – Gabriel Luci Dec 10 '21 at 16:50
  • System.Threading.Tasks.TaskCanceledException HResult=0x8013153B Message=A task was canceled. Source=System.Private.CoreLib StackTrace: at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) That's all it shows – CJ Scholten Dec 11 '21 at 16:42

1 Answers1

4

That exception will happen when code tries to run after the response has already been sent to the client. When you're using async/await, that can happen when something async isn't being awaited somewhere.

And indeed, that's happening here:

private void NotificationProcessor_OnNotificationReceived(NotificationEventArgs args)
{
    _ = NotificationProcessor_OnNotificationReceivedAsync(args).ForgetAndLogExceptionAsync(this.GraphLogger, $"Error processing notification {args.Notification.ResourceUrl} with scenario {args.ScenarioId}");
}

When await acts on an incomplete Task, it actually returns. It returns a Task of its own so that the caller can keep track of when it completes. So when NotificationProcessor_OnNotificationReceivedAsync returns, the work isn't actually complete. And because you aren't using the Task, execution just continues and ASP.NET wraps up the request and ends the tasks for that request.

Using event handlers with async code can be tricky, especially in cases like this where you need to hold up execution until the work is done, otherwise you get unexpected results. You could wait synchronously on the async code, using something like .GetAwaiter().GetResult(), but that can have unintended consequences of its own.

I think your best bet is to just use synchronous code in this case: use .SaveChanges() instead of .SaveChangesAsync().

About your use of .AddAsync(), the documentation says:

This method is async only to allow special value generators, such as the one used by 'Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo', to access the database asynchronously. For all other cases the non async method should be used.

So you should be using just .Add() all the time anyway, even inside an async method.

The comment about your try/catch block is valid too. If you're going to catch an exception just to throw a new one using the same message, then don't catch the exception at all. Even if you need to do some logging or something before throwing the exception again, then use just throw; rather than throw new Exception(ex.Message, ex);. Throwing new exceptions changes the stack trace of where the exception happened and can make debugging difficult.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • I'm using a sample from Microsoft, and you're right, the actual NotificationProcessor is void. It then calls the async function. (See edit above for the code) – CJ Scholten Dec 11 '21 at 16:43
  • Thanks! That helps. The 'Task cancelled' error has gone. Only now I get a new error. It doesn't happen with the first db call in a thread, but with subsequent ones: System.ObjectDisposedException: 'Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. – CJ Scholten Dec 13 '21 at 12:29
  • Dependency injection creates a `DbContext` object that lives for the life of the HTTP request. Once the response is returned to the client, the scope ends and the `DbContext` is disposed. That exception happens when your code that uses your `DbContext` runs after the response is returned and the `DbContext` has already been disposed. – Gabriel Luci Dec 13 '21 at 17:08
  • Either figure out why it is running after the response has already been sent (are you using `Task.Run` somewhere?) and fix that, or, if it's ok that it's running after the response is already sent, then you can create a new scope and get a new `DbContext` inside your `InsertCallAsync` method, like this: https://stackoverflow.com/a/48368934/1202807 – Gabriel Luci Dec 13 '21 at 17:09