首页 > 解决方案 > 如何使用 DynamicObject 在 LINQ 表达式中强制“动态”行为

问题描述

假设我有一个D派生自DynamicObject. 目的是D包装一些对象,以暴露比包装对象中最初可用的属性更多的属性。

我们还假设以下表达式编译并执行,并且变量值1符合预期。

var age = (int)((dynamic)new D(new Person{Age = 45})).Age;

现在,如果我尝试构建一个使用D而不是Person直接使用的表达式:

var p = Expression.Parameter(typeof(D), "p");
var e = System.Linq.Dynamic.DynamicExpression.ParseLambda(new[] { p }, null, "p.Age > 45");

我很高兴ParseException告诉我“'D'类型中不存在属性或字段'Age'”。typeof(D)typeof(Person)作品代替。

如何构建一个视为的p表达式dynamic?我已经尝试了很多东西,最终System.Linq.Dynamic从 NuGet 使用,但仍然没有运气。

编辑:这是使用的类。编辑:我找到了一个解决方案,DynamicExpression.CreateClass()但我不太高兴,因为它有效地复制了我的对象,有时可能有很深的层次结构。此外,如果表达式只涉及一个属性,这将是非常低效的。

public class D : DynamicObject
{
    Person _p;

    public D(Person p)
    {
        _p = p;
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return new [] {"Age"};
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var name = binder.Name;
        if (name == "Age")
        {
            result = _p.Age;
            return true;
        }

        result = null;
        return false;
    }
}

class Person
{
   public int Age { get; set; }
}

标签: c#.net

解决方案


答案更新为原始(中风)被证明是无关紧要的。

我做了另一项研究,可能找到了解决您问题的方法。你的问题缺乏更多的背景,但我想它对你有用。

解决方案

使动态行为起作用的第一件事是IDynamicMetaObjectProvider在类中实现接口。该接口表示动态对象,并具有GetMetaObject返回的单一方法DynamicMetaObject。在这种情况下,PersonMetaObject 类。

public sealed class Person : IDynamicMetaObjectProvider {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }

    public int GetAge() {
        return DateTime.Today.Year - BirthDate.Year;
    }

    DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) {
        return new PersonMetaObject(parameter, this);
    }
}

现在您必须实现对PersonMetaObject类的覆盖以允许自定义运行时行为。(作为有趣的部分阅读)

internal class PersonMetaObject : DynamicMetaObject {
    private TypeInfo _typeInfo = typeof(Person).GetTypeInfo();

    public PersonMetaObject(Expression parameter, Person value)
        : base(parameter, BindingRestrictions.Empty, value) { }

    public override DynamicMetaObject BindGetMember(GetMemberBinder binder) {
        Expression self = GetSelfExpression();

        string propertyName = binder.Name;

        return BindGetInternal(self, propertyName);
    }

    public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes) {
        Expression self = GetSelfExpression();

        string propertyName = indexes.First().Value.ToString();

        return BindGetInternal(self, propertyName);
    }

    private DynamicMetaObject BindGetInternal(Expression self, string propertyName) {
        switch (propertyName) {
            // Dynamic value computed in runtime
            case "FullName": {
                // Get FirstName property
                Expression firstName = Expression.Property(self, nameof(Person.FirstName));
                // Get LastName property
                Expression lastName = Expression.Property(self, nameof(Person.LastName));
                // Create constant containing space character
                Expression space = Expression.Constant(" ");


                Expression stringArray = Expression.NewArrayInit(typeof(string), firstName, lastName);

                Expression fullName = Expression.Call(null, typeof(string).GetMethod(nameof(String.Join), new[] { typeof(string), typeof(string[]) }), space, stringArray);

                // Create and return dynamic object metadata
                return new DynamicMetaObject(Expression.Convert(fullName, typeof(object)), BindingRestrictions.GetTypeRestriction(Expression, LimitType));
            }
            // Defined method invoked
            case "Age": {
                // Get method info using type reflection
                MethodInfo getAge = _typeInfo.GetMethod(nameof(Person.GetAge));

                // Call reflected method
                Expression age = Expression.Call(self, getAge);

                // Create and return dynamic object metadata
                return new DynamicMetaObject(Expression.Convert(age, typeof(object)), BindingRestrictions.GetTypeRestriction(Expression, LimitType));
            }
            // 
            default: {
                // if none case was hit we need to make sure to return declared property if exists
                if(_typeInfo.GetMembers().Any(m => m.Name == propertyName)) {
                    return new DynamicMetaObject(Expression.Convert(Expression.Property(self, propertyName), typeof(object)), BindingRestrictions.GetTypeRestriction(Expression, LimitType));
                }
                // If we get here something is wrong. To avoid exception we just return default value. But you would like to change it accordingly   
                // Create default object
                Expression @default = Expression.Default(typeof(object));

                // Create and return dynamic object metadata
                return new DynamicMetaObject(@default, BindingRestrictions.GetTypeRestriction(Expression, LimitType));
            }
        }
    }
    private Expression GetSelfExpression() {
        return Expression.Convert(Expression, LimitType);
    }
}

