目前我们有一个包可以从字符串字段动态生成 linq select。它适用于平面属性,但它不适用于像 someObj.NestedObj.SomeField 这样的嵌套字段。
我们当前的代码在服务方法中的工作原理如下:
_context.Shipments
.Where(s => s.Id == request.Id) // it does not matter just an example
.Select(request.Fields)
.ToPage(request); // ToPage extension comes from a nuget package
请求对象的参数“fields”只是一个以逗号分隔的字符串,包括Shipment对象的属性。
我对 Shipment 进行了一些重构,我将一些字段分组到一个名为 Address 的新类中,并将其添加到 Shipment 中,如下所示:
// before refactoring
class Shipment {
// other fields...
public string SenderAddress;
public string SenderCityName;
public string SenderCityId;
public string RecipientAddress;
public string CityName;
public string CityId;
}
// after refactoring
class Shipment {
// other fields...
public Address Sender;
public Address Recipient;
}
class Address {
public string AddressText;
public string CityName;
public string CityId;
}
为了当前的数据库映射,我添加了相应的映射:
public class ShipmentMap : DataEntityTypeConfiguration<Shipment>
{
public ShipmentMap()
{
ToTable("Shipments");
// other property mappings
Property(s => s.Recipient.AddressText).HasMaxLength(1100).HasColumnName("RecipientAddress");
Property(s => s.Recipient.CityName).HasMaxLength(100).HasColumnName("CityName");
Property(s => s.Recipient.CityId).IsOptional().HasColumnName("CityId");
Property(s => s.Sender.AddressText).HasMaxLength(1100).HasColumnName("SenderAddress");
Property(s => s.Sender.CityName).HasMaxLength(100).HasColumnName("SenderCityName");
Property(s => s.Sender.CityId).IsOptional().HasColumnName("SenderCityId");
}
}
DataEntityTypeConfiguration 来自 nuget 包:
public abstract class DataEntityTypeConfiguration<T> : EntityTypeConfiguration<T> where T : class
{
protected virtual void PostInitialize();
}
所以,我的问题是当 fields =“Recipient.CityId”时 select(fields) 不起作用。
如何动态生成 linq 以使用嵌套字段进行选择?
我在下面尝试使用 LINQ : Dynamic select 但它不起作用。
// assume that request.Fields= "Recipient.CityId"
// in the service method
List<Shipment> x = _context.Shipments
.Where(s => s.Id == request.Id)
.Select(CreateNewStatement(request.Fields))
.ToList();
// I tried to generate select for linq here
Func<Shipment, Shipment> CreateNewStatement(string fields)
{
// input parameter "o"
var xParameter = Expression.Parameter( typeof( Shipment ), "o" );
// new statement "new Data()"
var xNew = Expression.New( typeof( Shipment ) );
// create initializers
var bindings = fields.Split( ',' ).Select( o => o.Trim() )
.Select(o =>
{
string[] nestedProps = o.Split('.');
Expression mbr = xParameter;
foreach (var prop in nestedProps)
mbr = Expression.PropertyOrField(mbr, prop);
// property "Field1"
PropertyInfo mi = typeof( Shipment ).GetProperty( ((MemberExpression)mbr).Member.Name );
//
// original value "o.Field1"
var xOriginal = Expression.Property( xParameter, mi );
MemberBinding bnd = Expression.Bind( mi, xOriginal );
return bnd;
});
// initialization "new Data { Field1 = o.Field1, Field2 = o.Field2 }"
var xInit = Expression.MemberInit( xNew, bindings );
// expression "o => new Data { Field1 = o.Field1, Field2 = o.Field2 }"
var lambda = Expression.Lambda<Func<Shipment,Shipment>>( xInit, xParameter );
// compile to Func<Data, Data>
return lambda.Compile();
}
它会抛出异常,因为循环后 mbr 变为 CityId,并且“mi”为 null,因为发货时没有字段 CityId。我在这里缺少什么?如何为具有嵌套属性的给定字符串创建动态选择?
更新:
我找到了解决方案并将其添加为答案,我还创建了一个解决方案的 github 要点:
https://gist.github.com/mstrYoda/663789375b0df23e2662a53bebaf2c7c
很高兴您找到了具体问题的解决方案。
这是一个更通用的解决方案,一旦原始属性名称和类型匹配(例如
Entity
-> Dto
等),以及多层嵌套,它就会处理不同的源和目标类型:
public static Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>(string members) =>
BuildSelector<TSource, TTarget>(members.Split(',').Select(m => m.Trim()));
public static Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>(IEnumerable<string> members)
{
var parameter = Expression.Parameter(typeof(TSource), "e");
var body = NewObject(typeof(TTarget), parameter, members.Select(m => m.Split('.')));
return Expression.Lambda<Func<TSource, TTarget>>(body, parameter);
}
static Expression NewObject(Type targetType, Expression source, IEnumerable<string[]> memberPaths, int depth = 0)
{
var bindings = new List<MemberBinding>();
var target = Expression.Constant(null, targetType);
foreach (var memberGroup in memberPaths.GroupBy(path => path[depth]))
{
var memberName = memberGroup.Key;
var targetMember = Expression.PropertyOrField(target, memberName);
var sourceMember = Expression.PropertyOrField(source, memberName);
var childMembers = memberGroup.Where(path => depth + 1 < path.Length);
var targetValue = !childMembers.Any() ? sourceMember :
NewObject(targetMember.Type, sourceMember, childMembers, depth + 1);
bindings.Add(Expression.Bind(targetMember.Member, targetValue));
}
return Expression.MemberInit(Expression.New(targetType), bindings);
}
前两种方法只是公开暴露的高级助手。实际工作是由私有递归
NewObject
方法完成的。它将当前级别属性进行分组,并且对于每个分组,如果它是最后一个级别,则创建简单的分配,例如 PropertyN = source.Property1.Property2...PropertyN
,否则递归地 PropertyN = new TypeN { … }
。
与示例中的表达式相匹配的示例用法:
var test = BuildSelector<Shipment, Shipment>(
"Recipient.CityName, Sender.CityId, Sender.CityName, ParcelUniqueId");
当您需要
Compile
时,只需拨打Func
即可。
终于找到解决办法了。它为两级嵌套属性(例如 Shipment.Sender.CityName)生成正确的 lambda。所以任何需要同样东西的人都可以使用它。
希望有帮助。
/* this comes from request
* request.Fields = "Sender.CityId,Sender.CityName,Recipient.CityName,parcelUniqueId"
*/
// in the service method
var shipmentList = _context.Shipments.
.OrderByDescending(s => s.Id)
.Skip((request.Page -1) * request.PageSize)
.Take(request.PageSize)
.Select(new SelectLambdaBuilder<Shipment>().CreateNewStatement(request.Fields))
.ToList();
public class SelectLambdaBuilder<T>
{
// as a performence consideration I cached already computed type-properties
private static Dictionary<Type, PropertyInfo[]> _typePropertyInfoMappings = new Dictionary<Type, PropertyInfo[]>();
private readonly Type _typeOfBaseClass = typeof(T);
private Dictionary<string, List<string>> GetFieldMapping(string fields)
{
var selectedFieldsMap = new Dictionary<string, List<string>>();
foreach (var s in fields.Split(','))
{
var nestedFields = s.Split('.').Select(f => f.Trim()).ToArray();
var nestedValue = nestedFields.Length > 1 ? nestedFields[1] : null;
if (selectedFieldsMap.Keys.Any(key => key == nestedFields[0]))
{
selectedFieldsMap[nestedFields[0]].Add(nestedValue);
}
else
{
selectedFieldsMap.Add(nestedFields[0], new List<string> { nestedValue });
}
}
return selectedFieldsMap;
}
public Func<T, T> CreateNewStatement(string fields)
{
ParameterExpression xParameter = Expression.Parameter(_typeOfBaseClass, "s");
NewExpression xNew = Expression.New(_typeOfBaseClass);
var selectFields = GetFieldMapping(fields);
var shpNestedPropertyBindings = new List<MemberAssignment>();
foreach (var keyValuePair in selectFields)
{
PropertyInfo[] propertyInfos;
if (!_typePropertyInfoMappings.TryGetValue(_typeOfBaseClass, out propertyInfos))
{
var properties = _typeOfBaseClass.GetProperties();
propertyInfos = properties;
_typePropertyInfoMappings.Add(_typeOfBaseClass, properties);
}
var propertyType = propertyInfos
.FirstOrDefault(p => p.Name.ToLowerInvariant().Equals(keyValuePair.Key.ToLowerInvariant()))
.PropertyType;
if (propertyType.IsClass)
{
PropertyInfo objClassPropInfo = _typeOfBaseClass.GetProperty(keyValuePair.Key);
MemberExpression objNestedMemberExpression = Expression.Property(xParameter, objClassPropInfo);
NewExpression innerObjNew = Expression.New(propertyType);
var nestedBindings = keyValuePair.Value.Select(v =>
{
PropertyInfo nestedObjPropInfo = propertyType.GetProperty(v);
MemberExpression nestedOrigin2 = Expression.Property(objNestedMemberExpression, nestedObjPropInfo);
var binding2 = Expression.Bind(nestedObjPropInfo, nestedOrigin2);
return binding2;
});
MemberInitExpression nestedInit = Expression.MemberInit(innerObjNew, nestedBindings);
shpNestedPropertyBindings.Add(Expression.Bind(objClassPropInfo, nestedInit));
}
else
{
Expression mbr = xParameter;
mbr = Expression.PropertyOrField(mbr, keyValuePair.Key);
PropertyInfo mi = _typeOfBaseClass.GetProperty( ((MemberExpression)mbr).Member.Name );
var xOriginal = Expression.Property(xParameter, mi);
shpNestedPropertyBindings.Add(Expression.Bind(mi, xOriginal));
}
}
var xInit = Expression.MemberInit(xNew, shpNestedPropertyBindings);
var lambda = Expression.Lambda<Func<T,T>>( xInit, xParameter );
return lambda.Compile();
}
它编译 lambda 如下:
s => new Shipment {
Recipient = new Address {
CityName = s.Recipient.CityName
},
Sender = new Address {
CityId = s.Sender.CityId,
CityName = s.Sender.CityName
},
ParcelUniqueId = s.ParcelUniqueId
}
我分享一些调试截图:
我相信你的问题出在这段代码中:
string[] nestedProps = o.Split('.');
Expression mbr = xParameter;
foreach (var prop in nestedProps)
mbr = Expression.PropertyOrField(mbr, prop);
// property "Field1"
PropertyInfo mi = typeof( Shipment ).GetProperty( ((MemberExpression)mbr).Member.Name );
foreach
循环不断地给mbr
赋值,然后覆盖它,这意味着它的最终值将是nestedProps
中最后一个值的等价表达式。假设输入字符串是"Recipient.CityId"
,mbr
将是CityId
的表达式。然后,您尝试在 GetProperty
类型上执行 Shipment
操作,查找名为 CityId
的属性,该属性当然不存在(CityId
是 Address
的属性)。
不过,我不确定应该提出什么建议来解决问题,因为我不确定您最终想要从中得到什么。
扩展伊万的答案。并回答马克的问题。 下面也适用于嵌套列表。
给定字符串的含义
property,nestedProperties.property
它应该像这样构建表达式。
var query = querable.Select(r => new Entity
{
Property = r.Property,
NestedProperties = r.NestedProperties.Select(tn => new NestedEntity
{
Property = tn.Property,
})
.ToList()
});
public static Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>(string members) =>
BuildSelectorFromMembers<TSource, TTarget>(members.Split(',').Select(m => m.Trim()));
public static Expression<Func<TSource, TTarget>> BuildSelectorFromMembers<TSource, TTarget>(IEnumerable<string> members)
{
var parameter = Expression.Parameter(typeof(TSource), "e");
var body = NewObject(typeof(TTarget), parameter, members.Select(m => m.Split('.')));
return Expression.Lambda<Func<TSource, TTarget>>(body, parameter);
}
private static Expression NewObject(Type targetType, Expression source, IEnumerable<string[]> memberPaths, int depth = 0)
{
var target = Expression.Constant(null, targetType);
var bindings = memberPaths
.GroupBy(path => path[depth])
.Select(memberGroup => GetMember(source, depth, memberGroup, target))
.Cast<MemberBinding>()
.ToList();
return Expression.MemberInit(Expression.New(targetType), bindings);
}
private static MemberAssignment GetMember(Expression source, int depth, IGrouping<string, string[]> memberGroup, ConstantExpression target)
{
var memberName = memberGroup.Key;
var targetMember = Expression.PropertyOrField(target, memberName);
var sourceMember = Expression.PropertyOrField(source, memberName);
var childMembers = memberGroup.Where(path => depth + 1 < path.Length).ToList();
if (targetMember.Type.GetInterfaces().Any(r => r == typeof(IList)))
{
return GetMemberFromList(targetMember, childMembers, sourceMember);
}
var targetValue = !childMembers.Any() ? sourceMember : NewObject(targetMember.Type, sourceMember, childMembers, depth + 1);
return Expression.Bind(targetMember.Member, targetValue);
}
private static MemberAssignment GetMemberFromList(MemberExpression targetMember, List<string[]> childMembers, MemberExpression sourceMember)
{
var targetMemberType = targetMember.Type.GenericTypeArguments[0];
var buildSelectorMethod = typeof(GenericSelect).GetMethod(nameof(BuildSelectorFromMembers), BindingFlags.Public | BindingFlags.Static);
var genericBuildSelectorMethod = buildSelectorMethod?.MakeGenericMethod(targetMemberType, targetMemberType);
var selector = (LambdaExpression)genericBuildSelectorMethod?
.Invoke(null, new object[]
{
childMembers.Select(p => string.Join(".", p.Skip(1))),
});
var collectionBinding = Expression.Bind(targetMember.Member, sourceMember);
var selectMethod = typeof(Enumerable)
.GetMethods()
.First(m => m.Name == nameof(Enumerable.Select) && m.GetParameters().Length == 2)?
.MakeGenericMethod(targetMemberType, targetMemberType);
var selectCall = Expression.Call(selectMethod, collectionBinding.Expression, selector!);
var toListMethod = typeof(Enumerable)
.GetMethods()
.First(m => m.Name == nameof(Enumerable.ToList) && m.GetParameters().Length == 1)
.MakeGenericMethod(targetMemberType);
selectCall = Expression.Call(toListMethod, selectCall);
return Expression.Bind(targetMember.Member, selectCall);
}