Async Controller Actions in F #

Using C # - ASP.NET MVC 4, I can determine the action of an asynchronous controller, for example:

public async Task<ActionResult> IndexWorks() { var data = await DownloadAsync("http://stackoverflow.com"); return Content(data); } 

Is there a way to do something like this using F #?

I know that I could use the AsyncManager approach. I also know that @ Thomas Petrichek made a pretty neat AsyncActionBuilder , but it just feels like a lot of templates compared to the C # approach.

+4
source share
3 answers

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 -> // Preserve the stack trace, when rethrow ExceptionDispatchInfo.Capture(why).Throw() obj() (* Satisfy return value *) } } } 

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.

0
source

async / await uses Tasks, so you will need to convert back and forth between the Task object and F # Async objects. To convert from a task to Async, use Async.AwaitTask . To do this, use Async.StartAsTask . Your example would look like this:

 member x.IndexWorks() = async { let! data = Async.AwaitTask (DownloadAsync "http://stackoverflow.com") return x.Content(data) } |> Async.StartAsTask 

Alternatively, instead of using an async calculation expression, you can use a calculation expression that works for tasks out of the box. There is one in FSharpx:

 let task = FSharpx.Task.TaskBuilder() (...) member x.IndexWorks() = task { let! data = DownloadAsync "http://stackoverflow.com" return x.Content(data) } 
+7
source

I think there may 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 C # WebApi Controller

If you try to do this, as the accepted answer suggests (when trying to use the Ok , BadRequest , etc. built-in methods) you run

cannot access protected members from lambda

To solve this problem, I used ExtensionMethods directly, instead of trying to make a combination between async {} and Task , which MVC expects

 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 is with an additional upcast :> IHttpActionResult you can also return other BadRequest behavior, etc. from your model and still run it async , and type signatures should work and compile cleanly

+1
source

Source: https://habr.com/ru/post/1495857/


All Articles