为了账号安全,请及时绑定邮箱和手机立即绑定

使用嵌套属性动态生成 LINQ 选择

使用嵌套属性动态生成 LINQ 选择

C#
浮云间 2021-11-21 10:19:44
目前我们有一个包可以从字符串中的字段动态生成 linq 选择。它适用于平面属性,但它不是设计用于嵌套字段,如 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请求对象的参数“字段”只是一个以逗号分隔的字符串,包括 Shipment 对象的属性。我对 Shipment 进行了一些重构,我将一些字段分组到一个名为 Address 的新类中,并将其添加到 Shipment 中,如下所示:// before refactoringclass Shipment {    // other fields...    public string SenderAddress;    public string SenderCityName;    public string SenderCityId;    public string RecipientAddress;    public string CityName;    public string CityId;}// after refactoringclass 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();  }所以,我的问题是 select(fields) 在 fields = "Recipient.CityId" 时不起作用。如何动态生成用于选择嵌套字段的 linq?
查看完整描述

3 回答

?
拉风的咖菲猫

TA贡献1995条经验 获得超2个赞

很高兴您找到了解决您的特定问题的方法。


这是一个更通用的解决方案,一旦原始属性名称和类型匹配(例如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。


查看完整回答
反对 回复 2021-11-21
?
素胚勾勒不出你

TA贡献1827条经验 获得超9个赞

最后我找到了解决方案。它为两级嵌套属性(如 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 的一些截图:

//img1.sycdn.imooc.com//6199b14100017d8a14650825.jpg

//img1.sycdn.imooc.com//6199b14c0001403114030486.jpg

查看完整回答
反对 回复 2021-11-21
?
汪汪一只猫

TA贡献1898条经验 获得超8个赞

最后我找到了解决方案。它为两级嵌套属性(如 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 的一些截图:我相信你的问题出在这段代码中:


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)。


不过,我不确定要解决什么问题,因为我不确定您最终想要什么。


查看完整回答
反对 回复 2021-11-21
  • 3 回答
  • 0 关注
  • 217 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信