2

I have this function with action parameter, after it was executed the parameter on that function will have a value. Is there a way for me to get the value?

public void DetailsAsync(string param1, string param2,Action<IList<Detail>> callback)
{
       //process happen here and will have a callback to produce the data for detail
}

public class DetailController:ApiController
{
  private IList<Detail> details;

  private DetailCompleted(IList<Detail> detail)
  {
    //now detail parameter has a value that I can use
    details = detail;
  }


  [HttpGet]
  public IList<Detail> GetDetails()
  {
    ServiceManager.DetailsAsync("param1","param2",detailsCompleted)

   //after ServiceManager.DetailsAsync it will go to return details
    return details;
  }
}

When I tried this code, I placed a breakpoint on return details and breakpoint on detailsCompleted but what happen is that when I called the web api GetDatails, it will first execute return details and right after that it will execute detailsCompleted function. that's why currently I cant get the value.

M_M
  • 61
  • 6
  • Overall,this looks like a correct callback pattern to me, but I have some concerns over why some things contain the word "async" in their names. I suspect there may be some important code missing. Can you share the code inside `DetailsAsync`? – Bradley Uffner Apr 04 '19 at 10:31
  • @BradleyUffner unfortunately I don't have the code for it, all I know is the parameter required to use DetailsAsync but I think they are calling a service with async inside of it. – M_M Apr 04 '19 at 10:59
  • @BradleyUffner actually when set a breakpoint in DetailCompleted, the detail has the record I need, but since the "return details" was being executed first instead of DetailCompleted my details variable doesn't get the records. – M_M Apr 04 '19 at 11:05
  • That's what I suspected. You need some way to delay execution of the `return` statement until signaled by `DetailCompleted` that a result has been received. A `Semaphore`, or `TaskCompletionSource` would work, but may be a little tricky to set up. The real problem is that `DetailsAsync` is implemented with a poor async pattern. – Bradley Uffner Apr 04 '19 at 11:21
  • @BradleyUffner my understanding is that Semaphore or TaskCompletionSource can be implemented in void like DetailAsync right? since I dont have access on the code of DetailsAsync – M_M Apr 04 '19 at 11:51

3 Answers3

2

The return value of a Action<T> is by default void. If you want a return value, you should use Func<T,TResult>

https://learn.microsoft.com/en-us/dotnet/api/system.func-2?view=netframework-4.7.2

Amfasis
  • 3,932
  • 2
  • 18
  • 27
  • Thanks and yes I agree with you, the problem is that, I'm only allowed to create my own web api function and use ServiceManager object to get the records and As I mention I'm confused also on why the return details was being hit first and followed by DetailCompleted function. – M_M Apr 04 '19 at 10:17
  • When you say "by default void", do you mean that it can ever be non-void? – Seva Apr 04 '19 at 10:17
  • He is using a callback to "return" the data. There is no need to convert the method to a `Func` for that to work. – Bradley Uffner Apr 04 '19 at 10:33
  • 1
    @pteberf it can not be non-void, I think I actually meant to say "by definition" – Amfasis Apr 04 '19 at 10:59
  • @BradleyUffner Yes I think you're right, I misunderstood the question. The answer of pteberf is correct – Amfasis Apr 04 '19 at 11:01
2

I think the problem here is that DetailsAsync(), as implied by name is asynchronous and you return details before waiting for results of DetailsAsync(). So you should await it, but because DetailsAsync returns void, you can't.

So you could wrap the DetailsAsync in a Task and .Wait() for it, but it kind of sucks because you will block the calling thread.

[HttpGet]
public IList<Detail> GetDetails()
{
    Task.Run(() => 
        ServiceManager.DetailsAsync("param1", "param2", detailsCompleted)
    ).Wait();
    return details;
}
Seva
  • 1,631
  • 2
  • 18
  • 23
  • I apologize for not so clear explanation, the ServiceManager.DetailAsync is void and inside of it I think they are calling async function that's why I think they named it DetailAsync – M_M Apr 04 '19 at 10:31
  • Well if they are calling an async function inside and await for it, chances are DetailsAsync is async too. `async` methods can return `void` too. – Seva Apr 04 '19 at 10:37
  • @pteberf but when I tried to implement the code you gave, it says an error "cannot await void" – M_M Apr 04 '19 at 10:41
  • If `DetailsAsync` is implemented as I suspect, awaiting it won't help. What is the origin of `DetailsAsync`? Is it part of some public package, or is it internally written code that you just do not have access to? – Bradley Uffner Apr 04 '19 at 10:56
  • @BradleyUffner it's internally written code which I don't have access. – M_M Apr 04 '19 at 11:02
  • I think you might have to use a Semaphore here, as suggested in this answer: https://stackoverflow.com/questions/12858501/is-it-possible-to-await-an-event-instead-of-another-async-method – Amfasis Apr 04 '19 at 11:06
  • @pteberf when I tried it, it says "cannot convert from void to System.Func" – M_M Apr 04 '19 at 11:13
  • @Amfasis I don't know it but I'll check the details of it thanks – M_M Apr 04 '19 at 11:15
  • Yeah, pretty sure that `DetailsAsync` isn't implemented correctly. The semaphore that @Amfasis mentioned may work. A `TaskCompletionSource` might also be a good option. – Bradley Uffner Apr 04 '19 at 11:17
  • @pteberf yes it did run but the same thing happen, the code execution will go first in "return details" before going to ``` private DetailCompleted(IListdetail) that's why the details variable is still null – M_M Apr 04 '19 at 11:21
  • @BradleyUffner I'm not sure if DetailsAsync is not properly implemented, but using the said function actually gave me the records but the problem is that I can't get the records to be passed on another variable ```details – M_M Apr 04 '19 at 11:27
  • Yeah.. :( Nothing inside the `DetailsAsync` signals that the task is complete because there is no task. So you have to take care of this externally like @BradleyUffner and @Amfasis suggest. – Seva Apr 04 '19 at 11:48
1

Because of the way DetailsAsync is written, you will need some sort of signalling system to pause execution of GetDetails until the callback is fired. There are several options, but I choose an AutoResetEvent because it is fairly simple to work with and understand.

(I've changed some of the return types just so that I didn't have to create fake classes to match your code)

public class DetailController
{
    private IList<int> details;
    private AutoResetEvent callbackSignal = new AutoResetEvent(false);

    private void DetailCompleted(IList<int> detail)
    {
        details = detail;
        callbackSignal.Set();
    }    

    public IList<int> GetDetails()
    {    
        ServiceManager.DetailsAsync("param1", "param2", DetailCompleted);
        callbackSignal.WaitOne();
        return details;
    }
}

callbackSignal.WaitOne(); will block until "signaled". In the callback method, callbackSignal.Set(); sends the signal, telling anything waiting on the event that it is ok to proceed now.

Without knowing exactly how DetailsAsync is implemented, I can't guarantee that this will work, but I'm hopeful. You may also have to add some additional protection to make sure this is full reentrant if required.


If you would rather work with the more modern async / await pattern, you could wrap access to DetailsAsync access in a method that is Task returning, and uses a TaskCompletionSource to orchestrate the callback and return values.

public class DetailController
{
    public async Task<IList<int>> GetDetails()
    {
        var details = await ServiceWrapper.GetDetails();
        return details;
    }
}

public static class ServiceWrapper
{
    public static Task<IList<int>> GetDetails()
    {
        var tcs = new TaskCompletionSource<IList<int>>();
        ServiceManager.DetailsAsync("param1", "param2", (IList<int> details) =>
            {
                tcs.SetResult(details);
            });
        return tcs.Task;
    }
}
Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • Thanks for the sample code, i tried it but its like taking forever, until now it's still running. – M_M Apr 04 '19 at 11:59
  • I've added an example of the `TaskCompletionSource` pattern, which might work better for your specific case. – Bradley Uffner Apr 04 '19 at 12:04
  • woooowwwww!!!!, thanks a lot Bradley, your code for TaskCompletionSource did work!!! it took me the whole day!!! thank you again! – M_M Apr 04 '19 at 12:21