Search code examples
c#linqlambdalinq-to-entitiesexpression-trees

Expression to convert IQueryable<t> int List<SelectListItem>


I would like to create a repository method like this:

public List<SelectListItem> AllAsSelectListItems(
    Expression<Func<T, string>> valueProperty, 
    Expression<Func<T, string>> textProperty, 
    string selectedValue = "")
{
    // what goes here? I am having serious trouble with this bit!
}

That will enable me to call it like this:

List<SelectListItem> selectListItems = PersonRepository.AllAsSelectListItems(
    m => m.ID,
    m => m.Name,
    selectedIDAsString
);

And, with the selectedValue parameter being "1", it should produce a result like this:

List<SelectListItem>(){
    {Value: "1", Text: "Ted", Selected: true},
    {Value: "2", Text: "Sam", Selected: false},
    {Value: "3", Text: "Tracy", Selected: false}
};

I am having trouble with the generic AllAsSelectListItems() method.

You can see my attempt so far in the code below. But it's not ideal.

I have resorted to hard-coded strings to populate SelectListItem properties with T properties. I think an expression tree is the solution, but I'm struggling to code it correctly.

Also, assigning the ID property breaks it, because it's an int not a string.

Finally, I am also struggling to compare the selectedValue parameter to the SelectListItem.Value property.


Person Class

public class Person
{
    public int ID {get;set;}
    public string Name {get;set;}
}

Controller

public class PersonController : Controller 
{
    public IPersonRepository Repository {get;set;}

    public PersonController(IPersonRepository repository) 
    {
        Repository = repository;
    }

    public ActionResult SelectPerson(int selectedID)
    {
        string selectedIDAsString = selectedID.ToString();
        var selectListItems = Repository.AllAsSelectListItems(
            m => m.ID,
            m => m.Name,
            selectedIDAsString
        );
        return View(selectListItems);
    }
}

Repository

public class PersonRepository : Repository
{
     // various specialised methods 
}

public class Repository<T> : IRepository<T> where T : DBEntity
{
    private ApplicationDbContext db = null;
    private DbSet<T> table = null;

    public RepositoryBase()
    {
        this.db = new ApplicationDbContext();
        table = db.Set<T>();
    }
    public RepositoryBase(ApplicationDbContext db)
    {
        this.db = db;
        table = db.Set<T>();
    }

    protected virtual IQueryable<T> AllAsQueryable(
        params Expression<Func<T, object>>[] includeExpressions)
    {
        return includeExpressions.Aggregate<Expression<Func<T, object>>, IQueryable<T>>
            (table, (current, expression) => current.Include(expression));
    }

    public List<SelectListItem> AllAsSelectListItems(
        Expression<Func<T, string>> valueProperty, 
        Expression<Func<T, string>> textProperty, 
        string selectedValue = "")
    {
        // temp hard coded values until we learn how to use the expression parameters properly
        string valuePropertyHardCoded = "Name";
        string textPropertyHardCoded = "Name";
        Type currentType = typeof(T);
        var itemParam = Expression.Parameter(currentType, "x");
        var valueMember = Expression.PropertyOrField(itemParam, valuePropertyHardCoded);
        var textMember = Expression.PropertyOrField(itemParam, textPropertyHardCoded);

        var selector = Expression.MemberInit(Expression.New(typeof(SelectListItem)),
            Expression.Bind(typeof(SelectListItem).GetMember("Value").Single(), valueMember),
            Expression.Bind(typeof(SelectListItem).GetMember("Text").Single(), textMember)
        );
        var lambda = Expression.Lambda<Func<T, SelectListItem>>(
            selector, itemParam);

        return AllAsQueryable().Select(lambda.Compile()).ToList();
    }
}

Solution

  • You are almost there. There are few things to be realized:

    (A) Binding the passed valueProperty and textProperty expressions to a common parameter. Since the assumption is that they represent a property/field accessor, the passed expression Body should be of type MemberExpression and the actual member info can be extracted from MemberExpression.Member property.

    (B) Generating the Selected assignment by using Expression.Equal

    Putting it all together, it will look something like this

    public List<SelectListItem> AllAsSelectListItems(
            Expression<Func<T, string>> valueProperty,
            Expression<Func<T, string>> textProperty,
            string selectedValue = "")
    {
        if (valueProperty == null) throw new ArgumentNullException("valueProperty");
        if (textProperty == null) throw new ArgumentNullException("textProperty");
        if (!(valueProperty.Body is MemberExpression)) throw new ArgumentException("Must be a field or property.", "valueProperty");
        if (!(textProperty.Body is MemberExpression)) throw new ArgumentException("Must be a field or property.", "textProperty");
        var item = Expression.Parameter(typeof(T), "x");
        var valueMember = Expression.MakeMemberAccess(item, ((MemberExpression)valueProperty.Body).Member);
        var textMember = Expression.MakeMemberAccess(item, ((MemberExpression)textProperty.Body).Member);
        var targetType = typeof(SelectListItem);
        var bindings = new List<MemberBinding>
        {
            Expression.Bind(targetType.GetProperty("Value"), valueMember),
            Expression.Bind(targetType.GetProperty("Text"), textMember)
        };
        if (!string.IsNullOrEmpty(selectedValue))
            bindings.Add(Expression.Bind(targetType.GetProperty("Selected"), Expression.Equal(valueMember, Expression.Constant(selectedValue))));
        var selector = Expression.Lambda<Func<T, SelectListItem>>(
            Expression.MemberInit(Expression.New(targetType), bindings), item);
        var query = AllAsQueryable().Select(selector);
        var result = query.ToList();
        return result;
    }
    

    Update: Unfortunately SelectListItem.Value is of type string, and most of the time the source property (usually some sort of Id) is not a string. So let rename valueProperty to valueSelector and allow passing something like x => x.Id.ToString(). While we cannot easily rebind the passed expression, but we can easily use it unchanged and instead of creating a new parameter, just reuse the parameter of that expression.

    The modified method now would be something like this

    public List<SelectListItem> AllAsSelectListItems(
            Expression<Func<T, string>> valueSelector,
            Expression<Func<T, string>> textProperty,
            string selectedValue = "")
    {
        if (valueSelector == null) throw new ArgumentNullException("valueSelector");
        if (textProperty == null) throw new ArgumentNullException("textProperty");
        if (!(textProperty.Body is MemberExpression)) throw new ArgumentException("Must be a field or property.", "textProperty");
        var item = valueSelector.Parameters[0];
        var itemValue = valueSelector.Body;
        var itemText = Expression.MakeMemberAccess(item, ((MemberExpression)textProperty.Body).Member);
        var targetType = typeof(SelectListItem);
        var bindings = new List<MemberBinding>
        {
            Expression.Bind(targetType.GetProperty("Value"), itemValue),
            Expression.Bind(targetType.GetProperty("Text"), itemText)
        };
        if (!string.IsNullOrEmpty(selectedValue))
            bindings.Add(Expression.Bind(targetType.GetProperty("Selected"), Expression.Equal(itemValue, Expression.Constant(selectedValue))));
        var selector = Expression.Lambda<Func<T, SelectListItem>>(Expression.MemberInit(Expression.New(targetType), bindings), item);
        var query = AllAsQueryable().Select(selector);
        var result = query.ToList();
        return result;
    }