C #: un élément avec la même clé a déjà été ajouté lors de la compilation d'une expression

c# exception expression expression-trees lambda

Question

Ok, voici une question délicate. J'espère qu'il y a un gourou de l'expression ici qui peut repérer ce que je fais mal ici, parce que je ne comprends tout simplement pas.

Je construis des expressions que j'utilise pour filtrer les requêtes. Pour faciliter ce processus, j’ai Expression<Func<T, bool>> méthodes d’extension Expression<Func<T, bool>> qui rendent mon code plus propre et qui, jusqu’à présent, fonctionnent correctement. J'ai écrit des tests pour chacun d'eux sauf un, pour lequel j'ai écrit un pour aujourd'hui. Et ce test échoue complètement avec une ArgumentException avec une longue trace de pile. Et je ne comprends tout simplement pas. Surtout depuis que j'utilise cette méthode depuis un certain temps avec succès dans mes requêtes!

Quoi qu'il en soit, voici la trace de la pile obtenue lors de l'exécution du test:

failed: System.ArgumentException : An item with the same key has already been added.
    at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
    at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
    at System.Linq.Expressions.ExpressionCompiler.PrepareInitLocal(ILGenerator gen, ParameterExpression p)
    at System.Linq.Expressions.ExpressionCompiler.GenerateInvoke(ILGenerator gen, InvocationExpression invoke, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.Generate(ILGenerator gen, Expression node, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateBinary(ILGenerator gen, BinaryExpression b, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.Generate(ILGenerator gen, Expression node, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateUnliftedAndAlso(ILGenerator gen, BinaryExpression b)
    at System.Linq.Expressions.ExpressionCompiler.GenerateAndAlso(ILGenerator gen, BinaryExpression b, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateBinary(ILGenerator gen, BinaryExpression b, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.Generate(ILGenerator gen, Expression node, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateUnliftedOrElse(ILGenerator gen, BinaryExpression b)
    at System.Linq.Expressions.ExpressionCompiler.GenerateOrElse(ILGenerator gen, BinaryExpression b, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateBinary(ILGenerator gen, BinaryExpression b, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.Generate(ILGenerator gen, Expression node, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateInvoke(ILGenerator gen, InvocationExpression invoke, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.Generate(ILGenerator gen, Expression node, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateUnliftedAndAlso(ILGenerator gen, BinaryExpression b)
    at System.Linq.Expressions.ExpressionCompiler.GenerateAndAlso(ILGenerator gen, BinaryExpression b, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateBinary(ILGenerator gen, BinaryExpression b, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.Generate(ILGenerator gen, Expression node, StackType ask)
    at System.Linq.Expressions.ExpressionCompiler.GenerateLambda(LambdaExpression lambda)
    at System.Linq.Expressions.ExpressionCompiler.CompileDynamicLambda(LambdaExpression lambda)
    at System.Linq.Expressions.Expression`1.Compile()
    PredicateTests.cs(257,0): at Namespace.ExpressionExtensionsTests.WhereWithin_CollectionIsFilteredAsExpected()

Le test lui-même ressemble à ce qui suit, il échoue à l'instruction Compile:

[Test]
public void WhereWithin_CollectionIsFilteredAsExpected()
{
    var range = new[] { Range.Create(2, 7), Range.Create(15, 18) };

    var predicate = Predicate
        .Create<int>(x => x % 2 == 0)
        .AndWithin(range, x => x)
        .Compile();

    var actual = Enumerable.Range(0, 20)
        .Where(predicate)
        .ToArray();

    Assert.That(actual, Is.EqualTo(new[] { 2, 4, 6, 16, 18 }));
}

Je ne comprends tout simplement pas le message d'erreur. Je pensais que cela pouvait avoir à voir avec le fait que j'utilisais toujours x comme nom de paramètre, mais cela ne semblait pas aider lorsque j'essayais de les échanger. Ce qui le rend encore plus étrange pour moi, c'est que j'utilise cette méthode depuis un certain temps déjà dans les requêtes Linq2Sql plus volumineuses et qu'elles ont toujours bien fonctionné. Donc, dans mon test, j'ai essayé de ne pas compiler l'expression et d'utiliser AsQueryable afin de pouvoir l'utiliser à la place. Mais cela vient de faire que l'exception se produise à la place dans ToArray . Qu'est-ce qui se passe ici? Comment puis-je réparer cela?

Vous pouvez trouver le code fautif et ennuyeux dans le fichier zip en dessous de la ligne:


Remarque: j'avais posté une partie du code associé ici, mais après quelques commentaires, j'ai décidé d'extraire le code dans son propre projet, qui montre l'exception plus clairement. Et plus important encore, cela peut être exécuté, compilé et débogué.


Mise à jour: simplifiez davantage l’exemple de projet avec certaines des suggestions de @Mark. C'est comme supprimer la classe d'intervalle et simplement coder en dur l'intervalle constant. Nous avons également ajouté un autre exemple d'utilisation exacte de la même méthode. Ainsi, l'utilisation de la méthode AndWithin provoque le blocage de l'application, tandis que l'utilisation de la méthode WhereWithin fonctionne réellement correctement. Je me sens plutôt désemparé!

Réponse populaire

J'ai un peu remanié vos méthodes pour rendre le compilateur un peu plus heureux:

public static Expression<Func<TSubject, bool>> AndWithin<TSubject, TField>(
    this Expression<Func<TSubject, bool>> original,
    IEnumerable<Range<TField>> range, Expression<Func<TSubject, TField>> field) where TField : IComparable<TField>
{
  return original.And(range.GetPredicateFor(field));
}


static Expression<Func<TSource, bool>> GetPredicateFor<TSource, TValue>
    (this IEnumerable<Range<TValue>> range, Expression<Func<TSource, TValue>> selector) where TValue : IComparable<TValue>
{
  var param = Expression.Parameter(typeof(TSource), "x");

  if (range == null || !range.Any())
    return Expression.Lambda<Func<TSource, bool>>(Expression.Constant(false), param);

  Expression body = null;
  foreach (var r in range)
  {
    Expression<Func<TValue, TValue, TValue, bool>> BT = (val, min, max) => val.CompareTo(min) >= 0 && val.CompareTo(max) <= 0;
    var newPart = Expression.Invoke(BT, param,
                                      Expression.Constant(r.Start, typeof(TValue)),
                                      Expression.Constant(r.End, typeof(TValue)));

    body = body == null ? newPart : (Expression)Expression.OrElse(body, newPart);
  }

  return Expression.Lambda<Func<TSource, bool>>(body, param);
}

Les deux ont la restriction IComparable<TValue> (le seul changement à la première méthode).

Dans le second, je fais la comparaison via une implémentation de Func Expression, remarquez que la fonction est créée dans la boucle ... c’est la deuxième addition de cette expression (ce qui est identique, dans l’ancienne méthode) ça explose.

Avertissement: Je ne comprends toujours pas pourquoi votre méthode précédente n'a pas fonctionné, mais cette approche alternative contourne le problème. Faites-moi savoir si ce n'est pas ce que vous recherchez et nous essaierons autre chose.

En outre, sur kudos pose une question bien, un exemple de projet est exemplaire.



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow