Home > other >  How to select multiple columns using Expression.Lambda?
How to select multiple columns using Expression.Lambda?

Time:07-01

I have an efcore extension, that you find below. It allows me to select using a property path as string:

_dbContext.MyModel.Select("Name")    

instead of

_dbContext.MyModel.Select(x => x.Name)

This select returns IQueryable<string>. In addition I want to return the Id of the model. Without an extension it would look like this:

_dbContext.MyModel.Select(x => new { Id = x.Id, Name = x.Name });

I want to be able to call e.g.:

_dbContext.MyModel.Select("Name", withId: true)

and return { Id = x.Id, Name = x.Name }. The source type of Id can be either string or Guid. The destination type should be string. Any idea how to achieve that given the implementation below? The extension could also return { string?, string }, string? being the id.

Efcore extension:

public static IQueryable<string> SelectFromPropertyPath<T>(this IQueryable<T> query, string propertyPath)
{
    var parameter = Expression.Parameter(typeof(T), "e");
    var property = MakePropPath(parameter, propertyPath);

    if (property.Type != typeof(string))
    {
        if (property.Type != typeof(object))
        {
            property = Expression.Convert(property, typeof(object));
        }

        property = Expression.Call(_toStringMethod, property);
    }

    var lambda = Expression.Lambda<Func<T, string>>(property, parameter);

    return query.Select(lambda);
}

private static Expression MakePropPath(Expression objExpression, string path)
    => path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);


private static readonly MethodInfo _toStringMethod = typeof(Convert).GetMethods()
    .Single(m =>
        m.Name == nameof(Convert.ToString) && m.GetParameters().Length == 1 &&
        m.GetParameters()[0].ParameterType == typeof(object)
    );

CodePudding user response:

The following extension selects Value and Id based on property names and returns IQueryable<ValueWithId> which can be used later for filtering, grouping, materialization, etc.

var query1 = _dbContext.MyModel.Select("Name"); 
var query2 = _dbContext.MyModel.Select("Name", "OtherId");

And realization:

public static class QueryableExtensions
{
    public class ValueWithId
    {
        public string Id { get; set; }
        public string Value { get; set; }
    }

    public static IQueryable<ValueWithId> Select<T>(this IQueryable<T> query, string valuePath, string idPath = "Id")
    {
        var parameter = Expression.Parameter(typeof(T), "e");
        var idProperty = MakePropPath(parameter, idPath);
        var valueProperty = MakePropPath(parameter, valuePath);

        idProperty = EnsureString(idProperty);
        valueProperty = EnsureString(valueProperty);

        var body = Expression.MemberInit(Expression.New(_valueWithIdConstructor), 
            Expression.Bind(_idProp, idProperty), 
            Expression.Bind(_valueProp, valueProperty));

        var lambda = Expression.Lambda<Func<T, ValueWithId>>(body, parameter);

        return query.Select(lambda);
    }

    static Expression MakePropPath(Expression objExpression, string path)
    {
        return path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);
    }

    private static Expression EnsureString(Expression expression)
    {
        if (expression.Type == typeof(string))
            return expression;

        if (expression.Type != typeof(object))
            expression = Expression.Convert(expression, typeof(object));

        expression = Expression.Call(_toStringMethod, expression);

        return expression;
    }

    private static MethodInfo _toStringMethod = typeof(Convert).GetMethods()
        .Single(m =>
            m.Name == nameof(Convert.ToString) && m.GetParameters().Length == 1 &&
            m.GetParameters()[0].ParameterType == typeof(object)
        );

    private static readonly ConstructorInfo _valueWithIdConstructor = typeof(ValueWithId).GetConstructors().Single();
    private static readonly PropertyInfo _idProp = typeof(ValueWithId).GetProperty(nameof(ValueWithId.Id)) ?? throw new InvalidOperationException();
    private static readonly PropertyInfo _valueProp = typeof(ValueWithId).GetProperty(nameof(ValueWithId.Value)) ?? throw new InvalidOperationException();
}
  • Related