问题
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:
- You continue
GetCustomersAsync
with "only on faulted" continuation. - 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