In fact, it looks like one programmer, and this was done by Dmitry Morozov. He created a custom AsyncWorkflowController that allows you to return Async<ActionResult> from an ActionResult . The code for AsyncWorkflowController can be found at http://fssnip.net/5q .
However, its implementation is very difficult to debug due to the fact that the stack trace will not be saved when it is restarted in the user controller. So I made a small 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 *) } } }
I also changed the following line: new ReflectedControllerDescriptor(controllerType) ,
to new ReflectedAsyncControllerDescriptor(controllerType) - However, this change is purely optional since it has no meaning. I just found it more logical to use Async .
Then the full code:
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 ->
Using:
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 make sure that it actually does all of async and does not block the 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 threads will be different, so the start thread was put back into the pool, and the new one was returned when the async operation completed.