Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async Controller Actions in F#

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.

like image 904
ebb Avatar asked Aug 08 '13 00:08

ebb


3 Answers

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)
}
like image 75
Gustavo Guerra Avatar answered Oct 10 '22 03:10

Gustavo Guerra


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.

like image 40
ebb Avatar answered Oct 10 '22 04:10

ebb


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

like image 32
Jim Wallace Avatar answered Oct 10 '22 05:10

Jim Wallace