Question

I'm in the middle of unit testing a function I wrote to search a queryable of items. I just assert that I get 1 item back, which I should get if the method works. But I get 0 items back. In my method I use deferred execution and just use ToList before I return it. But if I instead change the method to work directly with a list and repeatedly call ToList, I get the correct results.

Am I correct to say that it is not safe to assume that deferred execution produces the same results as Immediate execution?

This is a small app to demonstrate that it returns 0 items

   class Program
    {

        static void Main(string[] args)
        {
            Dictionary<string, string> values = new Dictionary<string, string>()
            {
                 {
                     "Prop1",
                     "*Value*"
                 },
                 {
                     "Prop2",
                     "2*"
                 }
            };
            List<InputItem> items =new List<InputItem>()
            {
                new InputItem()
            };
            Console.WriteLine(Helper.SearchInputItems(items.AsQueryable(), values).Count);
            Console.ReadLine();
        }
    }

    public class InputItem
    {
        public Dictionary<string, string> MappedValues = new Dictionary<string, string>()
        {
            {
                     "Prop1",
                     "This is a value that should be found"
                 },
                 {
                     "Prop2",
                     "2 everything that begins with 2 should be found"
                 }
        };
    }
    public static class Helper
    {
        delegate bool Searcher(string input, string searchString);
        /// <summary>
        /// Searches the added input items.
        /// </summary>
        /// <param name="values">A dictionary of field names and the search pattern for that field.</param>
        /// <returns>List of found InputItems.</returns>
        public static List<InputItem> SearchInputItems(IQueryable<InputItem> inputItems, Dictionary<string, string> values)
        {
            foreach (var value in values)
            {
                string searchString = value.Value;
                Searcher searcher;
                if (searchString.StartsWith("*") && searchString.EndsWith("*"))
                {
                    searcher = new Searcher(StringHelpers.Contains);
                    searchString = searchString.Substring(1);
                    searchString = searchString.Remove(searchString.Length - 1);
                }
                else if (searchString.EndsWith("*"))
                {
                    searcher = new Searcher(StringHelpers.StartsWith);
                    searchString = searchString.Remove(searchString.Length - 1);
                }
                else
                {
                    searcher = new Searcher(StringHelpers.Exact);
                }
                inputItems = inputItems.Where(c =>
                    c.MappedValues.Any(x => x.Key == value.Key) &&
                    searcher(c.MappedValues.First(x => x.Key == value.Key).Value, searchString)
                    );

            }
            return inputItems.ToList();
        }
    }
    public static class StringHelpers
    {
        public static bool Contains(string input, string searchString)
        {
            return input.ToUpperInvariant().Contains(searchString.ToUpperInvariant());
        }
        public static bool StartsWith(string input, string searchString)
        {
            return input.ToUpperInvariant().StartsWith(searchString.ToUpperInvariant());
        }
        public static bool Exact(string input, string searchString)
        {
            return input.ToUpperInvariant() == searchString.ToUpperInvariant();
        }
    }

If I set a breakpoint I can actually see that it checks if 2 everything that begins with 2 should be found contains Value, which it doesn't and returns false. So it seems like the FirstOrDefault in the where clause selects the wrong item

Was it helpful?

Solution

Okay, I've got it. It's the old problem of capturing the loop variable.

Here:

        foreach (var value in values)
        {
            ...
            inputItems = inputItems.Where(c =>
                c.MappedValues.Any(x => x.Key == value.Key) &&
                searcher(c.MappedValues.First(x => x.Key == value.Key).Value, 
                                              searchString)
                );

        }

you're using value within a lambda expression, which means it will use "the current value of value" when that executes... and that value changes as the loop iterates.

Simply use:

foreach (var valueIterationVariable in values)
{
    var value = valueIterationVariable;
    // code as before
}

and I believe it will be okay. (I would question your use of the name "value", by the way, but that's a different matter.)

I haven't looked deeply into why it worked with IEnumerable<T> but not IQueryable<T>, but I suspect the extra deferral was to blame.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top