Update: See Part 2 of this post where I outline a further solution and the cause of the issue.
Custom controllers in Umbraco natively support async/await operations because they ultimately inherit from System.Web.Mvc.Controller which implements the IAsyncController interface. However there is some confusion in how to implement async/await in custom controllers and I personally have struggled to figure this out until recently. There are several threads which give useful but contradicting information and often don’t provide the full picture, so I thought I’d get something down in an article.
As the Umbraco documentation outlines, a custom controller should (usually) inherit from the Umbraco.Web.Mvc.RenderMvcController class. This base class providers a default Index() action that all Umbraco requests are routed through.
You can override this in order to alter or pass down your own model but you cannot make the action async since an async method has to have either a void, Task, or Task<T> return type and this would obviously change the action’s signature and not compile.
The seemingly obvious solution to this is to not include this action in the custom controller and add a custom Index() action that will take precedence over the base class' Index() action:
This is perfectly valid and will fire instead of the base class’ Index action. I use this approach very often since I can make the parameters of the action whatever I want, including a complex type. The other advantage of this approach is that I can make the action async:
But wait! If you run this it will fire but the output rendered will look like this:
System.Threading.Tasks.Task`1[System.Web.Mvc.ActionResult]
And this is where I got stuck for a very long time.
If you read the threads, you’ll learn that surface controllers (any controller that inherits from Umrbaco.Web.Mvc.SurfaceController see documentation) implicitly support async operations. For example, the following code works fine, so what gives?
My theory (and I’ll have to do a bit more rummaging to confirm it) is that the issue is caused by the way any controller implementing IRenderMvcController is instantiated when the Index action is called. Even in the hybrid framework where all the custom controllers inherit from SurfaceController, an async Index action will still output System.Threading.Tasks.Task`1[System.Web.Mvc.ActionResult].
The solution to all of this is not to call the Index() action at all. Eh? I’ll explain …
So to cover the basics and clarify, the document type of a page defines the custom controller name that the routing will instantiate, e.g. a document type with the alias NewsArticle will attempt to route to a custom controller called NewsArticleController. The template used by the page will define the action that is fired in the controller. If the template is also called NewsArticle (which is often the case), the routing will try and fire an action called NewsArticle in the NewsArticleController. If a matching action is not found, the default Index action will be fired.
So, in order to make your actions async you need to target the template’s action which take precedence over the default Index action. In a custom controller for a document type called Home, the action would be:
This will avoid the dreaded System.Threading.Tasks.Task`1[System.Web.Mvc.ActionResult] output and will function correctly. You can also extend this by passing in additional parameters as mentioned earlier: