Chaining Tasks in csharp with success and fault handler

泪湿孤枕 提交于 2020-02-05 03:31:09

问题


Edit See the title "Problem" at the end within my question to crack this question down.

Coming from nodejs where we could chain promises, in C# I'm seeing Async Tasks almost comparable. Here's my attempt.

Edit - I can't mark my uber level caller methods as async as a dll based library is calling it

Caller object

public void DoSomething(MyRequest request) 
{
    Delegate.Job1(request)
        .ContinueWith(Delegate.Job2)
        .ContinueWith(Fault, TaskContinuationOptions.OnlyOnFaulted)
        .ContinueWith(Result);
}

public void Result(Task<MyRequest> task)
{
    MyRequest request = task.Result;
    Console.Writeline(request.result1 + " " + request.result2);
}

public void Fault(Task<MyRequest> task)
{
   MyRequest request = task.Result;
   Console.Writeline(request.result);
}

Delegate Object

public async Task<MyRequest> Job1(MyRequest request) 
{
    var data = await remoteService.Service1Async();
    request.result1 = data;
    return request;
}

public async Task<MyRequest> Job2(Task<MyRequest> task)
{
    var data = await remoteService.Service2Async();
    request.result2 = data;
    return request;
}

Problem:

1) Edit (fixed, the linked dll to my project was missing it's linked dll) Task.Result (request) is coming as null in the Result method, Also Status = Faulted

2) Also is Fault Handling correct? I'm expecting Fault to be only called when an exception occurs within the Delegate methods, otherwise it should skip.

2-b) Another option is check within the Result function (delete Fault function) if Task.status = RanTocompletion and branch there for success or fault

Edit after the answer

I've a gotcha, what if I can't make my controller async.

Controller

public void ICannotBeAsync()
{
    try
    {
        var r = await caller.DoSomething(request); // though I can use ContinueWith here, ???
    }
    catch(Exception e)
    {
        //exception handling
    }
}

Caller

public async Task DoSomethingAsync(MyRequest request)
{
     request.result1 = await delegateInstance.Job1(request);
     request.result2 = await delegateInstance.Job2(request);
     Console.Writeline(request.result1 + " " + request.result2);
     return result;
}

Edit 2 - based on VMAtm Edit, please review OnlyOnFaulted (Fault) option.

Delegate.Job1(request)
    .ContinueWith(_ => Delegate.Job2(request), TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(() => {request.result = Task.Exception; Fault(request);}, TaskContinuationOptions.OnlyOnFaulted)
    .ContinueWith(Result, TaskContinuationOptions.OnlyOnRanToCompletion);

Problem -

Gave it a test, actual code below, none of the Result or Fault is getting called, although the method GetCustomersAsync returned successfuly. My understanding everything stops at Fault because it's marked to run on Fault only, execution stops there and Result handler is not called.

Customer.GetCustomersAsync(request)
    .ContinueWith(_ => { Debug.WriteLine("not executing"); Fault(request); }, TaskContinuationOptions.OnlyOnFaulted)
    .ContinueWith(_ => { Debug.WriteLine("not executing either"); Result(request); }, TaskContinuationOptions.OnlyOnRanToCompletion);

Edit 3 Building on Evk's answer.

Task<Request> task = Customer.GetCustomersAsync(request);
task.ContinueWith(_ => Job2Async(request), TaskContinuationOptions.OnlyOnRanToCompletion);
task.ContinueWith(_ => Job3Async(request), TaskContinuationOptions.OnlyOnRanToCompletion);
task.ContinueWith(_ => Result(request), TaskContinuationOptions.OnlyOnRanToCompletion);
task.ContinueWith(t => { request.Result = t.Exception; Fault(request); }, TaskContinuationOptions.OnlyOnFaulted);

回答1:


A lot of things has been said here, so I only answer to the last "Problem" section:

Customer.GetCustomersAsync(request)
    .ContinueWith(_ => { Debug.WriteLine("not executing"); Fault(request); }, TaskContinuationOptions.OnlyOnFaulted)
    .ContinueWith(_ => { Debug.WriteLine("not executing either"); Result(request); }, TaskContinuationOptions.OnlyOnRanToCompletion);

Problem here (and in original example too) is that the following happens:

  1. You continue GetCustomersAsync with "only on faulted" continuation.
  2. Then you continue that continuation, not GetCustomersAsync, with the next continuation, which can run only on completion.

In result, both continations can execute only when GetCustomersAsync fails. To fix:

var request = Customer.GetCustomersAsync(request);
request.ContinueWith(_ => { Debug.WriteLine("not executing"); Fault(request); }, TaskContinuationOptions.OnlyOnFaulted);
request.ContinueWith(_ => { Debug.WriteLine("not executing either"); Result(request); }, TaskContinuationOptions.OnlyOnRanToCompletion);

Note that even if you cannot change signature of some method and it absolutely must return void, you still can mark it as async:

public async void DoSomethingAsync(MyRequest request)
{
     try {
         await Customer.GetCustomersAsync(request);
         Result(request);      
     }
     catch (Exception ex) {
         Fault(request);
     }
}



回答2:


There are multiple issues with this code:

Delegate.Job1(request)
    .ContinueWith(Delegate.Job2)
    .ContinueWith(Fault, TaskContinuationOptions.OnlyOnFaulted)
    .ContinueWith(Result);

First of all, you are continuing the execution with Delegate.Job2 even if the Delegate.Job1 failed. So you need a OnlyOnRanToCompletion value here. Similar to the Result continuation, you are continuing in all the cases, so the task with an error still goes through the chain and, as you already see, is in Faulted state with a null as a result.

So, your code, if you can't use on that level await, could be like this (also, as @Evk stated, you had to add the exception handling to all of your code, which is realy ugly):

Delegate.Job1(request)
    .ContinueWith(Delegate.Job2, TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(Fault, TaskContinuationOptions.OnlyOnFaulted)
    .ContinueWith(Result, TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(Fault, TaskContinuationOptions.OnlyOnFaulted);

However, you still have an option to use the await keyword inside your method and after that use a lambda to run it synchronously, if it is an option for you:

public async Task DoSomethingAsync(MyRequest request)
{
    try
    {
         request.result1 = await delegateInstance.Job1(request);
         request.result2 = await delegateInstance.Job2(request);
         Console.Writeline(request.result1 + " " + request.result2);
         return result;
     }
     catch(Exception e)
     {

     }
}

public void ICannotBeAsync()
{
    var task = Task.Run(() => caller.DoSomethingAsync(request);
    // calling the .Result property will block current thread
    Console.WriteLine(task.Result);
}

Exception handling could be done on either levels, so it's up to you where to introduce it. If something goes wrong during the execution, Result property will raise an AggregateException as a wrapper to inner exceptions happened during the call. Also you can use a Wait method for a task, wrapped into a try/catch clause, and check the task state after that, and deal with it as you need (it has IsFaulted, IsCompleted, IsCanceled boolean properties).

Also, it's highly recommended to use some cancellation logic for your task-oriented tasks to be able to cancel unnecessary work. You can start with this MSDN article.

Update, based on your other questions:

If you still want to use the ContinueWith instead of the await, and want to change the signatures of the Job1, Job2 methods, you should change your code like this:

Delegate.Job1(request)
    .ContinueWith(_ => Delegate.Job2(request), TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(Result, TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(Fault, TaskContinuationOptions.OnlyOnFaulted);

The reason for this is that the ContinueWith method accepts a Func<Task, Task>, because you need, in general, inspect the task for it's status and/or it's result.

As for the question about not blocking the caller, you can try out the TaskCompletionSource<TResult> class, something like this:

public void ICannotBeAsync()
{
    var source = new TaskCompletionSource<TResult>();
    var task = Task.Run(() => caller.DoSomethingAsync(request, source);
    while (!source.IsCompleted && !source.IsFaulted)
    {
        // yeild the execution to other threads for now, while the result isn't available
        Thread.Yeild();
    }
}

public async Task DoSomethingAsync(MyRequest request, TaskCompletionSource<TResult> source)
{
     request.result1 = await delegateInstance.Job1(request);
     request.result2 = await delegateInstance.Job2(request);
     Console.Writeline(request.result1 + " " + request.result2);
     source.SetResult(result);
}


来源:https://stackoverflow.com/questions/42091348/chaining-tasks-in-csharp-with-success-and-fault-handler

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!