使用嵌套属性动态生成 LINQ 选择 [英] Dynamically generate LINQ select with nested properties

查看:18
本文介绍了使用嵌套属性动态生成 LINQ 选择的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

目前我们有一个包,可以从字符串中的字段动态生成 linq 选择.它适用于平面属性,但不适用于像 someObj.NestedObj.SomeField 这样的嵌套字段.

Currently we have a package that generates linq select dynamically from fields from string. It works well with flat properties but it is not designed to work with nested fields like someObj.NestedObj.SomeField.

我们当前的代码在服务方法中的工作方式如下:

Our current code works as below in the service method:

_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 对象的属性.

The parameter "fields" of request object is just a string which seperated with commas including Shipment object's properties.

我对 Shipment 进行了一些重构,我将一些字段分组到一个名为 Address 的新类中,并将其添加到 Shipment 中,如下所示:

I made some refactoring to Shipment, I grouped some fields into a new class named as Address and add it to Shipment as below:

// 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;
}

为了当前的数据库映射,我添加了相应的映射:

For the sake of current database mapping I added the corresponding mappings as :

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 包:

DataEntityTypeConfiguration comes from nuget packages as :

  public abstract class DataEntityTypeConfiguration<T> : EntityTypeConfiguration<T> where T : class
  {
    protected virtual void PostInitialize();
  }

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

So, my problem is with the select(fields) not works for when fields = "Recipient.CityId".

如何动态生成用于选择嵌套字段的 linq?

How can I dynamically generate linq for selecting with nested fields?

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

I tried below using LINQ : Dynamic select but it does not work.

// 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.为空,因为装运时没有字段 CityId.我在这里错过了什么?如何为具有嵌套属性的给定字符串创建动态选择?

It throws exception because mbr becomes CityId after the loop and "mi" is null because there is no field CityId on shipment. What am I missing here? How can I create dynamic select for given string with nested properties?

更新:

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

I found the solution and added it as answer, also I created a github gist for solution:

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

推荐答案

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

It's good that you've found a solution of your specific problem.

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

Here is a more general solution which handles different source and target types as soon as the primitive property names and types match (e.g. Entity -> Dto etc.), as well as multiple levels of nesting:

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{ ... } 否则.

The first two methods are just the publicly exposed high level helpers. The actual work is done by the private recursive NewObject method. It groups the current level properties and for each grouping, either creates simple assignment like PropertyN = source.Property1.Property2...PropertyN if it is the last level, or recursively PropertyN = new TypeN { … } otherwise.

与您的示例中的表达式匹配的示例用法:

Sample usage which matches the expression from your example:

var test = BuildSelector<Shipment, Shipment>(
    "Recipient.CityName, Sender.CityId, Sender.CityName, ParcelUniqueId");

当你需要Func时,只需调用Compile.

Simply call Compile when you need Func.

这篇关于使用嵌套属性动态生成 LINQ 选择的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