c#-动态生成具有嵌套属性的linq选择

时间:2018-08-08 18:13:29

标签: c# linq select dynamic

当前,我们有一个软件包,可以从字符串字段中动态生成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

3 个答案:

答案 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的一些屏幕截图:

enter image description here

enter image description here

答案 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}}。

但是,我不确定要解决该问题的建议是什么,因为我不确定您最终想要得到什么。