Dynamic Expression using LINQ. How To Find the Kitchens?
Asked Answered
B

5

9

I try do implement a user dynamic filter, where used selects some properties, selects some operators and selects also the values.

As I didn't find yet an answer to this question, I tried to use LINQ expressions.
Mainly I need to identify all houses which main rooms are kitchens(any sens, I know).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
//using System.Linq.Dynamic;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            Room aRoom = new Room() { Name = "a Room" };
            Room bRoom = new Room() { Name = "b Room" };
            Room cRoom = new Room() { Name = "c Room" };

            House myHouse = new House
            {
                Rooms = new List<Room>(new Room[] { aRoom }),
                MainRoom = aRoom
            };
            House yourHouse = new House()
            {
                Rooms = new List<Room>(new Room[] { bRoom, cRoom }),
                MainRoom = bRoom
            };
            House donaldsHouse = new House()
            {
                Rooms = new List<Room>(new Room[] { aRoom, bRoom, cRoom }),
                MainRoom = aRoom
            };

            var houses = new List<House>(new House[] { myHouse, yourHouse, donaldsHouse });

            //var kitchens = houses.AsQueryable<House>().Where("MainRoom.Type = RoomType.Kitchen");
            //Console.WriteLine("kitchens count = {0}", kitchens.Count());

            var houseParam = Expression.Parameter(typeof(House), "house");
            var houseMainRoomParam = Expression.Property(houseParam, "MainRoom");
            var houseMainRoomTypeParam = Expression.Property(houseMainRoomParam, "Type");

            var roomTypeParam = Expression.Parameter(typeof(RoomType), "roomType");

            var comparison = Expression.Lambda(
                Expression.Equal(houseMainRoomTypeParam,
                Expression.Constant("Kitchen", typeof(RoomType)))
                );

            // ???????????????????????? DOES NOT WORK
            var kitchens = houses.AsQueryable().Where(comparison);

            Console.WriteLine("kitchens count = {0}", kitchens.Count());
            Console.ReadKey();

        }
    }

    public class House
    {
        public string Address { get; set; }
        public double Area { get; set; }
        public Room MainRoom { get; set; }
        public List<Room> Rooms { get; set; }
    }

    public class Room
    {
        public double Area { get; set; }
        public string Name { get; set; }
        public RoomType Type { get; set; }
    }

    public enum RoomType
    {
        Kitchen,
        Bedroom,
        Library,
        Office
    }
}
Butter answered 16/8, 2011 at 10:54 Comment(0)
S
6
var kitchens = from h in houses
               where h.MainRoom.Type == RoomType.Kitchen
               select h;

But you must set the RoomType property on the rooms before.

Ok, edit:

so you must redefine:

