I have some C# code (in a .NET Standard 2.0 library) that looks like the following:

private object MySynchronousOp(object param, object cmnParam) 
{
  //Do stuff...
  return theObject;
}

private ResponseT Do<ResponseT, RequestT>(Func<RequestT, object, ResponseT> op, RequestT request)
{
  //Do some checks, get/setup cmnParam.
  return op.Invoke(request, cmnPatam);
}

public object RunMySynchronousOp(object param)
{
  return Do(MySynchronousOp, param);
}

This pattern is repeated for many MySynchronousOp and RunMySynchronousOp pairs, using Do to do a common set of checks and provide a valid common parameter.

Now I am adding an asynchronous pair of methods like the following, without changing the Do() method already defined:

private async Task<object> MyAsynchronousOp(object param, object cmnParam) 
{
  //Do stuff, with awaits here...
  return theObject;
}

public async Task<object> RunMyAsynchronousOp(object param) 
{
  return Do(MyAsynchronousOp, param);
}

This compiles, which surprised me and sitting here I find suspicious. I am not yet in a position to actually test this change, but was curious if I should expect this to work without any nasty surprises or unexpected behaviors. I am used to thinking of async methods as colored functions needing to be either called by other async functions or have Wait() called on the Task returned from it to make it synchronous again, but this would be a case where the async nature of the My... method passed through the Do method and showed up again in the Run... method, which I don't know if I believe.

Am I missing something?

Edit: As a note, I've used object in my example code here, but the actual code uses other types. I didn't copy and paste from the actual code because of work-related reasons. In the actual code there are several MySynchronousOp and RunMySynchronousOp methods each with different RequestT and ResponseT types. I just didn't want to replicate or invent any of that here and as far as I know (though I could be wrong I suppose) it doesn't affect the meat of my question.

4 Replies 4

In the line in the async version:

return Do(MyAsynchronousOp, param);

The Do<ResponseT, RequestT> function's type parameters are implicitly applied with object and Task<object> respectively, so the function is reified into

Task<object> Do(Func<object, object, Task<object>> op, object request)

To this you are giving as the first delegate parameter the MyAsynchronousOp function, which takes two object parameters and returns a Task<object>, so it all just works.

It is a bit weird to be messing with generics and then just bailing out with object. Why not pass actual types?

The only other question is whether you should elide the await, which is more efficient. Depending what you are doing in Do it may or may not make sense to do so. Do not do so if there are using or locks in it.

I've edited my question to address Charlieface's question about using object, where in the real code there are actual types and object is just a placeholder in my example. Maybe I should have given them names, but there are types with meaningful data being moved around.

I don't think anyone will be surprised if I reveal that the My...Op methods perform network requests. Currently these are all performed synchronously which is fine. The new MyAsynchronousOp request however will be used for long polling, so I was thinking it might be more efficient to make it asynchronous so that I don't have to dedicate a thread to it just to have it sit around and wait for responses. That was my motivation in adding the async in.

The meat of my question is that I was a little surprised (and a little concerned) that from an async method I call an apparently (superficially?) generic non-async method which then calls an async method, all without Wait-ing on Tasks or anything I would normally associate with going between async and non-async methods. I know that in this case the generic type-substitution will make the generic return a Task<object> which is an expected async method return type, but the method is not marked async and does not await anything itself. And it compiles.

Having a minute on my personal machine, I wrote up this actual toy example:

    internal class Program
    {
        static async Task<string> GetString1() { await Task.Delay(1000); return "Fast"; }
        static Task<string> GetString2() { return GetString1(); }
        static async Task<string> GetString3() { await Task.Delay(1000); return await GetString2(); }
        static string SlowString() { Thread.Sleep(1000); return "Slow"; }
        static void PrintString(string s) { Console.WriteLine($"{DateTime.UtcNow.ToString("O")} ({Thread.CurrentThread.ManagedThreadId}):  Got string:  {s}"); }
        static void Main(string[] args)
        {
            Console.WriteLine($"{DateTime.UtcNow.ToString("O")} ({Thread.CurrentThread.ManagedThreadId}):  Start");
            for (int i = 0; i < 40; ++i)
            {
                Task.Run(async () => { var str = await GetString3(); PrintString(str); });
            }
            Thread.Sleep(5000);
            Console.WriteLine($"{DateTime.UtcNow.ToString("O")} ({Thread.CurrentThread.ManagedThreadId}):  Done Async");
            for (int i = 0; i < 40; ++i)
            {
                Task.Run(async () => { var str = SlowString(); PrintString(str); });
            }
            Thread.Sleep(5000);
            Console.WriteLine($"{DateTime.UtcNow.ToString("O")} ({Thread.CurrentThread.ManagedThreadId}):  Done Async");
        }
    }

On my machine the async results get printed out basically simultaneously, which indicates that they are indeed being run asynchronously, with the awaits yielding the threadpool threads to the other tasks. The non-async results however stagger in over a couple of seconds as there are only so many threadpool threads, so some of the tasks have to wait for the earlier tasks to complete as they don't yield the thread.

So, it certainly does seem to work, and I've not noticed any gotchas in doing this. I guess the async/await combo is a much thinner veneer of syntactic sugar than I thought it was, maybe.

A method returning a task is not really any different from any other synchronous method. All the magic happens when you add the async keyword, since this makes the compiler rewrite your method into state machine that can be suspended and resumed at every await.

But you should either write your method with async and await, i.e:

public async Task<object> RunMyAsynchronousOp(object param) 
{
  return await Do(MyAsynchronousOp, param);
}

Or you can omit both if the method is simple enough, i.e. it only calls one method that returns a task, and that is the last thing the method does:

public Task<object> RunMyAsynchronousOp(object param) 
{
  return Do(MyAsynchronousOp, param);
}

One thing to be careful with is the lifetimes of any resources:

async Task<object> MyMethod(){
    using var resource = CreateResource();
    return await SomeAsyncMethod(resource );
}

In this case you need the async/await to ensure the resource is not disposed before the asynchronous method returns.

Am I missing something?

Yes, you're not returning what you think you're returning.

With the code

private async Task<object> MyAsynchronousOp(object param, object cmnParam)
{
    //Do stuff, with awaits here...
    return theObject;
}

private ResponseT Do<ResponseT, RequestT>(Func<RequestT, object, ResponseT> op, RequestT request)
{
    //Do some checks, get/setup cmnParam.
    return op.Invoke(request, cmnPatam);
}

public async Task<object> RunMyAsynchronousOp(object param)
{
    return Do(MyAsynchronousOp, param);
}

Your RunMyAsynchronousOp returns in fact Task<Task<object>> just like this analogous version:

public async Task<object> RunMyAsynchronousOp(object param)
{
    Task<object> theTaskFromMyAsynchronousOp = Do(MyAsynchronousOp, param);
    object obj = theTaskFromMyAsynchronousOp;
    return obj;
}

// or without async:

public Task<object> RunMyAsynchronousOp(object param)
{
    Task<object> theTaskFromMyAsynchronousOp = Do(MyAsynchronousOp, param);
    object obj = theTaskFromMyAsynchronousOp;
    Task<object> realResult = Task.FromResult(obj);
    return realResult;
}

So for the question:

Will This C# Generic Method Handle Async Properly?

The answer is: definitely not.

Your Reply

By clicking “Post Your Reply”, 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.