1

I am developing a console application in which I have a third party rest client (wordpress rest client to be precise), which has some methods with return Task objects. An example method signature can be like:

public Task<bool> Delete(int id);

I have a list of Post to be deleted. I could do something simply like:

public void DeleteGivenPosts(List<Post> posts) {
  posts.ForEach(async post => await wpRestClient.Delete(post.Id));
}

In this case the deletion is fire and forget. It would be better if I log the information about the post which is deleted. A log statement like:

logger.Log($"A post with {post.Id} is deleted");

So, I decided to project the Tasks.

public async Task DeleteGivenPosts(List<Post> posts) {
  var postDeletionTasks = posts.Select(post => wpRestClient.Delete(post.Id));

  foreach (var deletionTask in TaskExtensionUtil.GetTasksInCompletingOrder(postsDeletionTasks)) {
    bool deletionResult = await deletionTask;

    if(deletionResult) {
      //i want to log success of deletion here
    } else {
      //i want to log the failure of deletion here
    }
  }
}

Here TaskExtensionUtil.GetTasksInCompletingOrder() is a helper method which returns the tasks in the order they complete. Code for this is:

public static List<Task<T>> GetTasksInCompletingOrder<T>(IEnumerable<Task<T>> sourceTasks) {
  var sourceTasksArr = sourceTasks.ToArray();
  var taskCompletionSourceArr = new TaskCompletionSource<T>[sourceTasksArr.Length];

  var currentSlot = -1;
  for (int i = 0; i < sourceTasksArr.Length; i++) {
    taskCompletionSourceArr[i] = new TaskCompletionSource<T>();
    sourceTasksArr[i].ContinueWith(prev => {
      int indexToSet = Interlocked.Increment(ref currentSlot);
      taskCompletionSourceArr[indexToSet].SetResult(prev.Result);
    });
  }

  return taskCompletionSourceArr.Select(i => i.Task).ToList();
}

The problem is that the deletionResult is a bool. In order to log information about which post is deleted I need to get the Post object associated with the deletion task.

I was thinking to create a dictionary that maps the deletion task to the corresponding Post by doing something like:

posts.Select(post => new { deletionTask = wpRestClient.Delete(post.Id), post})
     .ToDictionary(i => i.deletionTask, i => i.post);

But this will not work because in the GetTasksInCompletingOrder the original deletion tasks are translated to TaskCompletionSource tasks. So I will always get an exception that the key is not present in the dictionary. Also I am not sure how will a dictionary behave when it has Task objects as keys.

At this point I have no clue how to achieve the logging. I will appreciate any help.

