web-dev-qa-db-fra.com

Attente C # en attente d'utilisation de LINQ ForEach ()

J'ai le code suivant qui utilise correctement le paradigme async/wait.

internal static async Task AddReferencseData(ConfigurationDbContext context)
{
    foreach (var sinkName in RequiredSinkTypeList)
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    }
}

Quelle est la manière équivalente d’écrire ceci si, au lieu d’utiliser foreach (), je souhaite utiliser LINQ ForEach ()? Celui-ci, par exemple, donne une erreur de compilation.

internal static async Task AddReferenceData(ConfigurationDbContext context)
{
    RequiredSinkTypeList.ForEach(
        sinkName =>
        {
            var sinkType = new SinkType() { Name = sinkName };
            context.SinkTypeCollection.Add(sinkType);
            await context.SaveChangesAsync().ConfigureAwait(false);
        });
}

Le seul code que j'ai eu à travailler sans erreur de compilation est la suivante.

internal static void AddReferenceData(ConfigurationDbContext context)
{
    RequiredSinkTypeList.ForEach(
        async sinkName =>
        {
            var sinkType = new SinkType() { Name = sinkName };
            context.SinkTypeCollection.Add(sinkType);
            await context.SaveChangesAsync().ConfigureAwait(false);
        });
}

Je crains que cette méthode n'ait pas de signature asynchrone, mais uniquement le corps. Est-ce l’équivalent correct de mon premier bloc de code ci-dessus? 

13
SamDevx

Non, ce n'est pas. Cette ForEach ne prend pas en charge async-await et requiert que votre lambda soit async void qui devrait uniquement être utilisé pour les gestionnaires d'événements. Son utilisation lancera toutes vos opérations async simultanément et ne les attendra pas.

Vous pouvez utiliser une constante foreach comme vous l’avez fait, mais si vous souhaitez une méthode d’extension, vous avez besoin d’une version spéciale de async.

Vous pouvez en créer une vous-même qui itère sur les éléments, exécute une opération async et awaits:

public async Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    foreach (var item in enumerable)
    {
        await action(item);
    }
}

Usage:

internal static async Task AddReferencseData(ConfigurationDbContext context)
{
    await RequiredSinkTypeList.ForEachAsync(async sinkName =>
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    });
}

Une implémentation différente (et généralement plus efficace) de ForEachAsync consisterait à démarrer toutes les opérations async et seulement ensuite await toutes ensemble, mais cela n'est possible que si vos actions peuvent s'exécuter simultanément, ce qui n'est pas toujours le cas (Entity Framework, par exemple):

public Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    return Task.WhenAll(enumerable.Select(item => action(item)));
}

Comme indiqué dans les commentaires, vous ne souhaiterez probablement pas utiliser SaveChangesAsync dans une structure foreach. Préparer vos modifications puis les enregistrer toutes en même temps sera probablement plus efficace. 

18
i3arnon

Pourquoi ne pas utiliser la méthode AddRange ()? 

context.SinkTypeCollection.AddRange(RequiredSinkTypeList.Select( sinkName  => new SinkType() { Name = sinkName } );

await context.SaveChangesAsync().ConfigureAwait(false);
0
Tzu

L'exemple initial avec foreach attend effectivement après chaque itération de boucle. Le dernier exemple appelle List<T>.ForEach() qui prend une valeur Action<T>, que votre lambda asynchrone compilera dans un délégué void, par opposition à la règle Task.

Effectivement, la méthode ForEach() invoquera des "itérations" une par une sans attendre que chacune d’elles se termine. Cela se propage également à votre méthode, ce qui signifie que AddReferenceData() peut se terminer avant le travail.

Donc non, ce n'est pas un équivalent et se comporte très différemment . En fait, en supposant qu'il s'agisse d'un contexte EF, il va exploser car il ne peut pas être utilisé simultanément sur plusieurs threads. 

Lisez également http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx mentionné par Deepu pour expliquer pourquoi il vaut probablement mieux s'en tenir à foreach.

0
Jacek Gorgoń

Pour écrire une attente dans une méthode, votre méthode doit être marquée comme étant asynchrone. Lorsque vous écrivez une méthode ForEach, vous écrivez une attente dans votre expression labda, qui est en d'autres termes une méthode différente que vous appelez à partir de votre méthode. Vous devez déplacer cette expression lamdba vers la méthode et marquer cette méthode comme asynchrone et aussi comme l'a déclaré @ i3arnon. Vous devez disposer de la méthode ForEach marquée asynchrone qui n'est pas encore fournie par .Net Framework. Vous devez donc l'écrire vous-même.

0
Pramod Jangam

Merci à tous pour vos commentaires. En prenant la partie "save"} en dehors de la boucle, je pense que les 2 méthodes suivantes sont maintenant équivalentes, l'une utilisant foreach(), une autre utilisant .ForEach(). Cependant, comme mentionné par Deepu, je vais lire le post d’Eric sur pourquoi foreach pourrait être meilleur.

public static async Task AddReferencseData(ConfigurationDbContext context)
{
    RequiredSinkTypeList.ForEach(
        sinkName =>
        {
            var sinkType = new SinkType() { Name = sinkName };
            context.SinkTypeCollection.Add(sinkType);
        });
    await context.SaveChangesAsync().ConfigureAwait(false);
}

public static async Task AddReferenceData(ConfigurationDbContext context)
{
    foreach (var sinkName in RequiredSinkTypeList)
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
    }
    await context.SaveChangesAsync().ConfigureAwait(false);
}
0
SamDevx