[英]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)
等。也许是这种区别(在Enumerable
和Queryable
)是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
. 可能的方法是
First
, FirstOrDefault
, Last
, LastOrDefault
, Singe
和SingleOrDefault
。
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. 或支持
FirstOrDefault
, Any
, All
(最终为Count
, Sum
, Min
, Max
),并在构建器中以不同的方式处理它们。
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.