动态生成具有嵌套属性的LINQ select [英] Dynamically generate LINQ select with nested properties

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

问题描述

当前,我们有一个软件包,可以从字符串字段中动态生成linq select.它适用于平面属性,但不适用于诸如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

参数"fields"请求对象的字符串只是一个用逗号分隔的字符串,其中包括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();
  }

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

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.我在这里想念什么?如何为具有嵌套属性的给定字符串创建动态选择?

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/663789375b0df23e2662a53bebaf2c7c

推荐答案

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

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 .

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

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