当前,我们有一个软件包,可以从字符串字段中动态生成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
答案 0 :(得分:4)
很高兴为您的特定问题找到了解决方案。
这是一个更通用的解决方案,一旦原始属性名称和类型匹配(例如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
。
答案 1 :(得分:3)
最后我找到了解决方案。它为两个级别的嵌套属性(例如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
}
我分享了debug的一些屏幕截图:
答案 2 :(得分:0)
我相信您的问题出在这段代码中:
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
是{的属性{1}}。
但是,我不确定要解决该问题的建议是什么,因为我不确定您最终想要得到什么。