3

Would like to be able to populate any properties of an object and search a collection for objects that match the given properties.

class Program
{
    static List<Marble> marbles = new List<Marble> { 
        new Marble {Color = "Red", Size = 3},
        new Marble {Color = "Green", Size = 4},
        new Marble {Color = "Black", Size = 6}
    };

    static void Main()
    {
        var search1 = new Marble { Color = "Green" };
        var search2 = new Marble { Size = 6 };
        var results = SearchMarbles(search1);
    }

    public static IEnumerable<Marble> SearchMarbles(Marble search)
    {
        var results = from marble in marbles
                      //where ???
                      //Search for marbles with whatever property matches the populated properties of the parameter
                      //In this example it would return just the 'Green' marble
                      select marble;
        return results;
    }

    public class Marble
    {
        public string Color { get; set; }
        public int Size { get; set; }
    }

}
Zev Spitz
  • 13,950
  • 6
  • 64
  • 136
fyrplace
  • 53
  • 4

7 Answers7

5

Admittedly, it is interesting and take me time. First, you need to get all properties of search object which have value different with default value, this method is generic using reflection:

var properties = typeof (Marble).GetProperties().Where(p =>
                {
                    var pType = p.PropertyType;
                    var defaultValue = pType.IsValueType 
                            ? Activator.CreateInstance(pType) : null;

                    var recentValue = p.GetValue(search);

                    return !recentValue.Equals(defaultValue);
                });

Then you can use LINQ All to filter:

var results = marbles.Where(m => 
                         properties.All(p => 
                         typeof (Marble).GetProperty(p.Name)
                                        .GetValue(m) == p.GetValue(search)));

P.s: This code has been tested

cuongle
  • 74,024
  • 28
  • 151
  • 206
2

You can use a separate Filter class like this:

class Filter
{
    public string PropertyName { get; set; }
    public object PropertyValue { get; set; }

    public bool Matches(Marble m)
    {
        var T = typeof(Marble);
        var prop = T.GetProperty(PropertyName);
        var value = prop.GetValue(m);
        return value.Equals(PropertyValue);
    }
}

You can use this Filter as follows:

var filters = new List<Filter>();
filters.Add(new Filter() { PropertyName = "Color", PropertyValue = "Green" });

//this is essentially the content of SearchMarbles()
var result = marbles.Where(m => filters.All(f => f.Matches(m)));

foreach (var r in result)
{
    Console.WriteLine(r.Color + ", " + r.Size);
}

You could use DependencyProperties to get rid of typing the property name.

Nico Schertler
  • 32,049
  • 4
  • 39
  • 70
2

I am going to propose the generic solution which will work with any number of properties and with any object. It will also be usable in Linq-To-Sql context - it will translate well to sql.

First, start by defining function which will test if the given value is to be treated as a non-set, e.g:

static public bool IsDefault(object o)
{
    return o == null || o.GetType().IsValueType && Activator.CreateInstance(o.GetType()).Equals(o);
}

Then, we will have a function which constructs a Lambda expression with test against the values of all set properties in search object:

static public Expression<Func<T, bool>> GetComparison<T>(T search)
{
    var param = Expression.Parameter(typeof(T), "t");

    var props = from p in typeof(T).GetProperties()
                where p.CanRead && !IsDefault(p.GetValue(search, null))
                select Expression.Equal(
                    Expression.Property(param, p.Name),
                    Expression.Constant(p.GetValue(search, null))
                );

    var expr = props.Aggregate((a, b) => Expression.AndAlso(a, b));
    var lambda = Expression.Lambda<Func<T, bool>>(expr, param);         
    return lambda;
} 

We can use it on any IQueryable:

public static IEnumerable<Marble> SearchMarbles (Marble search)
{
    var results = marbles.AsQueryable().Where(GetComparison(search));
    return results.AsEnumerable();
}   
Krizz
  • 11,362
  • 1
  • 30
  • 43
1

Assuming a property is unpopulated if it has the default value (i.e. Color == null and Size == 0):

var results = from marble in marbles
              where (marble.Color == search.Color || search.Color == null)
                 && (marble.Size == search.Size || search.Size == 0)
              select marble;
Adam
  • 15,537
  • 2
  • 42
  • 63
Zev Spitz
  • 13,950
  • 6
  • 64
  • 136
1

You could override equals in your Marbles class

public override bool Equals(object obj)
    {
        var other = obj as Marble;

        if (null == other) return false;

        return other.Color == this.color && other.size == this.size; // (etc for your other porperties
    }

and then you could search by

return marbles.Where(m => search == m);
dumdum
  • 838
  • 5
  • 10
  • Don't forget to also override the GetHashCode if you wish to use in Dictionary. – Science_Fiction Sep 23 '12 at 17:04
  • Liking this approach, it still requires property usage but it would make for queries like I was aiming for. – fyrplace Sep 23 '12 at 17:09
  • You could combine this with Desperter's reflection concept so you could implement Equals via reflecting through all the properties and still have your queries be simple equality checks. – dumdum Sep 23 '12 at 17:50
0

Using reflection, this method will work on all types, regardless of how many or what type of properties they contain.

Will skip any properties that are not filled out (null for ref type, default value for value type). If it finds two properties that are filled out that do not match returns false. If all filled-out properties are equal returns true.

IsPartialMatch(object m1, object m2)
{
    PropertyInfo[] properties = m1.GetType().GetProperties();
    foreach (PropertyInfo property in properties)
    {
        object v1 = property.GetValue(m1, null);
        object v2 = property.GetValue(m2, null);
        object defaultValue = GetDefault(property.PropertyType);

        if (v1.Equals(defaultValue) continue;
        if (v2.Equals(defaultVAlue) continue;
        if (!v1.Equals(v2)) return false;
    }

    return true;
}

To apply it to your example

public static IEnumerable<Marble> SearchMarbles(Marble search)
{
    return marbles.Where(m => IsPartialMatch(m, search))
}

GetDefault() is method from this post, Programmatic equivalent of default(Type)

Community
  • 1
  • 1
Despertar
  • 21,627
  • 11
  • 81
  • 79
0

If you want to avoid targeting specific properties, you could use reflection. Start by defining a function that returns the default value of a type (see here for a simple solution, and here for something more elaborate).

Then, you can write a method on the Marble class, which takes an instance of Marble as a filter:

public bool MatchesSearch(Marble search) {
    var t = typeof(Marble);
    return !(
        from prp in t.GetProperties()
        //get the value from the search instance
        let searchValue = prp.GetValue(search, null)
        //check if the search value differs from the default
        where searchValue != GetDefaultValue(prp.PropertyType) &&
              //and if it differs from the current instance
              searchValue != prp.GetValue(this, null)
        select prp
    ).Any();
}

Then, the SearchMarbles becomes:

public static IEnumerable<Marble> SearchMarbles(Marble search) {
    return
        from marble in marbles
        where marble.MatchesSearch(search)
        select marble;
}
Community
  • 1
  • 1
Zev Spitz
  • 13,950
  • 6
  • 64
  • 136
  • I believe `||` should be `&&` because you want to check if there are any properties where searchValue isn't the default value *and* isn't equal to the Marble's property's value. – Risky Martin Sep 23 '12 at 19:31