最后一部分是编写一些逻辑来测试它是否有效。在下面的代码中,您可以看到有两个使用 Func 的 IEnumerable 过滤器和两个 IQueryable。IQueryable 正在使用以编程方式创建的表达式。

class Program {
        static void Main(string[] args) {
            // Array of concrete types assigned to dynamic enumerable
            IEnumerable<dynamic> enumerable = new Person[] {
                new Person { BirthDate = new DateTime(1975, 8, 14), FirstName = "Alan",  LastName = "Smith" },
                new Person { BirthDate = new DateTime(2006, 1, 26), FirstName = "Elisa",  LastName = "Ridley" },
                new Person { BirthDate = new DateTime(1993, 12, 1), FirstName = "Randy",  LastName = "Knowles" },
                new Person { BirthDate = new DateTime(1946, 5, 8), FirstName = "Melissa",  LastName = "Fincher" }
            };

            IQueryable<dynamic> queryable = enumerable.AsQueryable();

            // Helper method to write data to console
            void Write(IEnumerable<dynamic> collection) {
                foreach (var item in collection) {
                    Console.WriteLine($"{item.FullName} born {item.BirthDate.ToShortDateString()}");
                }
                Console.WriteLine();
            }

            // Property access on dynamic object
            var youngsters = enumerable
                .Where(m => m.Age < 21);

            Console.WriteLine("Young people to test property acces:");
            Write(youngsters);

            // Index access on dynamic object
            var adults = enumerable
                .Where(m => m["Age"] > 20 && m["Age"] < 60);

            Console.WriteLine("Adult people to test index access:");
            Write(adults);

            // Composed lambda expression for queryable (be aware it is not going to work with EF)
            var seniors = queryable
                .Where(ExpressionFactory.PropertyGreaterThanPredicate("Age", 59));

            Console.WriteLine("Senior people to test property access lambda expression:");
            Write(seniors);

            // Composed lambda expression for queryable (be aware it is not going to work with EF)
            var some = queryable
                .Where(ExpressionFactory.IndexLessThanPredicate("Age", 60));

            Console.WriteLine("Some people to test index access lambda expression:");
            Write(some);

            Console.ReadKey();
        }
    }

ExpressionFactory实现返回的表达式包含负责运行时调用和实现动态行为的可能性的动态表达式。这可能是您在尝试期间遇到的问题。

public static class ExpressionFactory {
        public static Expression<Func<dynamic, bool>> PropertyGreaterThanPredicate(string propertyName, int value){
            var constant = Expression.Constant(value);

            var parameter = Expression.Parameter(typeof(object), "m");

            // Creating binder to access class member
            var binder = Binder.GetMember(CSharpBinderFlags.None, propertyName, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) });

            // Dynamic operation on the binder
            var dynamic = Expression.Dynamic(binder, typeof(object), parameter);

            // Converting result to same type as value to compare
            var converted = Expression.Convert(dynamic, constant.Value.GetType());

            var predicate = Expression.GreaterThan(converted, constant);

            var lambda = Expression.Lambda<Func<dynamic, bool>>(predicate, parameter);

            return lambda;
        }

        public static Expression<Func<dynamic, bool>> IndexLessThanPredicate(string indexName, int value) {
            var constant = Expression.Constant(value);

            var parameter = Expression.Parameter(typeof(object), "m");

            // Creating binder to access indexer
            var binder = Binder.GetIndex(CSharpBinderFlags.None, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) });

            // Dynamic operation on the binder
            var dynamic = Expression.Dynamic(binder, typeof(object), parameter, Expression.Constant(indexName));

            // Converting result to same type as value to compare
            var converted = Expression.Convert(dynamic, constant.Value.GetType());

            var predicate = Expression.LessThan(converted, constant);

            var lambda = Expression.Lambda<Func<dynamic, bool>>(predicate, parameter);

            return lambda;
        }
    }

如果您需要完整的工作示例,您可以在GitHub 存储库中找到它。

如果还有问题,请告诉我。我会尽力提供帮助。


原始答案

如何使它工作的唯一方法是使用泛型方法而不是传递参数表达式。

namespace ConsoleApp2 {
    using System;
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Linq.Dynamic;

    class Program {
        static void Main(string[] args) {
            var dynamics = new D[] {
                new D(new Person { Age = 1 }),
                new D(new Person { Age = 50 }),
                new D(new Person { Age = 100 })
            };

            var first = dynamics[0].Age;

            var second = dynamics.Where(x => x.Age > 45);

            var filter = System.Linq.Dynamic.DynamicExpression.ParseLambda<D, bool>("Age > 45");

            var third = dynamics.Where(filter.Compile());
        }

        public class D : DynamicObject {
            Person _p;

            public D(Person p) {
                _p = p;
            }

            public Type Type => this._p.GetType();

            public override IEnumerable<string> GetDynamicMemberNames() {
                return new[] { "Age" };
            }

            public override bool TryGetMember(GetMemberBinder binder, out object result) {
                var name = binder.Name;
                if (name == "Age") {
                    result = _p.Age;
                    return true;
                }

                result = null;
                return false;
            }

            public int Age => _p.Age;
        }

        public class Person {
            public int Age { get; set; }
        }
    }
}

希望能帮助到你!


推荐阅读