5

使用 C# - ASP.NET MVC 4,我可以定义一个异步控制器操作,例如:

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

有没有办法使用 F# 做类似的事情?

我知道我可以使用这种AsyncManager方法。我也知道@Tomas Petricek 做了一个非常整洁的AsyncActionBuilder,但与 C# 方法相比,它感觉就像很多样板。

4

3 回答 3

8

async/await 使用 Tasks,因此您需要在 Task 对象和 F# Async 对象之间来回转换。要将 Task 转换为 Async,请使用Async.AwaitTask. 做相反的使用Async.StartAsTask。您的示例变为:

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

或者,async您可以使用开箱即用的适用于任务的计算表达式,而不是使用计算表达式。FSharpx 中有一个:

let task = FSharpx.Task.TaskBuilder()

(...)

member x.IndexWorks() = task {
    let! data = DownloadAsync "http://stackoverflow.com"
    return x.Content(data)
}
于 2013-08-08T14:27:08.600 回答
1

实际上,Dmitry Morozov 似乎是一位程序员同事,让这件事成为可能。他做了一个习惯AsyncWorkflowController,可以Async<ActionResult>ActionResult. AsyncWorkFlowController可以在http://fssnip.net/5q找到代码。

然而,他的实现使得调试变得非常困难,因为在自定义控制器中重新抛出时堆栈跟踪不会被保留。因此,我做了一些改变以使这成为可能:

 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 *) } } }

我还更改了以下行:new ReflectedControllerDescriptor(controllerType)

to new ReflectedAsyncControllerDescriptor(controllerType)- 但是,此更改纯粹是可选的,因为它不会产生任何影响。我只是发现使用它更合乎逻辑Async

完整的代码将是:

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 *) } } }

用法:

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 }

并确认它实际上是异步执行所有操作,并且没有阻塞 ASP.NET 池中的任何线程,请使用:

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 }

开始线程和结束线程将不同,因此开始线程被放回池中,并且在异步操作完成时返回一个新线程。

于 2013-08-09T10:30:00.327 回答
1

我认为可能有很多人试图做类似的事情

type SomeController() =
    inherit ApiController()
    member x.Get() =
        let data = Download("http://stackoverflow.com")
        x.Ok(data) :> IHttpActionResult // Using built in Ok, BadRequest, etc.

type Get() = unit -> Task<IHttpActionResult>正如 C# WebApi 控制器所期望的那样

如果您尝试按照公认的答案建议进行操作(在尝试使用内置的Ok,BadRequest等方法时),您会遇到

无法从 lambda 中访问受保护的成员

为了解决这个问题,我直接使用了 ExtensionMethods,而不是尝试在MVC 所期望的之间async {}进行扭曲Task

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

于 2016-01-07T19:43:56.077 回答