Sanity check needed, as I'm still very unfamiliar with the nuances behind async/await and how they relate to more traditional Task / TPL code.
I have a high-level repository layer which is assembling a business object from various lower-level repositories. As a business decision, we are doing keyed lookups at the data layer and assembling the composite object in code. In the following code, "Account" is the business object, and the various _tAccountXXXRepo objects are all patterned similarly to this (we are using NPoco):
public class T_AccountMapperRepository : BaseNPocoRepository<T_AccountMapper>
, IRetrieveMany<T_AccountMapper>
, IRetrieve<AccountId, T_AccountMapper>
{
public T_AccountMapperRepository(IDatabase database) : base(database)
{
}
public async Task<T_AccountMapper> Retrieve(AccountId input)
{
return await Database.QueryAsync<T_AccountMapper>()
.First(x => x.AccountId == input)
.ConfigureAwait(false);
}
}
The code to fetch the different values can be logically executed in parallel, and I am wondering if this implementation is the correct pattern to do so: (the various methods being called on the Account object are thread-safe as well)
public async Task<Account> Retrieve(AccountId input1, DateTime input2)
{
var account = new Account();
await Task.WhenAll(
_tAccountMapperRepo.Retrieve(input1)
.ContinueWith(async result => account.SetAccountMap(await TranslateAccountMap(result))),
_tPaymentRedirectionRepo.Retrieve(input1, input2)
.ContinueWith(async result => account.ChangePayerToAccount(await TranslatePaymentRedirection(result))),
_tAccountAncestorRepo.Retrieve(input1, input2)
.ContinueWith(async result => await _tAccountMapperRepo.Retrieve((await result).AncestorId))
.ContinueWith(async result => account.MoveAccountToNewParentAccountMap(await TranslateAccountMap(await result))),
_tAccountRepo.Retrieve(input1)
.ContinueWith(async result => await _tAccountTypeRepo.Retrieve((await result).TypeId))
.ContinueWith(async result => account.SetAccountType(await TranslateAccountType(await result)))
);
return account;
}
Any method that is labelled with TranslateXXX all look similar to this:
private static async Task<AccountMap> TranslateAccountMap(Task<T_AccountMapper> mapTask)
{
if (!mapTask.IsCompletedSuccessfully)
throw new InvalidOperationException(nameof(mapTask), mapTask.Exception);
var map = await mapTask;
return new AccountMap(map.AccountId, map.Login, map.Namespace);
}
My main concerns are mixing Task and async/await, and whether or not my async & awaiting is re-introducing an element of synchronicity in what I'm hoping to make a very asynchronous process. My end goal is that as many various properties as possible are fetched in parallel and assembled
result
in yourContinueWith
delegates. They are really misleading. Because you haven't specifiedTaskContinuationOptions.OnlyOnRanToCompletion
that's why your ancestor / antecedent task could have failed. IMO, calling a failed task asresult
is misleading. \$\endgroup\$ – Peter Csala Sep 30 '20 at 13:19