c# - Dynamically generate linq select with nested properties

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP



c# - Dynamically generate linq select with nested properties



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



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



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 comes from nuget packages as :


public abstract class DataEntityTypeConfiguration<T> : EntityTypeConfiguration<T> where T : class

protected virtual void PostInitialize();



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



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



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();



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?



UPDATE :



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



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





In the foreach loop, you are repeatedly overriding mbr with a new value without using it. Did you mean to do this?
– Abion47
Aug 8 at 18:29


foreach


mbr





Actually I try to mix two different solution one in the question other is : stackoverflow.com/questions/17647627/… I saw the accepted answer do the same thing in loop.
– Emre Savcı
Aug 8 at 18:30





3 Answers
3



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



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:


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



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.


NewObject


PropertyN = source.Property1.Property2...PropertyN


PropertyN = new TypeN …



Sample usage which matches the expression from your example:


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



Simply call Compile when you need Func.


Compile


Func





I'am glad to see another solution, I was thinking to make my one more generic like yours with helping from recursive calls. Now I made just some modification with mine and yours and merge them. Thank you a lot.
– Emre Savcı
Aug 9 at 21:09



Finally I found the solution. It generates correct lambda for two level nested properties like Shipment.Sender.CityName. So anyone who needs the same thing can use it.



I hope it helps.


/* 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();



It compiles the lambda as below:


s => new Shipment
Recipient = new Address
CityName = s.Recipient.CityName
,
Sender = new Address
CityId = s.Sender.CityId,
CityName = s.Sender.CityName
,
ParcelUniqueId = s.ParcelUniqueId



I share the some screenshots from debug :



enter image description here



enter image description here



I believe your problem is in this chunk of code:


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



The foreach loop is repeatedly assigning a value to mbr and then overwriting it, which means its ultimate value is going to be the expression equivalent of the last value in nestedProps. Assuming the input string is "Recipient.CityId", mbr is going to be an expression of CityId. You then try to do GetProperty on the Shipment type looking for a property by the name of CityId, which of course doesn't exist (CityId being a property of Address).


foreach


mbr


nestedProps


"Recipient.CityId"


mbr


CityId


GetProperty


Shipment


CityId


CityId


Address



I'm not sure what to suggest in order to fix the problem, though, since I'm not sure what you ultimately want out of this.





I know eventually it becomes CityId. I want to generate linq select something like .Select(s => new s.Recipient.CityId ) and parameter fields can change dynamically.
– Emre Savcı
Aug 8 at 18:40





I added the answer which is solved my problem. I hope it helps you to understand what I was trying to solve. Thx for trying to help.
– Emre Savcı
Aug 9 at 10:44






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Firebase Auth - with Email and Password - Check user already registered

Dynamically update html content plain JS

How to determine optimal route across keyboard