Reputation: 1290
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
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
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
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