tags:

views:

62

answers:

2

I took a look at this answer and it goes in part to solving my issue.

However, what I need is the following.

Given I have an object;

Product
  string code
  List<suitability> items

and then i have this object;

Suitability
  key
  value

Each Product has a variable amount of [items] and the key/value pairs differ from product to product.

When searching I am given a list of Suitability objects. I now need to search all the products that contain (all) of the supplied suitability objects.

So for example a product may have dental = true and therapies = true.

I may get a request for all products that have dental = true and therapies = false.

+2  A: 

First of all I'd like to point out that key-value pairs (AKA EAV model) are a poor choice for representing this kind of data, and this question is a perfect example of why - it's much, much harder to search for arbitrary collections of attributes than it is to search for specific properties.

Of course, it can still be done:

var suitableProducts =
    from p in products
    where
        p.Items.Any(s => s.Key == "dental" && s.Value == "true") &&
        p.Items.Any(s => s.Key == "therapies" && s.Value == "false")
    select p;

It's just not as easy to write or efficient as querying on a Product class that actually has dental and therapies properties as opposed to nested attributes.

If you the number of items you need to query on can change, then the easiest way to handle that is to chain together filters:

var searchItems = ...
var result = products;
foreach (var searchItem in searchItems)
{
    result = result.Where(p =>
        p.Items.Any(s => s.Key == searchItem.Key &&
                         s.Value == searchItem.Value));
}
// Do something with result

If you're looking for a more "functional" way to do it without chaining then:

var suitabilityConditions = searchItems.Select(i =>
    (Predicate<Product>)(p => p.Items.Any(s => 
        s.Key == searchItem.Key && s.Value == searchItem.Value)));
Predicate<Product> match = p => suitabilityConditions.All(c => c(p));
var suitableProducts = products.Where(match);
Aaronaught
@Aaronaught, thanks for this but I can't code each line like you have. I may be given 2 items or 10. As for the approach (EAV) what model would you suggest where each product has a variable number of items attached to it?
griegs
@griegs: Added a version for a dynamic number of items. As for what I would suggest - I would suggest not having a design where each product can have arbitrary sets of attributes. Such designs are anti-patterns, and may *sometimes* be necessary but are usually the result of a combination of vague requirements and a desire to lower maintenance costs through end-user customization. Know that it will actually raise TCO due to myriad issues like this (another issue being that attributes are weakly-typed - what happens if you want to sort?); better to fix the vague requirements instead.
Aaronaught
I hear you and this was discussed but the boss pointed out that we need a way to change the number of items associated to a product to accommodate the businesses changes of mind on a monthly basis.
griegs
Good luck. Unfortunately it's likely that he will learn the hard way how much more it costs to maintain such a design than it does to simply dedicate some development resources to implementing said business changes when they come up.
Aaronaught
I suspect you're right. Thanks for the post btw.
griegs
+1  A: 

Using PredicateBuilder (from the authors of LinqPad) you can build up a query from a set of conditions not known at compile time. The following should be sufficient:

var predicate = PredicateBuilder.True<Product>();

foreach (Suitability criteria in searchCriteria)
{
    string tempKey = criteria.Key;
    string tempValue = criteria.Value;
    predicate = predicate.And(p => 
                     p.Items.Any(s => s.Key == tempKey && s.Value == tempValue));
}

return dataContext.Products.Where(predicate.Compile());

UPDATE: Heres some sample code that I have tested which works using an IEnumerable as the source, the resulting set productsResult contains the first and second products in products list:

var searchCriteria = new List<Suitability>()
    {
        new Suitability() { Key="a", Value="b" },
        new Suitability() { Key="a", Value="c" }
    };

var products = new List<Product>()
    {
        new Product()
            {
                Items = new List<Suitability>() {
                            new Suitability() { Key="a", Value="b" },
                            new Suitability() { Key="a", Value="c" }}
            },
        new Product()
            {
                Items = new List<Suitability>() {
                            new Suitability() { Key="a", Value="b" },
                            new Suitability() { Key="a", Value="c" },
                            new Suitability() { Key="b", Value="c" }}
            },
        new Product()
            {
                Items = new List<Suitability>() {
                            new Suitability() { Key="c", Value="d" }}
            }
    };

    var predicate = PredicateBuilder.True<Product>();

    foreach (Suitability criteria in searchCriteria)
    {
        string tempKey = criteria.Key;
        string tempValue = criteria.Value;
        predicate = predicate.And(p => p.Items.Any(
                         s => s.Key == tempKey && s.Value == tempValue));
    }

    IEnumerable<Product> productsResult = products.Where(predicate.Compile());
Simon Fox
@Simon, I tried implementing this but then I remembered I don't have a datacontext as this is not LINQ to SQL and when I apply this to my IEnumerable<Product> list I get an error. Error The type arguments for method 'System.Linq.Enumerable.Where<TSource>(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource,bool>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
griegs
@griegs add .Compile() to get the Func out of the Expression `predicate`...I have updated my answer.
Simon Fox
@Simon, I've changed my mind and selected this as the prefered answer. Works a treat thank you. I still have to agree with @Aaronaught that the approach of using the EAV model is not ideal but when you have no alternative...
griegs
@griegs Thanks!! No argument from me on the EAV model not being optimal but as you are locked in and have no choice I definitely prefer the simplicity gained using the PredicateBuilder
Simon Fox