简体   繁体   English

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

[英]Get Non-Static MethodInfo for IEnumerable<T>.First() (Or make the static method work with EF)

I have a method, GetSearchExpression , defined as: 我有一个方法GetSearchExpression ,定义为:

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

At a high level, the method takes in a Field or Property (such as Order.Customer.Name ), a comparison type (like Expression.Equals ), and a value (like "Billy"), then returns a lambda expression suitable for input to a Where statement o => o.Customer.Name == "Billy"} . 在较高的层次上,该方法接受字段或属性(例如Order.Customer.Name ),比较类型(例如Expression.Equals )和值(例如“ Billy”),然后返回适合以下条件的Lambda表达式输入到Where语句中o => o.Customer.Name == "Billy"}

Recently, I discovered an issue. 最近,我发现了一个问题。 Sometimes, the field I need is actually the field of an item in a collection (like Order.StatusLogs.First().CreatedDate ). 有时,我需要的字段实际上是集合中某个项目的字段(例如Order.StatusLogs.First().CreatedDate )。

I feel like that should be easy. 觉得那应该很容易。 The code that creates the left side of the expression (above, o => o.Customer.Name ) is as follows: 创建表达式左侧(上面, 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);

It seems like I should be able to revise this to check if the field/property exists, and if not, try to call a method by that name instead. 看来我应该能够对此进行修改,以检查字段/属性是否存在,如果不存在,请尝试改用该名称调用方法。 It would look like this: 它看起来像这样:

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;
    }                
}

The problem is that statement near the end ( var method currentType.GetMethod(part, Type.EmptyTypes); ). 问题是该语句接近结尾( var method currentType.GetMethod(part, Type.EmptyTypes); )。 Turns out "First" and "Last" don't exist for IEnumerable objects, so I get a null exception when I try to use my Method object. 原来IEnumerable对象不存在“第一个”和“最后一个”,因此当我尝试使用Method对象时会出现null异常。 In fact, the only way I can EVER them to show up in a GetMethod() call is by calling typeof(Enumerable).GetMethod() . 实际上,使它们出现在GetMethod()调用中的唯一方法是调用typeof(Enumerable).GetMethod() That's useless of course, because then I get a static method in return rather than the instance method I need. 当然,这是没有用的,因为然后我得到了一个静态方法作为回报,而不是所需的实例方法。

As a side-note: I tried using the static method, but Entity Framework throws a fit and won't accept it as part of the lambda. 附带说明:我尝试使用静态方法,但是Entity Framework抛出拟合,并且不会将其作为lambda的一部分接受。

I need help getting the instance MethodInfo of IEnumerable.First() & Last() . 我需要帮助来获取IEnumerable.First()Last()的实例MethodInfo Please help! 请帮忙!

What you are possibly looking for is System.Linq.Enumerable.First<T>(this IEnumerable<T> source) etc, so: start at typeof(System.Linq.Enumerable) and work from there. 您可能正在寻找的是System.Linq.Enumerable.First<T>(this IEnumerable<T> source)等,因此:从typeof(System.Linq.Enumerable)并从那里开始工作。 Note: you mention IEnumerable<T> , but it is possible that you actually mean IQueryable<T> , in which case you want Queryable.First<T>(this IQueryable<T> source) etc. Maybe this difference (between Enumerable and Queryable ) is why EF "throws a fit". 注意:您提到IEnumerable<T> ,但实际上 可能IQueryable<T> ,在这种情况下,您需要Queryable.First<T>(this IQueryable<T> source)等。也许是这种区别(在EnumerableQueryable )是EF“抛出合适”的原因。

My first attempt would be to identify if the instance is Enumerable<T> and treat the member name as method instead of a property/field like this 我的第一个尝试是确定实例是否为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);
    }
}

and try use it as follows 并尝试如下使用

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

The possible methods are First , FirstOrDefault , Last , LastOrDefault , Singe and SingleOrDefault . 可能的方法是FirstFirstOrDefaultLastLastOrDefaultSingeSingleOrDefault

But then you'll find that from the above methods only FirstOrDefault is supported in EF predicates. 但是,您会发现从上述方法中, EF谓词仅支持FirstOrDefault

Hence we can hardcode that call for collection types and do not include it in the accessors like this 因此,我们可以硬编码要求集合类型的代码,而不将其包括在这样的访问器中

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);
    }
}

and use it as follows 并如下使用

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

PS While this will work, it might not produce the intended result. 附言:尽管这将起作用,但可能不会产生预期的结果。 IEnumerable<T> navigation property means one-to-many relationship and assuming that the condition should apply only for the first (whatever that means in database, it's rather random) element does not make much sense. IEnumerable<T>导航属性表示one-to-many关系,并假定该条件仅应适用于第一个元素(无论在数据库中是什么意思,它都是随机的)都没有多大意义。 I would rather imply Any and try to build expression like this in the above case 我宁愿暗示Any并尝试在上述情况下构建这样的表达式

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

or support FirstOrDefault , Any , All , (eventually Count , Sum , Min , Max ) and handle them differently inside the builder. 或支持FirstOrDefaultAnyAll (最终为CountSumMinMax ),并在构建器中以不同的方式处理它们。

Still IMO for collections Any is the most logical equivalent of the single entity criteria. IMO仍然适用于集合Any是最符合逻辑的单实体标准。

But all that will be another story (question). 但是,所有这些将是另一个故事(问题)。

UPDATE: Initially I was thinking to stop here, but for the sake of completeness, here is a sample implementation of the Any concept: 更新:最初我想在这里停止,但是为了完整起见,这里是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);
    }
}

so for this sample model 所以对于这个样本模型

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; }
}

the sample expression 样本表达

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

produces 产生

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

Thank you to Marc and Ivan for their input. 感谢Marc和Ivan的投入。 They deserve credit as without their help I would have spent much longer finding a solution. 他们应该得到信誉,因为如果没有他们的帮助,我将需要花费更长的时间才能找到解决方案。 However, as neither answer solved the issue I was having, I'm posting the solution that worked for me (successfully applying criteria as well as successfully querying against an EF data source): 但是,由于没有一个答案能解决我遇到的问题,因此我正在发布对我有用的解决方案(成功地应用了标准以及成功地查询了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();
    }

I included related methods in case someone actually needs to sort through this at some point. 我提供了相关的方法,以防某人确实需要对此进行排序。 :) :)

Thanks again! 再次感谢!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM