动态生成具有嵌套属性的 LINQ select

问题描述 投票:0回答:4

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

c# linq select dynamic
4个回答
9
投票

很高兴您找到了具体问题的解决方案。

这是一个更通用的解决方案,一旦原始属性名称和类型匹配(例如

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
即可。


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
}

我分享一些调试截图:

enter image description here

enter image description here


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
Address
的属性)。

不过,我不确定应该提出什么建议来解决问题,因为我不确定您最终想要从中得到什么。


0
投票

扩展伊万的答案。并回答马克的问题。 下面也适用于嵌套列表。

给定字符串的含义

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);
}
© www.soinside.com 2019 - 2024. All rights reserved.