In this recipe, we make one of the handlers in the TodosModule
project asynchronous. Doing so is relevant for any handler that makes the current thread wait for something during processing—typically some sort of I/O. This could be a call to an external service, file access, or, as in our case, access to a database. Think about the deployment to AppHarbor we did in the Hosting Nancy on the Cloud (Intermediate) recipe. The Nancy application is on AppHarbor, but the MongoDB database is on another service. This means that whenever our handlers' call the database, it incurs a remote call, which in turn means that the thread will be waiting a while for the response. A lot of the times, we don't notice this wait, but nonetheless it means that we have a thread sitting around doing nothing for a while. If we make the database call asynchronously, the thread could do something else meanwhile. If we make the whole handler asynchronous, the thread could even process another request while the database call is pending.
At the time of writing, this feature of Nancy has not reached the final release, so we will be switching over to using a beta release. This means that the code shown here may well need to be changed if you are running on a later and final version of the Nancy async features.
As usual, just grab your copy of the code from the last recipe, and you are ready to start this one.
In order to install the Nancy async beta with NuGet, we need to add the feed from https://www.myget.org/F/nancyasync/ as a source in the package manager in Visual Studio.
Next, update the Nancy NuGet package to the newest beta version. This will add async support to Nancy. In order to update to a beta version, include the
IncludePrerelease
option in the NuGet command:PM> Update Nancy –IncludePrerelease
Run all tests to see if updating the Nancy package had any negative effects. The
RequestLoggingTests.Should_log_error_on_failing_request
test will fail because the error pipeline is now async. Due to the way in which .NET handles propagating exceptions in async code,FormatException
we are expecting to log is wrapped insideAggregateExcption
. We can get the error logging we want by changing just one line in the bootstrapper as follows:private void LogUnhandledExceptions(IPipelines pipelines) { pipelines.OnError.AddItemToStartOfPipeline((ctx, err) => { log.ErrorException(string.Format("Request {0} \"{1}\" failed. Exception: {2}", ctx.Request.Method, ctx.Request.Path, err.ToString()), err); return null; }); }
We are now ready to take advantage of async in our route handlers, so we go into the
TodosModule
project and change the handler for"/"
to the following:Get["/", true] = async (_, __) => { var allTodos = await todoStore.GetAll(); return Negotiate .WithModel(allTodos.Where(todo => todo.userName == Context.CurrentUser.UserName).ToArray()) .WithView("Todos"); };
We changed the signature of the delegate handling the route and also added an extra parameter to the route. We will get back to these changes in the following How it works… section. For now, we need to compile the code again. That is, we need to make the
todosStore.GetAll()
call return aTask
. TheIDataStore
interface becomes as follows:public interface IDataStore { Task<IEnumerable<Todo>> GetAll(); long Count { get; } bool TryAdd(Todo todo); bool TryRmove(int id, string userName); bool TryUpdate(Todo todo, string userName); }
As an exercise, you now have to update your implementation of
IDataStore
accordingly. The updated version of theMongoDataStore
class is in the code download for this recipe. Furthermore, you will need to update a few tests that fake out the call as we just made async. In each of these tests, the fake return values have to be wrapped in theTask
objects and these tasks need to be started. As an example, look at the following test fromDataStoreTests
:[Fact] public void Should_remove_deleted_todo_from_datastore() { var returnValue = new Task<IEnumerable<Todo>>(() => new [] { new Todo { id = 1 }, new Todo { id = 2 } }); returnValue.Start(); A.CallTo(() => fakeDataStore.GetAll()) .Returns(returnValue); sut.Delete("/todos/1"); A.CallTo(() => fakeDataStore.TryRmove(1, A<string>._)).MustHaveHappened(); }
When you have made similar changes to all tests, they should all pass again, and the application should work just like it used to except now one of our handlers is asynchronous.
Let's take another look at the handler for GET "/todos/
:
Get["/", true] = async (_, __) => { var allTodos = await todoStore.GetAll(); return Negotiate .WithModel(allTodos.Where(todo => todo.userName == Context.CurrentUser.UserName).ToArray()) .WithView("Todos"); };
The first thing to notice is that the signature of the handler changed from a lambda taking one argument to a lambda taking two arguments. The first argument is the same dynamic parameter argument as before where we made the handler async. The second argument is a cancellation token. The cancellation token is of the System.Threading.CancellationToken
type and is a standard part of programming with .NET Task<T>
objects. The cancellation token is used to propagate notifications to tasks if they are to be cancelled.
The second thing to notice is that there are now two arguments to the indexer on Get
. The first argument is the route as usual, but the second argument is new. The second argument is either a Boolean expression or a Func<NancyContext, bool>
argument. In both the cases, the second argument tells Nancy whether the handler should be executed asynchronously or not. Using a Func<NancyContext, bool>
argument allows your code to inspect the NancyContext for each individual request before deciding whether or not to handle it asynchronously, giving your application code quite fine-grained control. In our case, we have simply decided upfront to always do async handling.
Handlers for other HTTP methods apart from GET
can also be asynchronous and so can Before
, After
, and OnError
hooks. Each of these is made async in a similar fashion to how our handler for GET "/todos/"
was made async. All in all, it is straightforward to make your Nancy applications completely async if you want and if your application logic does not rely on being executed synchronously.