How can I create a dynamic multi-property Select on an IEnumerable<T> at runtime?

两盒软妹~` 提交于 2019-11-27 03:31:35

问题


I asked a very similar question yesterday, but it wasn't until today I realised the answer I accepted doesn't solve all my problems. I have the following code:

public Expression<Func<TItem, object>> SelectExpression<TItem>(string fieldName)
{
    var param = Expression.Parameter(typeof(TItem), "item");
    var field = Expression.Property(param, fieldName);
    return Expression.Lambda<Func<TItem, object>>(field, 
        new ParameterExpression[] { param });
}

Which is used as follows:

string primaryKey = _map.GetPrimaryKeys(typeof(TOriginator)).Single();
var primaryKeyExpression = SelectExpression<TOriginator>(primaryKey);
var primaryKeyResults = query.Select(primaryKeyExpression).ToList();

This allows me to pull out the primary keys from an IQueryable<TUnknown>. The problem is that this code only works with a single primary key and I need to add support for multiple PKs.

So, is there any way I can adapt the SelectExpression method above to take an IEnumerable<string> (which is my list of primary key property names) and have the method return an expression that selects those keys?

I.e. Given the following:

var knownRuntimePrimaryKeys = new string[] { "CustomerId", "OrderId" }`

My Select needs to do the following (at runtime):

var primaryKeys = query.Select(x=> new { x.CustomerId, x.OrderId });

回答1:


There is no easy way to do exactly what you want, because it would require you to create a new type dynamically (anonymous types are created by the compiler when they're known statically). While it is certainly feasible, it's probably not the easiest option...

You can achieve a similar result using tuples:

public Expression<Func<TItem, object>> SelectExpression<TItem>(string[] propertyNames)
{
    var properties = propertyNames.Select(name => typeof(TItem).GetProperty(name)).ToArray();
    var propertyTypes = properties.Select(p => p.PropertyType).ToArray();
    var tupleTypeDefinition = typeof(Tuple).Assembly.GetType("System.Tuple`" + properties.Length);
    var tupleType = tupleTypeDefinition.MakeGenericType(propertyTypes);
    var constructor = tupleType.GetConstructor(propertyTypes);
    var param = Expression.Parameter(typeof(TItem), "item");
    var body = Expression.New(constructor, properties.Select(p => Expression.Property(param, p)));
    var expr = Expression.Lambda<Func<TItem, object>>(body, param);
    return expr;
}



回答2:


You could use Tuple<> because anonymous types must be known at compile time:

public Expression<Func<TItem, object>> SelectExpression<TItem>(params string[] fieldNames)
{
    var param = Expression.Parameter(typeof(TItem), "item");
    var fields = fieldNames.Select(x => Expression.Property(param, x)).ToArray();
    var types = fields.Select(x => x.Type).ToArray();
    var type = Type.GetType("System.Tuple`" + fields.Count() + ", mscorlib", true);
    var tuple = type.MakeGenericType(types);
    var ctor = tuple.GetConstructor(types);
    return Expression.Lambda<Func<TItem, object>>(
        Expression.New(ctor, fields), 
        param
    );
}

and then:

var primaryKeyExpression = SelectExpression<TOriginator>("CustomerId", "OrderId");

will generate the following expression:

item => new Tuple<string, string>(item.CustomerId, item.OrderId)



回答3:


As someone has already pointed out, you are essentially trying to get an anonymous type constructed at runtime, which is not going to work.

Is there an alternative to using an Anonymous type and still achieve what I require?

This really depends on what you mean. The obvious answers are to use runtime constructs, such as a dictionary, a tuple, etc. But you are probably fully aware of this yourself, so I assume that you want a compile-time-typed result with real field names, so that any mis-use of primaryKeys gets caught during compilation.

If so, then I'm afraid your only option is to generate the relevant code before compilation. This isn't as bad as it might sound, but is not completely transparent: when you change the schema, you have to re-run the code generation somehow.

At our company we've done exactly that, being inspired by SubSonic but finding that SubSonic itself wasn't quite what we wanted. It worked out rather well in my opinion.



来源:https://stackoverflow.com/questions/9000753/how-can-i-create-a-dynamic-multi-property-select-on-an-ienumerablet-at-runtime

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!