【问题标题】:Dynamically generate LINQ select with nested properties使用嵌套属性动态生成 LINQ 选择
【发布时间】:2019-01-16 02:25:49
【问题描述】:

目前我们有一个包,可以从字符串中的字段动态生成 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();
  }

所以,我的问题是 select(fields) 在 fields = "Recipient.CityId" 时不起作用。

如何动态生成用于选择嵌套字段的 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”为空,因为在发货时没有字段 CityId。我在这里想念什么?如何为具有嵌套属性的给定字符串创建动态选择?

更新:

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

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

【问题讨论】:

  • foreach 循环中,您反复用新值覆盖mbr 而不使用它。你是故意的吗?
  • 实际上我尝试在另一个问题中混合两种不同的解决方案:stackoverflow.com/questions/17647627/… 我看到接受的答案在循环中做同样的事情。

标签: c# linq select dynamic


【解决方案1】:

我相信您的问题出在这段代码中:

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 的表达式。然后,您尝试在Shipment 类型上执行GetProperty,以查找名称为CityId 的属性,这当然不存在(CityIdAddress 的属性)。

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

【讨论】:

  • 我知道它最终会变成 CityId。我想生成类似于 .Select(s => new { s.Recipient.CityId }) 的 linq 选择,并且参数字段可以动态更改。
  • 我添加了解决我问题的答案。我希望它可以帮助您理解我试图解决的问题。感谢您的帮助。
【解决方案2】:

我终于找到了解决方案。它为两级嵌套属性(如 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
}

我分享了一些调试的截图:

【讨论】:

  • 我不能完全判断这是否保持对象形状,所以 in.child.field 映射到 out.child.field?
【解决方案3】:

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

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

当您需要Func 时,只需致电Compile

【讨论】:

  • 我很高兴看到另一种解决方案,我想通过递归调用的帮助使我的解决方案像您的一样通用。现在我对我的和你的做了一些修改并将它们合并。非常感谢。
  • 这太好了,我现在的解决方案中有一些非常相似的东西。非常感谢您的发布。我遇到的一个问题是我正在使用表达式通过 linq 创建 SQL。我只是在嵌套属性之一是集合时遇到问题,因为它正在尝试创建一个表达式 Like List.Id ... 失败,因为列表没有 ID ...有什么方法可以扩展它以在列表属性上创建表达式?等价于 Database.Users.Id(返回一个 id 列表)
  • @MarkMcGookin 可能,但现在不能说。但这开始朝着 AutoMapper / DynamicLINQ 方向发展,你不觉得吗?
猜你喜欢
  • 2021-05-25
  • 2021-10-11
  • 1970-01-01
  • 2014-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-30
  • 1970-01-01
相关资源
最近更新 更多