c# – 使用嵌套属性动态生成linq select

目前我们有一个包从字符串中的字段动态生成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 { 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 : EntityTypeConfiguration where T : class { protected virtual void PostInitialize(); } 

所以,我的问题是select(fields)不适用于fields =“Recipient.CityId”。

如何动态生成linq以便使用嵌套字段进行选择?

我在下面尝试使用LINQ:动态选择,但它不起作用。

 // assume that request.Fields= "Recipient.CityId" // in the service method List x = _context.Shipments .Where(s => s.Id == request.Id) .Select(CreateNewStatement(request.Fields)) .ToList(); // I tried to generate select for linq here Func 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>( xInit, xParameter ); // compile to Func return lambda.Compile(); } 

它会引发exception,因为mbr在循环后变为CityId并且“mi”为null,因为出货时没有字段CityId。 我在这里想念的是什么? 如何使用嵌套属性为给定字符串创建动态选择?

更新:

我找到了解决方案,并将其添加为答案,我也创建了一个github gist for solution:

https://gist.github.com/mstrYoda/663789375b0df23e2662a53bebaf2c7c

你找到了解决你特定问题的方法,这很好。

这是一个更通用的解决方案,一旦原始属性名称和类型匹配(例如Entity – > Dto等),就会处理不同的源和目标类型,以及多个嵌套级别:

 public static Expression> BuildSelector(string members) => BuildSelector(members.Split(',').Select(m => m.Trim())); public static Expression> BuildSelector(IEnumerable members) { var parameter = Expression.Parameter(typeof(TSource), "e"); var body = NewObject(typeof(TTarget), parameter, members.Select(m => m.Split('.'))); return Expression.Lambda>(body, parameter); } static Expression NewObject(Type targetType, Expression source, IEnumerable memberPaths, int depth = 0) { var bindings = new List(); 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( "Recipient.CityName, Sender.CityId, Sender.CityName, ParcelUniqueId"); 

只需在需要Func时调用Compile

最后我找到了解决方案。 它为两个级别的嵌套属性(如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().CreateNewStatement(request.Fields)) .ToList(); public class SelectLambdaBuilder { // as a performence consideration I cached already computed type-properties private static Dictionary _typePropertyInfoMappings = new Dictionary(); private readonly Type _typeOfBaseClass = typeof(T); private Dictionary> GetFieldMapping(string fields) { var selectedFieldsMap = new Dictionary>(); 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 { nestedValue }); } } return selectedFieldsMap; } public Func CreateNewStatement(string fields) { ParameterExpression xParameter = Expression.Parameter(_typeOfBaseClass, "s"); NewExpression xNew = Expression.New(_typeOfBaseClass); var selectFields = GetFieldMapping(fields); var shpNestedPropertyBindings = new List(); 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>( 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的截图:

在此处输入图像描述

在此处输入图像描述

我相信你的问题在于这段代码:

 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最后一个值的nestedProps 。 假设输入字符串是"Recipient.CityId" ,则mbr将成为CityId的表达式。 然后,您尝试在Shipment类型上执行GetProperty ,以通过CityId的名称查找属性,该属性当然不存在( CityIdAddress的属性)。

我不知道为了解决问题需要建议什么,因为我不确定你最终想要的是什么。