Today I've run into a problem when creating a Web API using async ApiControllers. I'm using MongoDB and since the C# driver doesn't support async, I tried to implement it in my repository layer.
The resulting method in the Building repository looked like this:
public async Task<IEnumerable<Building>> GetAll()
{
var tcs = new TaskCompletetionSource<IEnumerable<Building>>();
await Task.Run(() => {
var c = this.MongoDbCollection.FindAll();
tcs.SetResult(c);
});
return await tcs.Task;
}
Now this works perfectly when testing the repository by itself using NUnit.
But when testing from the controller (using a HttpClient) it never proceeds to the "return" line after running tcs.SetResult(c)
. The test just keeps running until i abort it manually.
When I remove the Task.Run code and do everything synchronously then everything works as it should:
public async Task<IEnumerable<Building>> GetAll()
{
var c = this.MongoDbCollection.FindAll();
return c;
}
Does anyone have any idea why I experience different behaviors when testing the repository + database and when testing controller + repository + database?
The controller method looks like this:
(buildingRepository
is injected in the constructor using Ninject)
public async Task<HttpResponseMessage> Get()
{
var result = await this.buildingRepository.GetAll();
return Request.CreateResponse(HttpStatusCode.OK, result);
}
EDIT: Here are also the test methods. The first one is the one that's not working:
(this.client
is a HttpClient object with the accept-header set to "application/json"
)
[Test]
public void Get_WhenBuildingsExist_ShouldReturnBuilding()
{
var task = this.client.GetAsync("/api/building/");
var result = task.Result.Content.ReadAsStringAsync().Result;
var o = JsonConvert.DeserializeObject<IEnumerable<Building>>(result);
Assert.That(o.Any());
}
[Test]
public void Get_WhenBuildingsExist_ShouldReturnAtLeastOneBuilding()
{
var buildings = this.buildingRepository.GetAll().Result;
Assert.That(buildings.Any());
}
There is a post I read that explains why invoking .Results
from an asynchronous task is a bad idea but I don't have it available at the moment. Basically, you're killing the async handling by doing this. Try changing your test as follow:
[Test]
public void Get_WhenBuildingsExist_ShouldReturnBuilding()
{
var task = this.client.GetAsync("/api/building/");
var resultTask = task.Result.Content.ReadAsStringAsync();
resultTask.Wait();
var result = resultTask.Result;
var o = JsonConvert.DeserializeObject<IEnumerable<Building>>(result);
Assert.That(o.Any());
}
The best option is to upgrade to a version of NUnit that supports async unit tests, and change all Result
/Wait
calls to await
:
[Test]
public async Task Get_WhenBuildingsExist_ShouldReturnBuilding()
{
var content = await this.client.GetAsync("/api/building/");
var result = await content.ReadAsStringAsync();
var o = JsonConvert.DeserializeObject<IEnumerable<Building>>(result);
Assert.That(o.Any());
}
If this isn't possible (e.g., Xamarin still runs a very old version of NUnit as of this writing), then you can use AsyncContext
from my AsyncEx library:
[Test]
public void Get_WhenBuildingsExist_ShouldReturnBuilding()
{
var o = AsyncContext.Run(async () =>
{
var content = await this.client.GetAsync("/api/building/");
var result = await content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<IEnumerable<Building>>(result);
});
Assert.That(o.Any());
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With