jjk_charles
jjk_charles

Reputation: 1290

DbContext getting disposed too early within Async method

When converting an existing synchronous method to async, I accidentally used "async void" on one of the methods, which resulted in some unexpected behavior.

Below is a simplified example of the kind of change I had actually performed,

public IActionResult Index()
{
    var vm = new ViewModel();
    try
    {
        var max = 0;

        if (_dbContext.OnlyTable.Any())
        {
            max = _dbContext.OnlyTable.Max(x => x.SomeColumn);
        }

        _dbContext.Add(new TestTable() { SomeColumn = max + 1 });
        _dbContext.SaveChanges();

        MakePostCallAsync("http:\\google.com", vm);

        if (!string.IsNullOrEmpty(vm.TextToDisplay))
        {
            vm.TextToDisplay = "I have inserted the value " + newmax + " into table (-1 means error)";
        }
        else
        {
            vm.TextToDisplay = "Errored!";
        }


    }
    catch (Exception ex)
    {
        vm.TextToDisplay = "I encountered error message - \"" + ex.Message + "\"";
    }
    return View("Index", vm);
}

private async void MakePostCallAsync(string url, ViewModel vm)
{
    var httpClient = new HttpClient();

    var httpResponse = await httpClient.PostAsync("http://google.com", null).ConfigureAwait(true);

    newmax = _dbContext.OnlyTable.Max(x => x.SomeColumn);
}

The issue is that, the MakePostCallAsync() method, when trying to query the database using DbContext, throws an exception saying DbContext is already disposed.

_dbContext in the example is injected using ASP .Net Core's DI (through AddDbContext() extension) with its default scope (Scoped).

I fail to and need help understand the following,

Repro is available in https://github.com/jjkcharles/SampleAsync

Upvotes: 5

Views: 8714

Answers (3)

Harald Coppoolse
Harald Coppoolse

Reputation: 30454

It seems to me you have some trouble understanding how to use async-await, because I see several major errors in your program.

This article, written by the ever so helpful Stephen Cleary helped me to understand how to use it properly.

Every function that wants to use async-await has to return Task instead of void and Task<TResult> instead of TResult. The only exception to this rule are event handlers that are not interested in the result of the actions.

So first change Index such that it returns a Task<IActionResult>. Once you've done this, your compiler will probably warn you that you forgot to await somewhere inside your Index function.

If you call an async function, you don't have to wait for it to finish, you can do other useful stuff, that will be performed whenever the async function has to await for something. But before you can use any result of the async function you have to await until it is ready:

var taskMakePostCall = MakePostCallAsync(...)
// if you have something useful to do, don't await
DoSomethingUseful(...);
// now you need the result of MakePostCallAsync, await until it is finished
await taskMakePostCall;
// now you  can use the results.

If your MakePostCallAsync would have returned something, for instance an int, the return value would have been Task<int> and your code would have been:

Task<int> taskMakePostCall = MakePostCallAsync(...)
DoSomethingUseful(...);
int result = await taskMakePostCall;

If you don't have something useful to do, just await immediately:

int result = await MakePostCallAsync(...);

The reason for your exception is that your MakePostCallAsync is not finished completely before you somewhere Dispose your dbContext, probably via a using statement. After adding this await before returning you are certain that MakePostCallAsync is completely finished before returning Index()

Upvotes: 2

Scott Chamberlain
Scott Chamberlain

Reputation: 127543

You should never use async void unless you are writing a event handler. If you want to use async/await you need to go all the way up the call stack till you get to return a Task<IActionResult>

public async Task<IActionResult> Index()
{
    var vm = new ViewModel();
    try
    {
        var max = 0;

        if (_dbContext.OnlyTable.Any())
        {
            max = _dbContext.OnlyTable.Max(x => x.SomeColumn);
        }

        _dbContext.Add(new TestTable() { SomeColumn = max + 1 });
        _dbContext.SaveChanges();

        await MakePostCallAsync("http:\\google.com", vm);

        if (!string.IsNullOrEmpty(vm.TextToDisplay))
        {
            vm.TextToDisplay = "I have inserted the value " + newmax + " into table (-1 means error)";
        }
        else
        {
            vm.TextToDisplay = "Errored!";
        }


    }
    catch (Exception ex)
    {
        vm.TextToDisplay = "I encountered error message - \"" + ex.Message + "\"";
    }
    return View("Index", vm);
}

private async Task MakePostCallAsync(string url, ViewModel vm)
{
    var httpClient = new HttpClient();

    var httpResponse = await httpClient.PostAsync("http://google.com", null).ConfigureAwait(true);

    newmax = _dbContext.OnlyTable.Max(x => x.SomeColumn);
}

Upvotes: 8

Jason Boyd
Jason Boyd

Reputation: 7019

In your example you are not awaiting your call to MakePostCallAsync("http:\\google.com", vm). This means the request continues execution immediately and it eventually executes the code that disposes of your _dbContext, presumably while MakePostCallAsync is still waiting for the HTTP client to return a response. Once the HTTP client does return a response your MakePostCallAsync tries to call newmax = _dbContext.OnlyTable.Max(x => x.SomeColumn) but your request has already been handled and your DB context disposed of by then.

Upvotes: 0

Related Questions