Navjot Singh
  • 678
  • 7
  • 18
  • Do you really want to run the deletions as fire-and-forget tasks? – Theodor Zoulias Jun 18 '20 at 22:20
  • @TheodorZoulias I just want to perform the deletions. If there is a failure I just want to tell that a certain deletion was unable to complete. – Navjot Singh Jun 19 '20 at 09:09
  • It is not clear to me if you are firing-and-forgetting the deletions intentionally, because you don't care if they succeed or fail at that moment, or you are doing it unintentionally, because you don't know how to wait them to complete before continuing. – Theodor Zoulias Jun 19 '20 at 10:40
  • @TheodorZoulias Let me reframe the requirement. I want to start multiple deletions in parallel. I want all these deletion tasks to complete before my console application exits. Also I want to log the result of each deletion whether it succeeded or failed. Also I don't want to wait for each deletion in a serial manner. I could have used a loop and in the loop i can await for each deletion but I just want to optimise by deleting in parallel. – Navjot Singh Jun 19 '20 at 11:01
  • According to these requirements, it seems to me that you shouldn't run the deletions as fire-and-forget. You should await them before closing the app. Do you know about the `Task.WhenAll` method? If not I could write a short answer with a usage example. – Theodor Zoulias Jun 19 '20 at 11:06
  • I think the accepted answer solves the problem. If you have alternate solution, then your answer is welcome. Yes I know about Task.WhenAll. – Navjot Singh Jun 19 '20 at 11:09
  • 1
    Yeap, I just noticed that the accepted answer contains a proper version of `DeleteGivenPosts` that uses `Task.WhenAll`. It also contains a problematic `DeleteGivenPosts` that uses the [non-async-friendly](https://stackoverflow.com/questions/18667633/how-can-i-use-async-with-foreach) `List.ForEach` method. Which is quite confusing, because the problematic version is standing on top, and the correct version is presented as a supposed example of how to use the problematic version. Anyway, your problem is now solved, so everything is nice and well. :-) – Theodor Zoulias Jun 19 '20 at 11:20
  • 1
    @TheodorZoulias thank you for your comment. I removed the confusing problematic version of `DeleteGivenPosts` from the top and exchanged it with the async one from the programm below. – Mong Zhu Jun 24 '20 at 06:19

2 Answers2

3

how about to write the logstatement after deletion?

public async Task DeleteGivenPosts(List<Post> posts)
{
    await Task.WhenAll(
    posts.Select(async post =>
    {
        bool res = await wpRestClient.Delete(post.Id);
        string message = res ? $"Post {post.Id} is deleted" : $"Post {post.Id} survived!";
        logger.Log(message);
    }));
}

Here is a small LinqPad Programm to examplify the workings of the method:

async void Main()
{
    List<Post> postList = Enumerable.Range(1, 12).Select(id => new Post {Id = id}).ToList();
    Console.WriteLine("Start Deletion");
    await DeleteGivenPosts(postList);
    Console.WriteLine("Finished Deletion");
}

public static MyRestClient wpRestClient = new MyRestClient();
// Define other methods and classes here
public async Task DeleteGivenPosts(List<Post> posts)
{
    await Task.WhenAll(
    posts.Select(async post =>
    {
        bool res = await wpRestClient.Delete(post.Id);
        string message = res ? $"Post {post.Id} is deleted" : $"Post {post.Id} survived!";
        Console.WriteLine(message);
    }));
}

public static Random rand = new Random(DateTime.Now.Millisecond);

public class MyRestClient
{
    public async Task<bool> Delete(int i)
    {
        return await Task<bool>.Run(() => { Thread.Sleep(400); return rand.Next(1,4) == 1;});
    }
}

public class Post
{
    public int Id { get; set; }
}

Output:

Start Deletion
Post 1 is deleted
Post 3 survived!
Post 5 survived!
Post 6 survived!
Post 2 survived!
Post 4 survived!
Post 8 survived!
Post 7 survived!
Post 9 survived!
Post 11 survived!
Post 10 is deleted
Post 12 is deleted
Finished Deletion

Mong Zhu
  • 23,309
  • 10
  • 44
  • 76
  • @NavjotSingh so that means you need an awaitable method `DeleteGivenPosts`? Or do you want the user to prevent to close your application?? – Mong Zhu Jun 18 '20 at 13:18
  • I have to make sure that the application does not exit before all the deletion tasks are completed. By the way in your linqPad program, does the main method wait for all posts to be deleted? – Navjot Singh Jun 18 '20 at 13:18
  • nope it does not, because your `DeleteGivenPosts` method is void – Mong Zhu Jun 18 '20 at 13:20
  • Yes, then I think I need an awaitable method. – Navjot Singh Jun 18 '20 at 13:21
  • @NavjotSingh now it does. – Mong Zhu Jun 18 '20 at 13:28
  • @NavjotSingh "I have to make sure that the application does not exit before..." I think you will have a hard time preventing the user from closing your application, Here is a [post](https://stackoverflow.com/a/34421778/5174469) elaborating on this topic – Mong Zhu Jun 18 '20 at 13:32
  • This application will be invoked by other application which is not supposed to kill it. So I think the last comment will not apply in my case – Navjot Singh Jun 18 '20 at 13:39
  • 1
    @NavjotSingh ok, then I am happy that we solved your problem. have a nice day mate – Mong Zhu Jun 18 '20 at 14:20
1

You could wrap it with your own method too, something like this:

IEnumerable<Task<DeletionResult>> postDeletionTasks = posts.Select(post => DeletePost(post.Id));

Where your method and result class might look something like:

private async Task<DeletionResult> DeletePost(int postId)
{
    bool result = await wpRestClient.Delete(postId);
    return new DeletionResult(result, postId);
}

And

public class DeletionResult
{
    public DeletionResult(bool result, int postId)
    {
        Result = result;
        PostId = postId;
    }

    public bool Result { get; }
    public int PostId { get; }
}

That way you'll have a list of tasks which result contains the PostId it affected.

devcrp
  • 1,323
  • 7
  • 17
  • what do you mean? I'm awaiting the `Delete` but not awaiting the `DeletePost` in the `.posts.Select()`, not sure if I follow you. – devcrp Jun 18 '20 at 13:44
  • I understood your answer incorrectly. My bad. I think your answer also solves the problem. – Navjot Singh Jun 18 '20 at 13:55