0

I have the following scenario:

Customer has a number of accounts, each of them has a number of cards attached.

Now I have a request where I need to query accounts with cards on multiple customers. I have async methods to query accounts and cards separately, i.e. FindAccounts(string customer), FindCards(string[] accounts).

So I have this method:

public async Task<Data> FindCustomersWithCards(string[] customers) { var accountsTasks = customers.Select(_service.FindAccounts); var accounts = await Task.WhenAll(accountsTasks); var cardsTasks = accounts.Select(_service.FindCards); var cards = await Tasks.WhenAll(cardsTasks) ... } 

While this will work it has a problem that you have to wait for accounts of all customers to be finished before cards can be queried. A more efficient implementation would go on and query cards for customer accounts as soon as accounts querying for a particular customer is finished (without waiting for other customers).

My question is if it is possible to do this with async/await. I think I can manage with ContinueWith, but I am not 100% sure it is OK to mix async/await with ContinueWith approach.

12
  • You are right, ContinueWith sounds like what you want. You will build up a bigger task "Get account and then get cards" for each account Commented Feb 21, 2017 at 10:55
  • What about mixing two approaches? Do you know of any drawbacks of that? Commented Feb 21, 2017 at 11:02
  • Yeah I meant mixing it, you can await on a ContinueWith, basically you can await in anything that return a Task and ContinueWith does. If you show your method signatures I can provide oyu an example code Commented Feb 21, 2017 at 11:23
  • Obviously what you've shown isn't the real code (since it doesn't return anything). Do you, in your final form, only require the cards? You're only obtaining the accounts as an intermediate step and don't need them for anything else? Commented Feb 21, 2017 at 11:25
  • 1
    @IlyaChernomordik Instead of writing complicated code, just create an asynchronous function that does what you want for a single customer and call it once for each customer. TPL with async/await is no more complex that writing the equivalent synchronous code Commented Feb 21, 2017 at 11:58

2 Answers 2

3

It might be more sensible to split it by customer and async within that:

private async Task<Card> FindCardForCustomerAsync(string customer) { var account = await _service.FindAccountAsync(customer); return await _service.FindCardAsync(account); } public async Task<Data> FindCustomersWithCards(string[] customers) { var cardsTasks = customers.Select(FindCardForCustomerAsync); var cards = await Tasks.WhenAll(cardsTasks) … } 

However, it's worth considering the balance of efficiencies of how your FindAccounts and FindCards work. E.g. if they work as a single SELECT pushed to a database then the greater concurrency of turning it into multiple smaller bits of work may not be worth the greater amount of overhead that has. It can be the case that waiting for 20 or even 200 results is only marginally slower than waiting for 1, and then splitting into 20 requests gains very little, even before the extra connections involved are considered.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks, it seems rather obvious now, but I did not think of it myself for some reason. @Panagiotis Kanavos have pointed out the same solution in comments. The reason that I don't have one SELECT is that I don't have control over the data store and allows only these fragmented queries. I would rather query them all if I had the chance :)
If your FindAccounts is making several calls behind the scenes then the odds are certainly better that this will improve things, or at least not make them worse.
FindAccounts does just one call really, but the problem was that I had to find accounts for ALL customers before I could continue with their cards :)
Oh, it doesn't relate to the strings you were passed?
I think we have a misunderstanding here :) FindAccounts itself does one call, but I used it as a method group on multiple customers, so there will be multiple calls.
0

It is hard to say if the tasks per customer approach will bring you the benefit, the best way to find it out is to make a test on your scenario.

I made a simple (event that it may look quite messy) example how the first approach can be modified to allow execution of tasks per each customer.
You have mentioned that you can manage this, I just wanted to post it here so anyone interested in that can play with it.

I used Task.Run(...) to simulate async tasks.

public class Account { public string AccountName { get; set; } public string CustomerName { get; set; } } public class Card { public string CardName { get; set; } public string AccountName { get; set; } } public List<Account> Accounts { get; set; } public List<Card> Cards { get; set; } //OLD public async Task<string[]> FindAccounts(string customer) { return await Task.Run(() => { return Accounts.Where(a => a.CustomerName == customer).Select(a => a.AccountName).ToArray(); }); } //OLD public async Task<string[]> FindCards(string[] accounts) { return await Task.Run(() => { return Cards.Where(c => accounts.Contains(c.AccountName)).Select(a => a.CardName).ToArray(); }); } //NEW public async Task<string[]> FindCards(Task<string[]> findAccountsTasks) { return await Task.Run(async () => { var accounts = await findAccountsTasks; return Cards.Where(c => accounts.Contains(c.AccountName)).Select(a => a.CardName).ToArray(); }); } //NEW public async Task<string[]> FindCards(string customer) { return await await FindAccounts(customer).ContinueWith(FindCards); } private async void button7_Click(object sender, EventArgs e) { Accounts = new List<Account> { new Account {CustomerName = "Tomas", AccountName = "TomasAccount1"}, new Account {CustomerName = "Tomas", AccountName = "TomasAccount2"}, new Account {CustomerName = "Tomas", AccountName = "TomasAccount3"}, new Account {CustomerName = "John", AccountName = "JohnAccount1"} }; Cards = new List<Card> { new Card {AccountName = "TomasAccount1", CardName = "TomasAccount1Card1"}, new Card {AccountName = "TomasAccount1", CardName = "TomasAccount1Card2"}, new Card {AccountName = "TomasAccount1", CardName = "TomasAccount1Card3"}, new Card {AccountName = "TomasAccount1", CardName = "TomasAccount2Card1"}, new Card {AccountName = "JohnAccount1", CardName = "JohnAccount1Card1"}, new Card {AccountName = "JohnAccount1", CardName = "JohnAccount1Card2"}, }; var customers = new List<string> { "Tomas", "John" }.ToArray(); //OLD var accountstasks = customers.Select(FindAccounts); var accounts = await Task.WhenAll(accountstasks); var cardTasks = accounts.Select(FindCards); var cards = await Task.WhenAll(cardTasks); //NEW cardTasks = customers.Select(FindCards); cards = await Task.WhenAll(cardTasks); } 

1 Comment

Thanks for your suggestion, apparently there was a solution that is even easier and clearer :)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.