2

I am learning F# through building an ASP.NET Core app. I need to implement an interface Microsoft.AspNetCore.Identity.IUserStore<'TUser>; this interface features methods that return Task like:

member this.CreateAsync (user : User, cancellationToken : CancellationToken) =
    async {
        let connectionString = configuration.GetConnectionString("SocialScoreApp")
        let! rowCountResult = user |> createAsync connectionString
        match rowCountResult with
        | Ok _ -> return IdentityResult.Success
        | Error e ->
        let identityError = new IdentityError()
        identityError.Description <- e
        return IdentityResult.Failed(identityError)
    } |> Async.StartAsTask

But, according to the documentation, Async.StartAsTask "executes a computation on the thread pool". But this is an I/O bound operation so I don't want any new threads involved. Is there any way to achieve that?

Edit: This is the createAsync code for reference:

let createAsync (connectionString : string) (user:User) =
    let sql = "..."
    let args = dict [
        // ...
    ]
        
    async {
        use connection = new SqlConnection(connectionString)
        do! connection.OpenAsync() |> Async.AwaitTask

        try
            let! rowCount = connection.ExecuteAsync(sql, args) |> Async.AwaitTask
            return Ok rowCount
        with
        | e -> return Error e.Message
    }
5
  • 1
    Where does C# come into this? Commented Jun 25, 2021 at 8:43
  • Sorry, meant to put .NET. Will fix. Commented Jun 25, 2021 at 8:43
  • Tasks aren't threads. They're essentially the same as an async computation - a promise that something will complete in the future. You already use Tasks in the async computation. Asynchronous ADO.NET methods return a Task Commented Jun 25, 2021 at 8:47
  • I know tasks aren't threads. But Async.StartAsTask does seem to start the computation on another thread according to how I interpret the documentation. If I remove |> Async.StartAsTask I get an error, because the return type is Async<IdentityResult> instead of Task<IdentityResult> Commented Jun 25, 2021 at 8:57
  • Async and Task instances are different regarding when they are run : Async is run on demand but Task is run immediately. So it's not possible to create a Task without the "I/O effect" that bathers you. Commented Jun 25, 2021 at 11:42

1 Answer 1

4

If you look at how things work, the StartAsTask (source) operation creates a TaskCompletionSource, which creates a task that is returned to the caller. This is later used to report the result of the computation. In the meantime, the computation is started using QueueAsync, which invokes QueueWorkItemWithTrampoline (source) and this uses standard .NET ThreadPool.QueueUserWorkItem.

This means that when you run StartAsTask, the async workflow is added to the thread pool. However, the key thing for IO-bound operations is that you are using let! in your snippet:

let connectionString = configuration.GetConnectionString("SocialScoreApp")
let! rowCountResult = user |> createAsync connectionString

The work item added to the thread pool will only run GetConnectionString. It will then call createAsync, which presumably contains some non-blocking IO operation. As soon as the evaluation gets to this point, the work in the thread pool completes and it schedules a callback to be called when the IO operation completes - the callback is invoked later and adds the rest of the computation to thread pool as a new work item. So, you are not blocking the thread pool while the IO operation is running.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you Tomas, on spot as always! Added the createAsync code for reference.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.