获取 IEnumerable 的非静态方法信息.First()(或使静态方法与 EF 一起使用)

本文关键字:静态方法 一起 EF 信息 IEnumerable 获取 First | 更新日期: 2023-09-27 18:34:34

>我有一个方法,GetSearchExpression,定义为:

    private Expression<Func<T, bool>> GetSearchExpression(
        string targetField, ExpressionType comparison, object value, IEnumerable<EnumerableResultQualifier> qualifiers = null);

在高级别上,该方法接受字段或属性(如Order.Customer.Name(、比较类型(如Expression.Equals(和值(如"Billy"(,然后返回适合输入Where语句o => o.Customer.Name == "Billy"}的lambda表达式。

最近,我发现了一个问题。有时,我需要的字段实际上是集合中项目的字段(如Order.StatusLogs.First().CreatedDate(。

我觉得这应该很容易。创建表达式左侧的代码(上图o => o.Customer.Name(如下所示:

var param = Expression.Parameter(typeof(T), "t");
Expression left = null;
//turn "Order.Customer.Name" into List<string> { "Customer", "Name" }
var deQualifiedFieldName = DeQualifyFieldName(targetField, typeof(T));
//loop through each part and grab the specified field or property
foreach (var part in deQualifiedFieldName)
    left = Expression.PropertyOrField(left == null ? param : left, part);

似乎我应该能够修改它以检查字段/属性是否存在,如果不存在,请尝试使用该名称调用方法。它看起来像这样:

var param = Expression.Parameter(typeof(T), "t");
Expression left = null;
var deQualifiedFieldName = DeQualifyFieldName(targetField, typeof(T));
var currentType = typeof(T);
foreach (var part in deQualifiedFieldName)
{
    //this gets the Type of the current "level" we're at in the hierarchy passed via TargetField
    currentType = SingleLevelFieldType(currentType, part);
    if (currentType != null) //if the field/property was found
    {
        left = Expression.PropertyOrField(left == null ? param : left, part);                    
    }
    else
    {   //if the field or property WASN'T found, it might be a method                    
        var method = currentType.GetMethod(part, Type.EmptyTypes); //doesn't accept parameters
        left = Expression.Call(left, method);
        currentType = method.ReturnType;
    }                
}

问题是接近结尾的陈述(var method currentType.GetMethod(part, Type.EmptyTypes);(。 事实证明,IEnumerable对象不存在"第一个"和"最后一个",因此当我尝试使用我的 Method 对象时,我会收到一个空异常。事实上,我能让他们出现在GetMethod()电话中的唯一方法是打电话给typeof(Enumerable).GetMethod()。这当然是没有用的,因为这样我就会得到一个静态方法作为回报,而不是我需要的实例方法。

作为

旁注:我尝试使用静态方法,但实体框架抛出了一个适合,并且不接受它作为 lambda 的一部分。

我需要帮助获取IEnumerable.First()Last()的实例MethodInfo。请帮忙!

获取 IEnumerable<T> 的非静态方法信息.First()(或使静态方法与 EF 一起使用)

我的第一个尝试是确定实例是否Enumerable<T>并将成员名称视为方法而不是像这样的属性/字段

public static class ExpressionUtils
{
    public static Expression<Func<T, bool>> MakePredicate<T>(
        string memberPath, ExpressionType comparison, object value)
    {
        var param = Expression.Parameter(typeof(T), "t");
        var right = Expression.Constant(value);
        var left = memberPath.Split('.').Aggregate((Expression)param, (target, memberName) =>
        {
            if (typeof(IEnumerable).IsAssignableFrom(target.Type))
            {
                var enumerableType = target.Type.GetInterfaces()
                    .Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
                return Expression.Call(typeof(Enumerable), memberName, enumerableType.GetGenericArguments(), target);
            }
            return Expression.PropertyOrField(target, memberName);
        });
        var body = Expression.MakeBinary(comparison, left, right);
        return Expression.Lambda<Func<T, bool>>(body, param);
    }
}

并尝试按如下方式使用它

var predicate = ExpressionUtils.MakePredicate<Order>(
    "StatusLogs.First.CreatedDate", ExpressionType.GreaterThanOrEqual, new DateTime(2016, 1, 1));

可能的方法有FirstFirstOrDefaultLastLastOrDefaultSingeSingleOrDefault

但随后你会发现,从上述方法中,EF 谓词中仅支持 FirstOrDefault

因此,我们可以对调用集合类型进行硬编码,并且不将其包含在像这样的访问器中

public static class ExpressionUtils
{
    public static Expression<Func<T, bool>> MakePredicate2<T>(
        string memberPath, ExpressionType comparison, object value)
    {
        var param = Expression.Parameter(typeof(T), "t");
        var right = Expression.Constant(value);
        var left = memberPath.Split('.').Aggregate((Expression)param, (target, memberName) =>
        {
            if (typeof(IEnumerable).IsAssignableFrom(target.Type))
            {
                var enumerableType = target.Type.GetInterfaces()
                    .Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
                target = Expression.Call(typeof(Enumerable), "FirstOrDefault", enumerableType.GetGenericArguments(), target);
            }
            return Expression.PropertyOrField(target, memberName);
        });
        var body = Expression.MakeBinary(comparison, left, right);
        return Expression.Lambda<Func<T, bool>>(body, param);
    }
}

并按如下方式使用它

var predicate = ExpressionUtils.MakePredicate<Order>(
    "StatusLogs.CreatedDate", ExpressionType.GreaterThanOrEqual, new DateTime(2016, 1, 1));

附言虽然这将起作用,但它可能不会产生预期的结果。 IEnumerable<T>导航属性意味着one-to-many关系,并且假设条件应仅适用于第一个(无论这在数据库中意味着什么,它都是相当随机的(元素没有多大意义。我宁愿暗示Any并尝试在上述情况下构建这样的表达式

t => t.StatusLogs.Any(s => s.CreatedDate >= new DateTime(2016, 1, 1))

或者支持FirstOrDefaultAnyAll、(最终CountSumMinMax(并在构建器内部以不同的方式处理它们。

对于集合Any,IMO仍然是单一实体标准最合乎逻辑的等效物。

但这一切都将是另一个故事(问题(。

更新:最初我想到此为止,但为了完整起见,这里是Any概念的示例实现:

public static class ExpressionUtils
{
    public static Expression<Func<T, bool>> MakePredicate<T>(string memberPath, ExpressionType comparison, object value)
    {
        return (Expression<Func<T, bool>>)MakePredicate(
            typeof(T), memberPath.Split('.'), 0, comparison, value);
    }
    static LambdaExpression MakePredicate(Type targetType, string[] memberNames, int index, ExpressionType comparison, object value)
    {
        var parameter = Expression.Parameter(targetType, targetType.Name.ToCamel());
        Expression target = parameter;
        for (int i = index; i < memberNames.Length; i++)
        {
            if (typeof(IEnumerable).IsAssignableFrom(target.Type))
            {
                var itemType = target.Type.GetInterfaces()
                    .Single(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                    .GetGenericArguments()[0];
                var itemPredicate = MakePredicate(itemType, memberNames, i, comparison, value);
                return Expression.Lambda(
                    Expression.Call(typeof(Enumerable), "Any", new[] { itemType }, target, itemPredicate),
                    parameter);
            }
            target = Expression.PropertyOrField(target, memberNames[i]);
        }
        if (value != null && value.GetType() != target.Type)
            value = Convert.ChangeType(value, target.Type);
        return Expression.Lambda(
            Expression.MakeBinary(comparison, target, Expression.Constant(value)),
            parameter);
    }
    static string ToCamel(this string s)
    {
        if (string.IsNullOrEmpty(s) || char.IsLower(s[0])) return s;
        if (s.Length < 2) return s.ToLower();
        var chars = s.ToCharArray();
        chars[0] = char.ToLower(chars[0]);
        return new string(chars);
    }
}

所以对于此示例模型

public class Foo
{
    public ICollection<Bar> Bars { get; set; }
}
public class Bar
{
    public ICollection<Baz> Bazs { get; set; }
}
public class Baz
{
    public ICollection<Detail> Details { get; set; }
}
public class Detail
{
    public int Amount { get; set; }
}

示例表达式

var predicate = ExpressionUtils.MakePredicate<Foo>(
    "Bars.Bazs.Details.Amount", ExpressionType.GreaterThan, 1234);

生产

foo => foo.Bars.Any(bar => bar.Bazs.Any(baz => baz.Details.Any(detail => detail.Amount > 1234)))

您可能正在寻找的是System.Linq.Enumerable.First<T>(this IEnumerable<T> source)等,因此:从typeof(System.Linq.Enumerable)开始,然后从那里开始工作。注意:你提到IEnumerable<T>,但有可能实际上指的是IQueryable<T>,在这种情况下,你想要Queryable.First<T>(this IQueryable<T> source)等。也许这种差异(EnumerableQueryable之间(就是EF"投掷"的原因。

感谢Marc和Ivan的投入。他们值得称赞,因为如果没有他们的帮助,我会花更长的时间寻找解决方案。但是,由于这两个答案都没有解决我遇到的问题,因此我发布了对我有用的解决方案(成功应用条件以及成功查询 EF 数据源(:

    private Expression<Func<T, bool>> GetSearchExpression(string targetField, ExpressionType comparison, object value, string enumMethod)
    {
        return (Expression<Func<T, bool>>)MakePredicate(DeQualifyFieldName(targetField, typeof(T)), comparison, value, enumMethod);
    }
    private LambdaExpression MakePredicate(string[] memberNames, ExpressionType comparison, object value, string enumMethod = "Any")
    {
        //create parameter for inner lambda expression
        var parameter = Expression.Parameter(typeof(T), "t");
        Expression left = parameter;
        //Get the value against which the property/field will be compared
        var right = Expression.Constant(value);
        var currentType = typeof(T);
        for (int x = 0; x < memberNames.Count(); x++)
        {
            string memberName = memberNames[x];
            if (FieldExists(currentType, memberName))
            {
                //assign the current type member type 
                currentType = SingleLevelFieldType(currentType, memberName);
                left = Expression.PropertyOrField(left == null ? parameter : left, memberName);
                //mini-loop for non collection objects
                if (!currentType.IsGenericType || (!(currentType.GetGenericTypeDefinition() == typeof(IEnumerable<>) ||
                                                     currentType.GetGenericTypeDefinition() == typeof(ICollection<>))))
                    continue;
                ///Begin loop for collection objects -- this section can only run once
                //get enum method
                if (enumMethod.Length < 2) throw new Exception("Invalid enum method target.");
                bool negateEnumMethod = enumMethod[0] == '!';
                string methodName = negateEnumMethod ? enumMethod.Substring(1) : enumMethod;
                //get the interface sub-type
                var itemType = currentType.GetInterfaces()
                                          .Single(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                                          .GetGenericArguments()[0];
                //generate lambda for single item
                var itemPredicate = MakeSimplePredicate(itemType, memberNames[++x], comparison, value);
                //get method call
                var staticMethod = typeof(Enumerable).GetMember(methodName).OfType<MethodInfo>()
                                                     .Where(m => m.GetParameters().Length == 2)
                                                     .First()
                                                     .MakeGenericMethod(itemType);
                //generate method call, then break loop for return
                left = Expression.Call(null, staticMethod, left, itemPredicate);
                right = Expression.Constant(!negateEnumMethod);
                comparison = ExpressionType.Equal;
                break;
            }
        }
        //build the final expression
        var binaryExpression = Expression.MakeBinary(comparison, left, right);
        return Expression.Lambda<Func<T, bool>>(binaryExpression, parameter);
    }
    static LambdaExpression MakeSimplePredicate(Type inputType, string memberName, ExpressionType comparison, object value)
    {
        var parameter = Expression.Parameter(inputType, "t");
        Expression left = Expression.PropertyOrField(parameter, memberName);
        return Expression.Lambda(Expression.MakeBinary(comparison, left, Expression.Constant(value)), parameter);
    }
    private static Type SingleLevelFieldType(Type baseType, string fieldName)
    {
        Type currentType = baseType;
        MemberInfo match = (MemberInfo)currentType.GetField(fieldName) ?? currentType.GetProperty(fieldName);
        if (match == null) return null;
        return GetFieldOrPropertyType(match);
    }
    public static Type GetFieldOrPropertyType(MemberInfo field)
    {
        return field.MemberType == MemberTypes.Property ? ((PropertyInfo)field).PropertyType : ((FieldInfo)field).FieldType;
    }
    /// <summary>
    /// Remove qualifying names from a target field.  For example, if targetField is "Order.Customer.Name" and
    /// targetType is Order, the de-qualified expression will be "Customer.Name" split into constituent parts
    /// </summary>
    /// <param name="targetField"></param>
    /// <param name="targetType"></param>
    /// <returns></returns>
    public static string[] DeQualifyFieldName(string targetField, Type targetType)
    {
        return DeQualifyFieldName(targetField.Split('.'), targetType);
    }
    public static string[] DeQualifyFieldName(string[] targetFields, Type targetType)
    {
        var r = targetFields.ToList();
        foreach (var p in targetType.Name.Split('.'))
            if (r.First() == p) r.RemoveAt(0);
        return r.ToArray();
    }

我包含了相关的方法,以防有人在某个时候确实需要对此进行排序。 :)

再次感谢!