Assemblage asynchrone d'un objet complexe
Vérification de l'intégrité nécessaire, car je ne suis toujours pas familier avec les nuances derrière async / await et comment elles se rapportent au code Task / TPL plus traditionnel.
J'ai une couche de référentiel de haut niveau qui assemble un objet métier à partir de divers référentiels de niveau inférieur. En tant que décision commerciale, nous effectuons des recherches clés au niveau de la couche de données et assemblons l'objet composite dans le code. Dans le code suivant, "Account" est l'objet métier et les différents objets _tAccountXXXRepo sont tous modelés de la même façon (nous utilisons 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);
}
}
Le code pour récupérer les différentes valeurs peut être exécuté logiquement en parallèle, et je me demande si cette implémentation est le bon modèle pour le faire: (les différentes méthodes appelées sur l'objet Account sont également thread-safe)
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;
}
Toutes les méthodes étiquetées avec TranslateXXX ressemblent toutes à ceci:
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);
}
Mes principales préoccupations sont de mélanger Task et async / await, et de savoir si mon asynchrone et mon attente réintroduisent un élément de synchronicité dans ce que j'espère faire d'un processus très asynchrone. Mon objectif final est que le plus grand nombre de propriétés possibles soient récupérées en parallèle et assemblées
Réponses
Quant à moi, ça a l'air bien sauf une chose. Vous avez déjà terminé Tasken .ContinueWithbloc, vous pouvez donc utiliser .Resultalors au lieu de awaitet éviter asyncState Machine redondante .
Au fait, si vous utilisez async/await, il n'y a probablement pas de suite nécessaire.
Considérez cette implémentation sans continuation du 1er Task.WhenAll()travail.
public async Task<Account> Retrieve(AccountId input1, DateTime input2)
{
var account = new Account();
// probably try-catch wrapper maybe useful here as replacement of !mapTask.IsCompletedSuccessfully
await Task.WhenAll((Func<Task>)(async () =>
{
T_AccountMapper map = await _tAccountMapperRepo.Retrieve(input1);
// continuation:
AccountMap accountMap = new AccountMap(map.AccountId, map.Login, map.Namespace);
account.SetAccountMap(accountMap);
})
//, (Func<Task>)(async() => {...})
);
return account;
}
Comme vous pouvez le voir, la TranslateAccountMapméthode n'est plus nécessaire.
Enfin, c'est à vous.
PS Voici un lien utile .
Sur la base des commentaires d'Aepot et des leçons d' AspNetCore Async Guidance , voici ce que j'ai refactoré le code en:
public async Task<Account> RetrieveAsync(AccountId input1, DateTime input2)
{
Account account = new Account();
await Task.WhenAll(
SetAccountMap(account, input1, input2),
ChangePayer(account, input1, input2),
SetParentMap(account, input1, input2),
SetAccountType(account, input1, input2)
);
return account;
}
private async Task SetAccountType(Account account, AccountId accountId, DateTime dateTime)
{
var baseAccount = await _tAccountRepo.RetrieveAsync(accountId);
var accountType = await _tAccountTypeRepo.RetrieveAsync(baseAccount.TypeId);
var convertedAccountType = _accountTypeConverter.Convert(accountType);
account.SetAccountType(convertedAccountType);
}
private async Task SetParentMap(Account account, AccountId accountId, DateTime dateTime)
{
var accountAncestor = await _tAccountAncestorRepo.RetrieveAsync(accountId, dateTime);
var accountMap = await _tAccountMapperRepo.RetrieveAsync(accountAncestor.DescendentId);
var convertedAccountMap = _accountMapConverter.Convert(accountMap);
account.MoveAccountToNewParentAccountMap(convertedAccountMap);
}
private async Task ChangePayer(Account account, AccountId accountId, DateTime dateTime)
{
var paymentRedirection = await _tPaymentRedirectionRepo.RetrieveAsync(accountId, dateTime);
var convertedPaymentRedirection = _paymentRedirectionConverter.Convert(paymentRedirection);
account.ChangePayerToAccount(convertedPaymentRedirection);
}
private async Task SetAccountMap(Account account, AccountId accountId, DateTime dateTime)
{
var accountMap = await _tAccountMapperRepo.RetrieveAsync(accountId);
var convertedAccountMap = _accountMapConverter.Convert(accountMap);
account.SetAccountMap(convertedAccountMap);
}
Ici, j'ai deux versions alternatives du code révisé de Bryan Boettcher.
Dans sa version, il y a 4 fonctions ETL et elles fonctionnent simultanément.
Dans mes solutions, seule la partie Extract s'exécute simultanément. Les parties Transformation et Load sont exécutées séquentiellement.
Alternative n ° 1
S'il vous plaît garder à l' esprit que je devais utiliser certains noms composés de type (comme: AccountType, PaymentRedirection) parce que je ne pouvais pas dire les types exacts en fonction du code fourni.
public async Task<Account> RetrieveAsync(AccountId accountId, DateTime dateTime)
{
//Extract
var accountTypeJob = RetrieveAccountType(accountId);
var paymentRedirectionJob = RetrievePaymentRedirection(accountId, dateTime);
var accountMapJob = RetrieveAccountMap(accountId);
var parentAccountMapJob = RetrieveAccountMapForParent(accountId, dateTime);
//Transform
var accountType = await accountTypeJob;
var convertedAccountType = _accountTypeConverter.Convert(accountType);
var paymentRedirection = await paymentRedirectionJob;
var convertedPaymentRedirection = _paymentRedirectionConverter.Convert(paymentRedirection);
var accountMap = await accountMapJob;
var convertedAccountMap = _accountMapConverter.Convert(accountMap);
var parentAccountMap = await parentAccountMapJob;
var convertedParentAccountMap = _accountMapConverter.Convert(parentAccountMap);
//Load
var account = new Account();
account.SetAccountType(convertedAccountType);
account.ChangePayerToAccount(convertedPaymentRedirection);
account.SetAccountMap(convertedAccountMap);
account.MoveAccountToNewParentAccountMap(convertedParentAccountMap);
return account;
}
private async Task<AccountType> RetrieveAccountType(AccountId accountId)
{
var baseAccount = await _tAccountRepo.RetrieveAsync(accountId);
return await _tAccountTypeRepo.RetrieveAsync(baseAccount.TypeId);
}
private async Task<AccountMap> RetrieveAccountMapForParent(AccountId accountId, DateTime dateTime)
{
var accountAncestor = await _tAccountAncestorRepo.RetrieveAsync(accountId, dateTime);
return await _tAccountMapperRepo.RetrieveAsync(accountAncestor.DescendentId);
}
private async Task<PaymentRedirection> RetrievePaymentRedirection(AccountId accountId, DateTime dateTime)
{
return await _tPaymentRedirectionRepo.RetrieveAsync(accountId, dateTime);
}
private async Task<AccountMap> RetrieveAccountMap(AccountId accountId)
{
return await _tAccountMapperRepo.RetrieveAsync(accountId);
}
Comme vous pouvez le voir, j'ai 4 RetrieveXYZfonctions et celles-ci sont responsables du chargement des données. Je suppose que ce sont des opérations d'E / S, donc elles ne sont pas liées au processeur (la concurrence n'est pas limitée par les cœurs disponibles).
Pendant la phase d' extraction , je lance tous les Tasks, il n'est donc pas nécessaire d'appeler explicitement Task.WhenAllla phase de transformation, j'accède aux données récupérées via les awaitmots - clés.
Alternative n ° 2
Avec une simple fonction d'assistance, nous pouvons rendre la RetrieveAsyncfonction plus nette et plus concise.
public static class TaskExtensions
{
public static async Task<(T1, T2, T3, T4)> WhenAll<T1, T2, T3, T4>(Task<T1> t1, Task<T2> t2, Task<T3> t3, Task<T4> t4)
{
return (await t1, await t2, await t3, await t4);
}
}
En tirant parti de cette méthode d'assistance, la nouvelle version de RetrieveAsyncressemblerait à ceci:
public async Task<Account> RetrieveAsync(AccountId accountId, DateTime dateTime)
{
//Extract
var (accountType, paymentRedirection, accountMap, parentAccountMap) = await TaskExtensions.WhenAll(
RetrieveAccountType(accountId), RetrievePaymentRedirection(accountId, dateTime),
RetrieveAccountMap(accountId), RetrieveAccountMapForParent(accountId, dateTime));
//Transform
var convertedAccountType = _accountTypeConverter.Convert(accountType);
var convertedPaymentRedirection = _paymentRedirectionConverter.Convert(paymentRedirection);
var convertedAccountMap = _accountMapConverter.Convert(accountMap);
var convertedParentAccountMap = _accountMapConverter.Convert(parentAccountMap);
//Load
var account = new Account();
account.SetAccountType(convertedAccountType);
account.ChangePayerToAccount(convertedPaymentRedirection);
account.SetAccountMap(convertedAccountMap);
account.MoveAccountToNewParentAccountMap(convertedParentAccountMap);
return account;
}
Avantages
- Seules les opérations liées aux E / S s'exécutent simultanément
- Chaque fonction d'assistance a une responsabilité unique
- La fonction de niveau supérieur agit en tant que coordinateur
- Il n'y a pas de ressource partagée, nous n'avons donc pas à nous soucier autant de la sécurité des threads
Désavantages
- Si les parties Transform et / ou Load sont également liées aux E / S, cette approche serait plus lente
- Il ne s'appuie pas sur les fonctionnalités intégrées
Task.WhenAllplutôt que sur lesValueTuplecapacités de déconstruction de C # 7 et, par conséquent, il ne peut pas être utilisé dans les anciennes versions de .NET Framework.