Thank you @morio. Your comment about Expression<Func<T, T>> is exactly what I needed to make this work.
I do not know how to perform an anonymous projection which seems like what most want. I say I want Field1 and Field2 from Data and I get back something like: new { Field1 = o.Field1, Field2 = o.Field2 };
But I have a need similar to many where I want to plot x and y values, but don't know until run time which ones they are.
So rather than use an anonymous object, I create one that has the properties I want. In this case, X and Y.
Here are the source and target classes:
public class Source
{
public int PropertyA { get; set; }
public double PropertyB { get; set; }
public double PropertyC { get; set; }
}
public class Target
{
public double X { get; set; }
public double Y { get; set; }
}
And here is the code that does the mapping between the Source and the Target.
public static class SelectBuilder
{
/// <summary>
/// Creates a Func that can be used in a Linq Select statement that will map from the source items to a new target type.
/// Typical usage pattern is that you have an Entity that has many properties, but you want to dynamically set properties
/// on a smaller target type, AND, you don't know the mapping at compile time.
/// For example, you have an Entity that has a year and 10 properties. You want to have time (year) as the X axis, but
/// the user can chose any of the 10 properties to plot on the y axis. This would allow you to map one of the entity
/// properties to the Y value dynamically.
/// </summary>
/// <typeparam name="TSource">Type of the source, for example, and Entity Framework entity.</typeparam>
/// <typeparam name="TTarget">Type of the target, a projection of a smaller number of properties than the entity has.</typeparam>
/// <param name="propertyMappings">A list of named tuples that map the sourceProperty to the targetProperty.</param>
/// <returns>A func that can be used inside the Select.
/// So if
/// var select = SelectBuilder.GetSelectStatement<Source, Target>(propertyMappings), then
/// you can perform the select,
/// var results = items.Select(select);</returns>
public static Expression<Func<TSource, TTarget>> GetSelectStatement<TSource, TTarget>(IEnumerable<(string sourceProperty, string targetProperty)> propertyMappings)
{
// Get the source parameter, "source". This will allow the statement to be "X = source.SourceA".
// It needs to be of the source type, and the name is what will be used in the Select lambda.
var sourceParameter = Expression.Parameter(typeof(TSource), "source");
// Now define the ability to create a new Target type.
var newTarget = Expression.New(typeof(TTarget));
// Now develop the bindings or member assignments for each property.
var bindings = new List<MemberAssignment>();
foreach (var propertyMapping in propertyMappings)
{
var sourceMemberInfo = typeof(TSource).GetProperty(propertyMapping.sourceProperty);
var targetMemberInfo = typeof(TTarget).GetProperty(propertyMapping.targetProperty);
// This allows getting the value. Source parameter will provide the "source" part and sourceMemberInfo the property name.
// For example, "source.SourceA".
var sourceValue = Expression.Property(sourceParameter, sourceMemberInfo);
// Provide conversion in the event there is not a perfect match for the type.
// For example, if SourceA is int and the target X is double?, we need to convert from int to double?
var convertExpression = Expression.Convert(sourceValue, targetMemberInfo.PropertyType);
// Put together the target assignment, "X = Convert(source.SourcA, double?)" (TODO: How does the convert actually happen?)
var targetAssignment = Expression.Bind(targetMemberInfo, convertExpression);
bindings.Add(targetAssignment);
}
var memberInit = Expression.MemberInit(newTarget, bindings);
// Here if we map SourceA to X and SourceB to Y the lambda will be:
// {source => new Target() {X = Convert(source.SourceA, Nullable`1), Y = Convert(source.SourceB, Nullable`1)}}
var lambda = Expression.Lambda<Func<TSource, TTarget>>(memberInit, sourceParameter);
return lambda;//.Compile();
}
}
And finally a unit test that works.
[Fact(DisplayName = "GetSelectStatement works")]
public void Test2()
{
// Arrange
var source = new Source { PropertyA = 1, PropertyB = 2, PropertyC = 3 };
var expectedX = Convert.ToDouble(source.PropertyA);
var expectedY = Convert.ToDouble(source.PropertyB);
var items = new List<Source> { source }.AsQueryable();
// Let's map SourceA to X and SourceB to Y.
var propertyMappings = new List<(string sourceProperty, string targetProperty)>
{
("PropertyA", "X"), ("PropertyB", "Y")
//(nameof(Source.PropertyA), nameof(Target.X)),
//(nameof(Source.PropertyB), nameof(Target.Y))
};
// Act
var select = SelectBuilder.GetSelectStatement<Source, Target>(propertyMappings);
var actual = items.Select(select).First();
// Assert
actual.X.Should().Be(expectedX);
actual.Y.Should().Be(expectedY);
}
I've edited my previous answer since now I know how to convert from int to double. I've also made the unit test easier to understand.
I hope this helps others.
result
to be? – AnticlinalData
, if it just have two properties.Data
has five. – VenettaveneziaSelect
and return it as a typedList
orIEnumerable
? – Diplovar result= list.Select(x => new {Field1 = Field1, Field2 = Field2});
– Eta