6

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 });
Community
  • 1
  • 1
djdd87
  • 67,346
  • 27
  • 156
  • 195
  • 1
    The problem is that anonymous types that are the results of the select statemet are created at compile time.. – m0sa Jan 25 '12 at 10:09
  • 1
    Is there an alternative to using an Anonymous type and still achieve what I require? Or is it back to the drawing board? – djdd87 Jan 25 '12 at 10:17
  • @GenericTypeTea, would a Tuple be an acceptable option for you? – Thomas Levesque Jan 25 '12 at 10:21
  • @ThomasLevesque Yes, I don't see why not. What did you have in mind? – djdd87 Jan 25 '12 at 10:22
  • @GenericTypeTea, I'll post an answer in a few minutes – Thomas Levesque Jan 25 '12 at 10:24
  • @GenericTypeTea hm, didn't think you'd find Tuples acceptable :) It's only slightly better than an array of values, in that index-out-of-range is detected at compile time. But you'll have to refer to them as `Item1`, `Item2` etc - not great, and you lose the ability to actually iterate over them in a loop. – Roman Starkov Jan 25 '12 at 10:35

3 Answers3

3

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;
}
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • 2
    Unfortunately this doesn't seem to work with an IQueryable from Entity Framework. "Only parameterless constructors and initializers are supported in LINQ to Entities." – djdd87 Jan 25 '12 at 11:01
  • 1
    @GenericTypeTea Maybe it is obvious, but I solved this error message with compiling the expression before usage, ie.: var primaryKeyResults = query.Select(primaryKeyExpression.Compile()).ToList(); – Jorr.it Feb 20 '15 at 12:47
  • 3
    @Jorr.it Your comment may be true but the query will not be executed on the database – Tokk Oct 11 '17 at 13:43
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)
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Unfortunately this doesn't seem to work with an IQueryable from Entity Framework. "Only parameterless constructors and initializers are supported in LINQ to Entities." – djdd87 Jan 25 '12 at 10:59
  • 1
    @GenericTypeTea, oh, well, that's sad. In this case you could use Reflection.Emit to generate a dynamic type at runtime with the right number of properties and then build an expression assigning those properties. Seems like a lot of work though. – Darin Dimitrov Jan 25 '12 at 11:02
  • I'm starting to wonder if it's better to just force the consuming application to implement an interface I specify on all their EF POCO objects. Then I could just have them implement a GetPrimaryKeySelectExpression method... but then that would lead to all sorts of possible problems (not including the fact it breaks the entire principle of a POCO class) as well as being an utter pain in the b'hind for the user to implement. Swings and roundabouts! – djdd87 Jan 25 '12 at 11:05
  • @GenericTypeTea Maybe it is obvious, but I solved this error message with compiling the expression before usage, ie.: var primaryKeyResults = query.Select(primaryKeyExpression.Compile()).ToList(); – Jorr.it Feb 20 '15 at 12:45
  • What if `TItem` is unknown? In my scenario, I want to load properties in `TItem` where those properties can be of various subclasses in the hierarchy map unknown to us at compile time. Can we build this function while attaching properties of `TItem` that exist only in subclasses of it (to be used in EF)? – Shimmy Weitzhandler May 28 '17 at 01:56
1

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.

Roman Starkov
  • 59,298
  • 38
  • 251
  • 324
  • I've got no knowledge of the primary keys at runtime. The code above is used by a consuming application I've got no control over. See the previous question for a better explanation. – djdd87 Jan 25 '12 at 10:35