Using C# - ASP.NET MVC 4, I can define an async controller action like:
public async Task<ActionResult> IndexWorks()
{
var data = await DownloadAsync("http://stackoverflow.com");
return Content(data);
}
Is there a way to do something similar, using F#?
I'm aware of that I could use the AsyncManager
approach. I'm also aware of that @Tomas Petricek have made a quite neat AsyncActionBuilder
, but it just feels like a lot of boilerplate, compared to the C# approach.
async/await uses Tasks, so you'll need to convert back and forth between Task object and F# Async objects. To convert from Task to Async, use Async.AwaitTask
. To do the opposite use Async.StartAsTask
. Your example becomes:
member x.IndexWorks() =
async {
let! data = Async.AwaitTask (DownloadAsync "http://stackoverflow.com")
return x.Content(data)
} |> Async.StartAsTask
Alternatively, instead of using the async
computation expression, you can use a computation expression that works for Tasks out of the box. There's one in FSharpx:
let task = FSharpx.Task.TaskBuilder()
(...)
member x.IndexWorks() = task {
let! data = DownloadAsync "http://stackoverflow.com"
return x.Content(data)
}
It actually seems like a fellow programmer, Dmitry Morozov have made such thing possible. He have made a custom AsyncWorkflowController
that makes it possible to return Async<ActionResult>
from an ActionResult
. The code for the AsyncWorkFlowController
can be found at http://fssnip.net/5q.
However, his implementation makes it very difficult to debug, due to the fact that the stack trace wont be preserved when rethrowen in the custom controller. Therefore I've made a little change to make this possible:
member actionDesc.EndExecute(asyncResult) =
match endAsync'.Value(asyncResult) with
| Choice1Of2 value -> box value
| Choice2Of2 why ->
// Preserve the stack trace, when rethrow
ExceptionDispatchInfo.Capture(why).Throw()
obj() (* Satisfy return value *) } } }
Also I've changed the following line: new ReflectedControllerDescriptor(controllerType)
,
to new ReflectedAsyncControllerDescriptor(controllerType)
- However this change is purely optional, as it wont make any difference. I just found it more logical to use the Async
one.
The full code would then be:
open System
open System.Web.Mvc
open System.Web.Mvc.Async
open System.Runtime.ExceptionServices
open Unchecked
type AsyncWorkflowController() =
inherit AsyncController()
override __.CreateActionInvoker() =
upcast { new AsyncControllerActionInvoker() with
member __.GetControllerDescriptor(controllerContext) =
let controllerType = controllerContext.Controller.GetType()
upcast { new ReflectedAsyncControllerDescriptor(controllerType) with
member ctrlDesc.FindAction(controllerContext, actionName) =
let forwarder = base.FindAction(controllerContext, actionName) :?> ReflectedActionDescriptor
if(forwarder = null || forwarder.MethodInfo.ReturnType <> typeof<Async<ActionResult>>) then
upcast forwarder
else
let endAsync' = ref (defaultof<IAsyncResult -> Choice<ActionResult, exn>>)
upcast { new AsyncActionDescriptor() with
member actionDesc.ActionName = forwarder.ActionName
member actionDesc.ControllerDescriptor = upcast ctrlDesc
member actionDesc.GetParameters() = forwarder.GetParameters()
member actionDesc.BeginExecute(controllerContext, parameters, callback, state) =
let asyncWorkflow =
forwarder.Execute(controllerContext, parameters) :?> Async<ActionResult>
|> Async.Catch
let beginAsync, endAsync, _ = Async.AsBeginEnd(fun () -> asyncWorkflow)
endAsync' := endAsync
beginAsync((), callback, state)
member actionDesc.EndExecute(asyncResult) =
match endAsync'.Value(asyncResult) with
| Choice1Of2 value -> box value
| Choice2Of2 why ->
// Preserve the stack trace, when rethrow
ExceptionDispatchInfo.Capture(why).Throw()
obj() (* Satisfy return value *) } } }
Usage:
type TestController() =
inherit AsyncWorkflowController()
member x.IndexWorks() = async {
let startThread = Thread.CurrentThread.ManagedThreadId
let! data = asyncDownload "http://stackoverflow.com"
let endThread = Thread.CurrentThread.ManagaedThreadId
return ContentResult(Content = "Start = %i | End = %i" startThread endThread) :> ActionResult }
And to confirm that it actually does everything async, and is not blocking any thread from the ASP.NET Pool, use:
member x.IndexWorks() = async {
let startThread = Thread.CurrentThread.ManagedThreadId
let! data = asyncDownload "http://stackoverflow.com"
let endThread = Thread.CurrentThread.ManagaedThreadId
return ContentResult(Content = "Start = %i | End = %i" startThread endThread) :> ActionResult }
The start and end thread will differ, hence the start thread was put back into the pool, and a new was returned when the async operation had completed.
I think there might be a bunch of people trying to do something like
type SomeController() =
inherit ApiController()
member x.Get() =
let data = Download("http://stackoverflow.com")
x.Ok(data) :> IHttpActionResult // Using built in Ok, BadRequest, etc.
Where type Get() = unit -> Task<IHttpActionResult>
as expected from a C# WebApi Controller
If you try to do it as the accepted answer suggests (while trying to use the built-in Ok
, BadRequest
, etc. methods) you run into
can't access protected members from within a lambda
To solve this I used the ExtensionMethods directly rather than try to do contort between async {}
and Task
that MVC is expecting
type SomeController() =
inherit ApiController()
member x.Get() = async {
let! data = DownloadAsync("http://stackoverflow.com") |> Async.AwaitTask
return System.Web.Http.Results.OkNegotiatedContentResult(data, x) :> IHttpActionResult // Pass in 'this' pointer (x) into extension method along with data
} |> Async.StartAsTask
This with the additional upcast :> IHttpActionResult
you can also return different behavior BadRequest
, etc from your model and still have it run async
and the type signatures should work out and compile cleanly
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