获取 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
。请帮忙!
我的第一个尝试是确定实例是否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));
可能的方法有First
、FirstOrDefault
、Last
、LastOrDefault
、Singe
和SingleOrDefault
。
但随后你会发现,从上述方法中,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))
或者支持FirstOrDefault
、Any
、All
、(最终Count
、Sum
、Min
、Max
(并在构建器内部以不同的方式处理它们。
对于集合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)
等。也许这种差异(Enumerable
和Queryable
之间(就是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();
}
我包含了相关的方法,以防有人在某个时候确实需要对此进行排序。 :)
再次感谢!