var comparison = Expression.Lambda<Func<House, bool>>(...

Then, when you use it:

var kitchens = houses.AsQueryable().Where(comparison.Compile());

Edit #2:

Ok, here you go:

var roomTypeParam = Expression.Parameter(typeof(RoomType), "roomType");



// ???????????????????????? DOES NOT WORK
var comparison = Expression.Lambda<Func<House, bool>>(
    Expression.Equal(houseMainRoomTypeParam,
    Expression.Constant(Enum.Parse(typeof(RoomType), "Kitchen"), typeof(RoomType))), houseParam);



// ???????????????????????? DOES NOT WORK
var kitchens = houses.AsQueryable().Where(comparison);

Edit #3: Of, for your needs, I am out of ideas for now. I give you one last one:

Declare an extension method on the String type:

internal static object Prepare(this string value, Type type)
{
    if (type.IsEnum)
        return Enum.Parse(type, value);

    return value;
}

Then use it in that expression like:

Expression.Constant("Kitchen".Prepare(typeof(RoomType)), typeof(RoomType))

That's because apparently enums are treated differently. That extension will leave the string unaltered for other types. Drawback: you have to add another typeof() there.

Synergistic answered 16/8, 2011 at 11:7 Comment(7)
I need a dynamic query. This query is a static one. If the user will select other property instead of "Type" of room, your method should be updated... the RoomType is Kitchen (0) by default.Butter
good remark. Now the runtime exception is "Argument types do not match" when building the "comparison". PS. No need to "Compile" a priori...Butter
@Butter Check if this suits you.Synergistic
Pasib, the solution works. I doubt, however, using it in the real project, because of unclear implementation if we deal with heterogenous and initially unknow filter (list of properties(like Name), operators(like >=) and property values( like "test"))...Butter
errata: "RoomType.Kitchen" is not good for me. I need a string instead of a hardcoded value.Butter
@Butter how about you use Enum.Parse(type, string)? See edits.Synergistic
Enum.Parse... maybe, but this is like i'd used the hardcoded Enum, because Enum.parse is used only with enums. I need a universal string value without specific parsers to Enum, or Double or DateTime... Because I don't know a propri if the value is an enum or not. more that than, I shoud know apriori the type of enum. I have no this kind of data in "design" mode.Butter
F
0
// ???????????????????????? DOES NOT WORK
var kitchens = houses.AsQueryable().Where(comparison);

The Where method takes a Func<House, bool> or a Expression<Func<House, bool>> as the parameter, but the variable comparison is of type LambdaExpression, which doesn't match. You need to use another overload of the method:

var comparison = Expression.Lambda<Func<House, bool>>(
                Expression.Equal(houseMainRoomTypeParam,
                Expression.Constant("Kitchen", typeof(RoomType))));
//now the type of comparison is Expression<Func<House, bool>>

//the overload in Expression.cs
public static Expression<TDelegate> Lambda<TDelegate>(Expression body, params ParameterExpression[] parameters);
Fimbriation answered 16/8, 2011 at 11:8 Comment(1)
I already understood this from Visual Studio. Thank you. However, I don't know how to proceed.Butter
G
0

I wouldn't build the where clause in that way - I think it's more complex than it needs to be for your needs. Instead, you can combine where clauses like this:

var houses = new List<House>(new House[] { myHouse, yourHouse, donaldsHouse });

// A basic predicate which always returns true:
Func<House, bool> housePredicate = h => 1 == 1;

// A room name which you got from user input:
string userEnteredName = "a Room";

// Add the room name predicate if appropriate:
if (!string.IsNullOrWhiteSpace(userEnteredName))
{
    housePredicate += h => h.MainRoom.Name == userEnteredName;
}

// A room type which you got from user input:
RoomType? userSelectedRoomType = RoomType.Kitchen;

// Add the room type predicate if appropriate:
if (userSelectedRoomType.HasValue)
{
    housePredicate += h => h.MainRoom.Type == userSelectedRoomType.Value;
}

// MainRoom.Name = \"a Room\" and Rooms.Count = 3 or 
// ?????????????????????????
var aRoomsHouses = houses.AsQueryable<House>().Where(housePredicate);

I tested this one, honest :)

Genome answered 16/8, 2011 at 11:14 Comment(6)
thank you, I will test it. To be more clear, I try to do something like here: https://mcmap.net/q/1170874/-implement-a-properties-filter-in-net/185593Butter
Unless you need to include user-specified ordering of results, I'd recommend using predicates like this. If you need to allow users to order results, I'd recommend dynamic linq.Genome
as you can see in the GUI from the link of first comment, I can't know a priori the property(es) to apply to the filter. So variables like "userEnteredName" have no sens in my code, as far I don't deal with the property "Name", but with a initially unknown (list of) property(ies) and unknown operator(s).Butter
I assume you know what property each piece of filter data a user posts to your page relates to - even if they're posted like "property1Name:property1Value,property2Name:property2Value" - so you should be able to parse the input to find out which properties have filters(?) Once you know that, you can apply the filters conditionally.Genome
in fact from the filter grid I can obtain a list of properties, then a list of operators, and a list of values. So I need to compare the first property using the first operator to the first value, then the second property using the second operator to the second value, etc... As your code "hardcodes" the operators, si in order to use this code I will need to have 2 switches for each filer grid line (one for select case property, other for select case operator), and also a third one to link that filters (with OR or AND)...Butter
Yeah, fair enough. We're doing something similar on the project we're currently working on and are using dynamic linq without any problems. There's a tiny overhead in the parsing, but we've not seen any performance issues.Genome
U
-1

what about this

var kitchens = houses
                .SelectMany(h => h.Rooms, (h, r) => new {House = h, Room = r})
                .Where(hr => hr.Room.Type == RoomType.Kitchen)
                .Select(hr => hr.House);
Unlucky answered 16/8, 2011 at 11:6 Comment(1)
I need a dynamic query. This query is a static one. If the user will select other property instead of "Type" of room, your method should be updated...Butter
N
-1

To add a new Enum type to dynamic Linq, you must add the following code :

typeof(Enum),
typeof(T)

T : Enum type

in predefined types of dynamic. That works for me.

Needlepoint answered 22/3, 2013 at 13:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.