Dekim
Dekim

Reputation: 654

LINQ with reflection for nested property values

I'm looking to create a Linq Where, based on a nested property.

Let's say this is my project:

public class Car {
    public Engine Engine { get; set; }
}

public class Engine {
    public int HorsePower { get; set; }
}

var myCar = new Car() {
    Engine = new Engine() {
        HorsePower = 400
    }
};

I use this code found somewhere, which allow to create Expression,

private Expression<Func<TItem, bool>> PropertyEquals<TItem, TValue>( PropertyInfo property, TValue value ) {
    var param = Expression.Parameter( typeof( TItem ) );
    var memberExp = Expression.Property( param, property );

    BinaryExpression body;

    //If nullable, Expression.Equal won't work even if the value is not null. So we convert to non nullable (the compared expression)
    Type typeIfNullable = Nullable.GetUnderlyingType( memberExp.Type );
    if ( typeIfNullable != null ) {
        var convertedExp = Expression.Convert( memberExp, Expression.Constant( value ).Type );
        body = Expression.Equal( convertedExp, Expression.Constant( value ) );
    } else {
        body = Expression.Equal( memberExp, Expression.Constant( value ) );
    }

    return Expression.Lambda<Func<TItem, bool>>( body, param );
}

Now, I had like to have an equivalent, calling my method PropertyEquals

var engine = myCar.Select( c => c.Engine.HorsePower == 400 );

Something like

var property = GetPropertyForDotSequence( typeof( Car ), "Engine.HorsePower" );
.Select( PropertyEquals<TEntity, int>( property , 400 ) );

I found a method which allow to find property based on a dotted format (GetPropertyForDotSequence), which work properly, but return HorsePower property info, and not Engine.HorsePower, so I get an error saying Car doesnot have an int32 property called HorsePower.

private PropertyInfo GetPropertyForDotSequence ( Type baseType, string propertyName ) {
        var parts = propertyName.Split( '.' );

        return ( parts.Length > 1 )
            ? GetPropertyForDotSequence( baseType.GetProperty( parts[ 0 ] ).PropertyType, parts.Skip( 1 ).Aggregate( ( a, i ) => a + "." + i ) )
            : baseType.GetProperty( propertyName );
    }

Upvotes: 1

Views: 1463

Answers (2)

Ivan Stoev
Ivan Stoev

Reputation: 205549

In order to achieve your goal, instead of using a separate helper function to extract the last property info from a property path and then passing the property info to your function, all that should be done inside the function itself, i.e. it should receive the string containing the property path like this

public static partial class Utils
{
    public static Expression<Func<TItem, bool>> PropertyEquals<TItem, TValue>(string propertyPath, TValue value)
    {
        var source = Expression.Parameter(typeof(TItem), "source");
        var propertyNames = propertyPath.Split('.');
        var member = Expression.Property(source, propertyNames[0]);
        for (int i = 1; i < propertyNames.Length; i++)
            member = Expression.Property(member, propertyNames[i]);
        Expression left = member, right = Expression.Constant(value, typeof(TValue));
        if (left.Type != right.Type)
        {
            var nullableType = Nullable.GetUnderlyingType(left.Type);
            if (nullableType != null)
                right = Expression.Convert(right, left.Type);
            else
                left = Expression.Convert(left, right.Type);
        }
        var body = Expression.Equal(left, right);
        var expr = Expression.Lambda<Func<TItem, bool>>(body, source);
        return expr;
    }
}

I'm not quite sure how it can be useful though because the signature does not allow inferring the generic types, so it would require something like this

var predicate = Utils.PropertyEquals<Car, int>("Engine.HorsePower", 400);
bool result = predicate.Compile().Invoke(myCar);

IMO it would be useful if used with combination of the following extension methods

public static partial class Utils
{
    public static IQueryable<T> WherePropertyEquals<T, TValue>(this IQueryable<T> source, string propertyPath, TValue value)
    {
        return source.Where(PropertyEquals<T, TValue>(propertyPath, value));
    }
    public static IEnumerable<T> WherePropertyEquals<T, TValue>(this IEnumerable<T> source, string propertyPath, TValue value)
    {
        return source.Where(PropertyEquals<T, TValue>(propertyPath, value).Compile());
    }
}

so you can write something like this

List<Car> cars = new List<Car> { myCar };
var cars400 = cars.WherePropertyEquals("Engine.HorsePower", 400).ToList();

Upvotes: 1

Arghya C
Arghya C

Reputation: 10068

You can use this method to get the property value from an object with the nested property name as string

public static object GetNestedPropertyValue(object obj, string nestedDottedPropertyName)
{
    foreach (String part in nestedDottedPropertyName.Split('.'))
    {
        if (obj == null)
            return null;

        PropertyInfo info = obj.GetType().GetProperty(part);
        if (info == null)
            return null;

        obj = info.GetValue(obj, null);
    }
    return obj;
}

But this is not valid Linq statement

var engine = myCar.Select( c => c.Engine.HorsePower == 400 );

What you can do instead is, if you have a car object like this

var myCar = new Car()
{
    Engine = new Engine()
    {
        HorsePower = 400
    }
};

You can get the value of Engine.HorsePower as

var horsePower = (int)GetNestedPropertyValue(myCar, "Engine.HorsePower");

Edit

For a Linq example, if you have a List<Car> like this

var myCar2 = new Car()
{
    Engine = new Engine()
    {
        HorsePower = 800
    }
};
var cars = new List<Car> { myCar, myCar2 }; //myCar defined above

You can use Linq as

var car400 = cars.FirstOrDefault(c => (int)GetNestedPropertyValue(c, "Engine.HorsePower") == 400); //=> myCar
var horsePowers = cars.Select(c => (int)GetNestedPropertyValue(c, "Engine.HorsePower")); //=> 400, 800

Upvotes: 0

Related Questions