Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apply Linq Func<T, TResult> key selector at single element level

Sorry if the title is misleading, wasn't sure how to describe this one.

My end goal is to have an extension method of IQueryable<T> and some form (see below for example) of expression that will allow me to have to return an IQueryable<EntityIndex<T>> (or similar) which contains the original T in the Entity field, and an array/ienumerable containing the elements as describe by the some form of expression.

I know that doesn't really make sense, hopefully it will after an example...

This is what I have so far:

class EntityIndex<T, TKey>
{
    T Entity { get; set; }
    // Doesn't have to be IEnumerable, whatever is easier
    IEnuermable<TKey> Index { get; set; }
}
static class Elsewhere
{
    [Extension()]
    public IQueryable<EntityIndex<T, TKey>> IndexBy<T, TKey>(this IQueryable<T> source, Expression<Func<T, TKey[]>> indexSelector)
    {
        return source.Select(n => new EntityIndex<T, TKey> {
            Entity = n,
            Index = new T[] { n }.Select(indexSelector)
        });
    }
}

Note: The above does not compile, it's simply there to try and show what I'm trying to achieve.

I've used the standard selector, but sub-optimally, had to arbitrarily create an array of T on the assignment to the 'Index' property to be able to apply the selector. I'm hoping a better choice of parameter may resolve this, but possibly not. The main issue is this doesn't compile so if there is a minor tweak that will allow it to work that's fine by me, if you can understand my gibberish and understand what I'm trying to do, and happen to know a better way to go about it I'd be greatly appreciative.

Ideally, I need the solution to be understood by the L2S engine, which I'm not convinced the above is thanks to the introduction of the EntityIndex class, but I'm holding out hope that it'll treat it as an anonymous class.

EDIT:

Good point Damien, the bigger picture is probably much easier to describe...

I want an extension method that accepts an expression, the expression should describe which fields on the entity to index, which will be used after this particular expression to allow a criterion (where clause) to be applied to the selected fields.

Long story short, in a number of places in code we have a wildcard string search. If I have an EntityA with Property1, Property2, Property3, etc, it is not uncommon to see code such as:

Handwritten, please excuse minor typos

public string[] WildcardSearch(string prefixText, int count)
{
    string searchTerm = prefixText.Replace(wildcard, string.Empty);
    if (prefixText.StartsWith(wildcard) && prefixText.EndsWith(wildcard)) {
        return entitySet.Where(n => n.Property1.Contains(searchTerm) || n.Property2.Contains(searchTerm)).Select(n => n.Property3).ToArray();
    } else if (prefixText.StartsWith(wildcard)) {
        return entitySet.Where(n => n.Property1.EndsWith(searchTerm) || n.Property2.EndsWith(searchTerm)).Select(n => n.Property3).ToArray();
        // you get the picture, same with EndsWith, no wildcards defaults to contains...
    }
}

EDIT:

Further clarification - using the above WildcardEarch as an example, what I was hoping for was to be able to have a selector as follows or similar:

Func<EntityA, IEnumerable<string>> indexSelector = n => new string[] {
    n.Property1,
    n.Property2
};

// Alternatively, a ParamArray of keySelector might work?
Func<EntityA, string>[] keySelectors = new Func<EntityA, string>[] {
    n => n.Property1,
    n => n.Property2
};

Given an adequate expression describing which fields on the entity to search, returning the IQueryable<EntitySearch<T>> as shown above, I hoped to be able to apply a single criterion, similar to:

Func<EntitySearch<T>, bool> criterion = n => false;
if (wildcardIsContains) {
    criterion = n => n.Values.Any(x => x.Contains(searchTerm));
} else if (wildCardIsStartsWith) {
    criterion = n => n.Values.Any(x => x.Contains(searchTerm));
    //etc
}

Given the extension at the very top that I can't get to work, and this criterion logic, I should be able to take an IQueryable<T>, select some fields, and apply an appropriate wildcard search on the fields, finally returning IQueryable<T> again having added the filtering.

Thanks¬!

Please comment if you need more information/clarification...

EDIT: Fair one @subkamren and thanks for the interest. Some non-generic examples may be of use. I'll draft something up and add them shortly. For the time being, some clarification based on your comment...

Given an IQueryable<Animal> I want an extension allowing me to select fields on Animal which I intend to search/index by. For example, Animal.Description, Animal.Species.Name etc. This extension should return something like an IIndexedQueryable<Animal>. That is the issue I'm trying to deal with in the question above. The wider picture mentioned, which I'd be exceptionally pleased if you're willing to help with, is as follows:

The IIndexedQueryable<T> interface in turn I would like an extension for which could take a string search term. The extension should resolve the wildcards within the search term, extend the original IQueryable with the necessary criterion to perform a search on the indexed fields, and return an IQueryable<T> again.

I appreciate this could be done in a single step, but I hoped to do it this way so that later on I can look into adding a third extension method applicable to IIndexedQueryable<T> allowing me to perform a freetext search with SQL Server... ^^ Make any sense?

That's the bigger picture at least, this question deals primarily with being able to specify the fields I aim to index in such a way I can use them thereafter as mentioned here.

like image 370
Smudge202 Avatar asked Dec 14 '11 09:12

Smudge202


1 Answers

So something like:

public static IEnumerable<EntityIndex<T, Y>> IndexBy<T, Y>(this IEnumerable<T> entities, Func<T, Y> indexSelector) {
    return entities.Select(e => new EntityIndex<T, Y> { Entity = e, IndexValue = indexSelector(e) });
}

Noting that generically defining EntityIndex with the TIndexType (called Y here) is important because you don't know ahead of time what the index is. The use of a generic allows Y to be an enumeration, thus the following would work as an index selector:

// Assuming Animal has attributes "Kingdom", "Phylum", "Family", "Genus", "Species"
// this returns an enumeration of EntityIndex<Animal, String[]>
var animalsClassified = someAnimals.IndexBy(a => new String[] { a.Kingdom, a.Phylum, a.Family, a.Genus, a.Species });

EDIT (Adding further detail):

Using the above, you can group the results by unique index value:

var animalClassifications = animalsClassified
                                .SelectMany(ac => ac.IndexValue.Select(iv => new { IndexValue = iv, Entity = ac.Entity }))
                                .GroupBy(kvp => kvp.IndexValue)

What I've described here, by the way, is (a very simplified form of) the MapReduce algorithm as popularized by Google. A distributed form of the same is commonly used for keyword identification in text search, where you want to build an index of (search term)->(list of containing documents).

like image 177
Chris Shain Avatar answered Oct 28 '22 12:10

Chris Shain