dynamicexpresso / DynamicExpresso

C# expressions interpreter
http://dynamic-expresso.azurewebsites.net/
MIT License
2.02k stars 379 forks source link

LINQ Enumerable Extensions Do Not Work With ExpandoObject Collection Properties #304

Open RonAmihai opened 9 months ago

RonAmihai commented 9 months ago

LINQ extensions within expressions do not work when applied to properties of ExpandoObject. (For comparison - ExpressionEvaluator does support that with OptionInstanceMethodsCallActive)

Given the following code:

dynamic dynamicData = new ExpandoObject();
dynamicData.some = new List<string> { "one", "two", "two", "three" };

var interpreter = new Interpreter(InterpreterOptions.LambdaExpressions).Reference(typeof(Enumerable));
interpreter.SetVariable("data", dynamicData);

// Works since 'Contains' is a method of List
var nonLinqMethodResult = interpreter.Eval("data.some.Contains(\"two\")");

// Throws since 'Any' is an extension method of Enumerable
var linqMethodResult = interpreter.Eval("data.some.Any(x => x == \"two\")");

The following exception is thrown:

Unhandled exception. DynamicExpresso.Exceptions.ParseException: Invalid Operation (at index 30).
 ---> System.InvalidOperationException: Extension node must override the property Expression.NodeType.
   at System.Linq.Expressions.Expression.get_NodeType()
   at System.Dynamic.Utils.ExpressionUtils.RequiresCanRead(Expression expression, String paramName, Int32 idx)
   at System.Linq.Expressions.ExpressionExtension.ValidateDynamicArgument(Expression arg, String paramName, Int32 index)
   at System.Linq.Expressions.ExpressionExtension.MakeDynamic(CallSiteBinder binder, Type returnType, ReadOnlyCollection`1 arguments)
   at DynamicExpresso.Parsing.Parser.ParseDynamicMethodInvocation(Type type, Expression instance, String methodName, Expression[] args)
   at DynamicExpresso.Parsing.Parser.ParseMethodInvocation(Type type, Expression instance, Int32 errorPos, String methodName, TokenId open, String openExpected, TokenId close, String closeExpected)
   at DynamicExpresso.Parsing.Parser.ParseMethodInvocation(Type type, Expression instance, Int32 errorPos, String methodName)
   at DynamicExpresso.Parsing.Parser.ParseMemberAccess(Type type, Expression instance)
   at DynamicExpresso.Parsing.Parser.ParseMemberAccess(Expression instance)
   at DynamicExpresso.Parsing.Parser.ParsePrimary()
   at DynamicExpresso.Parsing.Parser.ParseUnary()
   at DynamicExpresso.Parsing.Parser.ParseMultiplicative()
   at DynamicExpresso.Parsing.Parser.ParseAdditive()
   at DynamicExpresso.Parsing.Parser.ParseShift()
   at DynamicExpresso.Parsing.Parser.ParseTypeTesting()
   at DynamicExpresso.Parsing.Parser.ParseComparison()
   at DynamicExpresso.Parsing.Parser.ParseLogicalAnd()
   at DynamicExpresso.Parsing.Parser.ParseLogicalXor()
   at DynamicExpresso.Parsing.Parser.ParseLogicalOr()
   at DynamicExpresso.Parsing.Parser.ParseConditionalAnd()
   at DynamicExpresso.Parsing.Parser.ParseConditionalOr()
   at DynamicExpresso.Parsing.Parser.ParseConditional()
   at DynamicExpresso.Parsing.Parser.ParseAssignment()
   at DynamicExpresso.Parsing.Parser.ParseExpressionSegment()
   --- End of inner exception stack trace ---
   at DynamicExpresso.Parsing.Parser.ParseExpressionSegment()
   at DynamicExpresso.Parsing.Parser.ParseExpressionSegment(Type returnType)
   at DynamicExpresso.Parsing.Parser.Parse()
   at DynamicExpresso.Parsing.Parser.Parse(ParserArguments arguments)
   at DynamicExpresso.Interpreter.ParseAsLambda(String expressionText, Type expressionType, Parameter[] parameters)
   at DynamicExpresso.Interpreter.Parse(String expressionText, Type expressionType, Parameter[] parameters)
   at DynamicExpresso.Interpreter.Eval(String expressionText, Type expressionType, Parameter[] parameters)
   at DynamicExpresso.Interpreter.Eval(String expressionText, Parameter[] parameters)
   at Program.<Main>$(String[] args) in /Users/ronam/Projects/ConsoleApp1/ConsoleApp1/Program.cs:line 16
RonAmihai commented 9 months ago

Currently, as a temporary solution (until implementing proper dynamic LINQ support), I've solved that using LINQ extension methods for object.

Then, by using the extensions below, one can define the interpreter as follows to enable dynamic LINQ functionality:

var interpreter = new Interpreter(InterpreterOptions.LateBindObject | InterpreterOptions.LambdaExpressions)
    .Reference(typeof(DynamicLinqExtensions));

Notes:

Implementation:

DynamicLinqExtensions ```cs public static class DynamicLinqExtensions { private static IEnumerable AsEnumerable(this object? source) => source is IEnumerable enumerable ? enumerable.Cast() : throw new InvalidCastException($"Type '{source?.GetType().ToString() ?? "null"}' is not enumerable"); private static object? Aggregate(this object source, Func func) => Enumerable.Aggregate(source.AsEnumerable(), func); private static object? Aggregate(this object source, object? seed, Func func, Func? resultSelector = null) => resultSelector is null ? Enumerable.Aggregate(source.AsEnumerable(), seed, func) : Enumerable.Aggregate(source.AsEnumerable(), seed, func, resultSelector); private static bool Any(this object source, Func? predicate = null) => predicate is null ? Enumerable.Any(source.AsEnumerable()) : Enumerable.Any(source.AsEnumerable(), predicate); private static bool All(this object source, Func predicate) => Enumerable.All(source.AsEnumerable(), predicate); private static IEnumerable Append(this object source, object? element) => Enumerable.Append(source.AsEnumerable(), element); private static IEnumerable Prepend(this object source, object? element) => Enumerable.Prepend(source.AsEnumerable(), element); private static double? Average(this object source, Func? selector = null) => selector is null ? Enumerable.Average(source.AsEnumerable(), AsNumeric) : Enumerable.Average(source.AsEnumerable(), selector); private static IEnumerable Chunk(this object source, int size) => Enumerable.Chunk(source.AsEnumerable(), size); private static IEnumerable Concat(this object first, object? second) => Enumerable.Concat(first.AsEnumerable(), second.AsEnumerable()); private static bool Contains(this object source, object? value) => Enumerable.Contains(source.AsEnumerable(), value); private static int Count(this object source, Func? predicate = null) => predicate is null ? Enumerable.Count(source.AsEnumerable()) : Enumerable.Count(source.AsEnumerable(), predicate); private static long LongCount(this object source, Func? predicate = null) => predicate is null ? Enumerable.LongCount(source.AsEnumerable()) : Enumerable.LongCount(source.AsEnumerable(), predicate); private static bool TryGetNonEnumeratedCount(this object source, out int count) => Enumerable.TryGetNonEnumeratedCount(source.AsEnumerable(), out count); private static IEnumerable DefaultIfEmpty(this object source, object? defaultValue = null) => Enumerable.DefaultIfEmpty(source.AsEnumerable(), defaultValue); private static IEnumerable Distinct(this object source) => Enumerable.Distinct(source.AsEnumerable()); private static IEnumerable DistinctBy(this object source, Func keySelector) => Enumerable.DistinctBy(source.AsEnumerable(), keySelector); private static object? ElementAt(this object source, int index) => Enumerable.ElementAt(source.AsEnumerable(), index); private static object? ElementAtOrDefault(this object source, int index) => Enumerable.ElementAtOrDefault(source.AsEnumerable(), index); private static IEnumerable Except(this object first, object second) => Enumerable.Except(first.AsEnumerable(), second.AsEnumerable()); private static IEnumerable ExceptBy(this object first, object second, Func keySelector) => Enumerable.ExceptBy(first.AsEnumerable(), second.AsEnumerable(), keySelector); private static object? First(this object source, Func? predicate = null) => predicate is null ? Enumerable.First(source.AsEnumerable()) : Enumerable.First(source.AsEnumerable(), predicate); private static object? FirstOrDefault(this object source, object? defaultValue) => Enumerable.FirstOrDefault(source.AsEnumerable(), defaultValue); private static object? FirstOrDefault(this object source, Func? predicate = null, object? defaultValue = null) => predicate is null ? Enumerable.FirstOrDefault(source.AsEnumerable(), defaultValue) : Enumerable.FirstOrDefault(source.AsEnumerable(), predicate, defaultValue); private static IEnumerable> GroupBy(this object source, Func keySelector, Func? elementSelector = null) => elementSelector is null ? Enumerable.GroupBy(source.AsEnumerable(), keySelector) : Enumerable.GroupBy(source.AsEnumerable(), keySelector, elementSelector); private static IEnumerable GroupBy(this object source, Func keySelector, Func, object?> resultSelector) => Enumerable.GroupBy(source.AsEnumerable(), keySelector, resultSelector); private static IEnumerable GroupBy(this object source, Func keySelector, Func elementSelector, Func, object?> resultSelector) => Enumerable.GroupBy(source.AsEnumerable(), keySelector, elementSelector, resultSelector); private static IEnumerable GroupJoin(this object outer, object inner, Func outerKeySelector, Func innerKeySelector, Func, object?> resultSelector) => Enumerable.GroupJoin(outer.AsEnumerable(), inner.AsEnumerable(), outerKeySelector, innerKeySelector, resultSelector); private static IEnumerable Intersect(this object first, object second) => Enumerable.Intersect(first.AsEnumerable(), second.AsEnumerable()); private static IEnumerable IntersectBy(this object first, object second, Func keySelector) => Enumerable.IntersectBy(first.AsEnumerable(), second.AsEnumerable(), keySelector); private static IEnumerable Join(this object outer, object inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector) => Enumerable.Join(outer.AsEnumerable(), inner.AsEnumerable(), outerKeySelector, innerKeySelector, resultSelector); private static object? Last(this object source, Func? predicate = null) => predicate is null ? Enumerable.Last(source.AsEnumerable()) : Enumerable.Last(source.AsEnumerable(), predicate); private static object? LastOrDefault(this object source, object? defaultValue) => Enumerable.LastOrDefault(source.AsEnumerable(), defaultValue); private static object? LastOrDefault(this object source, Func? predicate = null, object? defaultValue = null) => predicate is null ? Enumerable.LastOrDefault(source.AsEnumerable(), defaultValue) : Enumerable.LastOrDefault(source.AsEnumerable(), predicate, defaultValue); private static ILookup ToLookup(this object source, Func keySelector, Func? elementSelector = null) => elementSelector is null ? Enumerable.ToLookup(source.AsEnumerable(), keySelector) : Enumerable.ToLookup(source.AsEnumerable(), keySelector, elementSelector); private static double? Max(this object source, Func? selector = null) => selector is null ? Enumerable.Max(source.AsEnumerable(), AsNumeric) : Enumerable.Max(source.AsEnumerable(), selector); private static object? MaxBy(this object source, Func keySelector) => Enumerable.MaxBy(source.AsEnumerable(), keySelector); private static double? Min(this object source, Func? selector = null) => selector is null ? Enumerable.Min(source.AsEnumerable(), AsNumeric) : Enumerable.Min(source.AsEnumerable(), selector); private static object? MinBy(this object source, Func keySelector) => Enumerable.MinBy(source.AsEnumerable(), keySelector); private static IOrderedEnumerable Order(this object source) => Enumerable.Order(source.AsEnumerable()); private static IOrderedEnumerable OrderBy(this object source, Func keySelector) => Enumerable.OrderBy(source.AsEnumerable(), keySelector); private static IOrderedEnumerable OrderDescending(this object source) => Enumerable.OrderDescending(source.AsEnumerable()); private static IOrderedEnumerable OrderByDescending(this object source, Func keySelector) => Enumerable.OrderByDescending(source.AsEnumerable(), keySelector); private static IOrderedEnumerable ThenBy(this IOrderedEnumerable source, Func keySelector) => Enumerable.ThenBy(source, keySelector); private static IOrderedEnumerable ThenByDescending(this IOrderedEnumerable source, Func keySelector) => Enumerable.ThenByDescending(source, keySelector); private static IEnumerable Reverse(this object source) => Enumerable.Reverse(source.AsEnumerable()); private static IEnumerable Select(this object source, Func selector) => Enumerable.Select(source.AsEnumerable(), selector); private static IEnumerable Select(this object source, Func selector) => Enumerable.Select(source.AsEnumerable(), selector); private static IEnumerable SelectMany(this object source, Func> selector) => Enumerable.SelectMany(source.AsEnumerable(), selector); private static IEnumerable SelectMany(this object source, Func> selector) => Enumerable.SelectMany(source.AsEnumerable(), selector); private static IEnumerable SelectMany(this object source, Func> collectionSelector, Func resultSelector) => Enumerable.SelectMany(source.AsEnumerable(), collectionSelector, resultSelector); private static IEnumerable SelectMany(this object source, Func> collectionSelector, Func resultSelector) => Enumerable.SelectMany(source.AsEnumerable(), collectionSelector, resultSelector); private static bool SequenceEqual(this object first, object second) => Enumerable.SequenceEqual(first.AsEnumerable(), second.AsEnumerable()); private static object? Single(this object source, Func? predicate = null) => predicate is null ? Enumerable.Single(source.AsEnumerable()) : Enumerable.Single(source.AsEnumerable(), predicate); private static object? SingleOrDefault(this object source, object? defaultValue) => Enumerable.SingleOrDefault(source.AsEnumerable(), defaultValue); private static object? SingleOrDefault(this object source, Func? predicate = null, object? defaultValue = null) => predicate is null ? Enumerable.SingleOrDefault(source.AsEnumerable(), defaultValue) : Enumerable.SingleOrDefault(source.AsEnumerable(), predicate, defaultValue); private static IEnumerable Skip(this object source, int count) => Enumerable.Skip(source.AsEnumerable(), count); private static IEnumerable SkipWhile(this object source, Func predicate) => Enumerable.SkipWhile(source.AsEnumerable(), predicate); private static IEnumerable SkipWhile(this object source, Func predicate) => Enumerable.SkipWhile(source.AsEnumerable(), predicate); private static IEnumerable SkipLast(this object source, int count) => Enumerable.SkipLast(source.AsEnumerable(), count); private static double? Sum(this object source, Func? selector = null) => selector is null ? Enumerable.Sum(source.AsEnumerable(), AsNumeric) : Enumerable.Sum(source.AsEnumerable(), selector); private static IEnumerable Take(this object source, int count) => Enumerable.Take(source.AsEnumerable(), count); private static IEnumerable TakeLast(this object source, int count) => Enumerable.TakeLast(source.AsEnumerable(), count); private static IEnumerable TakeWhile(this object source, Func predicate) => Enumerable.TakeWhile(source.AsEnumerable(), predicate); private static IEnumerable TakeWhile(this object source, Func predicate) => Enumerable.TakeWhile(source.AsEnumerable(), predicate); private static object?[] ToArray(this object source) => Enumerable.ToArray(source.AsEnumerable()); private static List ToList(this object source) => Enumerable.ToList(source.AsEnumerable()); private static Dictionary ToDictionary(this object source, Func keySelector, Func? elementSelector = null) => elementSelector is null ? Enumerable.ToDictionary(source.AsEnumerable(), keySelector) : Enumerable.ToDictionary(source.AsEnumerable(), keySelector, elementSelector); private static HashSet ToHashSet(this object source) => Enumerable.ToHashSet(source.AsEnumerable()); private static IEnumerable Union(this object first, object second) => Enumerable.Union(first.AsEnumerable(), second.AsEnumerable()); private static IEnumerable UnionBy(this object first, object second, Func keySelector) => Enumerable.UnionBy(first.AsEnumerable(), second.AsEnumerable(), keySelector); private static IEnumerable Where(this object source, Func predicate) => Enumerable.Where(source.AsEnumerable(), predicate); private static IEnumerable Where(this object source, Func predicate) => Enumerable.Where(source.AsEnumerable(), predicate); private static IEnumerable Zip(this object first, object second, Func resultSelector) => Enumerable.Zip(first.AsEnumerable(), second.AsEnumerable(), resultSelector); private static IEnumerable<(object? First, object? Second)> Zip(this object first, object second) => Enumerable.Zip(first.AsEnumerable(), second.AsEnumerable()); private static IEnumerable<(object? First, object? Second, object? Third)> Zip(this object first, object second, object third) => Enumerable.Zip(first.AsEnumerable(), second.AsEnumerable(), third.AsEnumerable()); private static double? AsNumeric(object? item) => item is null ? null : Convert.ToDouble(item); } ```
metoule commented 3 months ago

It's worth noting that the C# compiler doesn't allow it either:

dynamic dynamicData = new ExpandoObject();
dynamicData.Some = new List<string> { "one", "two", "two", "three" };

var result = dynamicData.Some.Any(x => x == "two");

raises compiler error

error CS1977: Cannot use a lambda expression as an argument to a dynamically dispatched operation
 without first casting it to a delegate or expression tree type.
davideicardi commented 3 months ago

If C# compiler doesn't allow this scenario, I think we can ignore it. @RonAmihai using ExpandoObject really necessary in your scenario? Maybe you can create some custom type. This will also improve performance and type safety.

RonAmihai commented 3 months ago

@davideicardi I've ended up implementing an optimized custom expression tree compiler for my exact scenario (object with nested Dictionary<string, object>).

I agree that ExpandoObject should be avoided in general. However, in cases where it can't be avoided, LINQ support can be an optional feature—not that mandatory, though.