Tuesday, April 5, 2016

LINQ IQueryable.ToDictionary

The normal ToDictionary() method is built off of IEnumerable. If you use it with EntityFramework and IQueryable you have potential perf problems because it's using a bunch of bandwidth to select fields that just get thrown away.

Here's a smarter ToDictionary method that's built off of IQueryable and not IEnumerable. It integrates with EntityFramework to ensure that you select only the minimal number of columns needed, instead of fetching them all and projecting out only two columns after everything's in memory.

Usage:

var colorDictionary = this.Db.Items.ToDictionary(x => x.Id, x => x.Color); 
// results in the following SQL:

// SELECT 

//     [Extent1].[Id] AS [Id], 

//     [Extent1].[Color] AS [Color]

//     FROM [dbo].[Item] AS [Extent1]


Code:


    public static class IQueryableExtensions

    {

        public static IDictionary<KeyType, ValueType> ToDictionary<T, KeyType, ValueType>(this IQueryable<T> input, Expression<Func<T, KeyType>> keySelector, Expression<Func<T, ValueType>> valueSelector)

        {

            // deconstruct keySelector and valueSelector as member expressions

            var keyAccess = (keySelector.Body as MemberExpression)?.Member; // discard the "x => x" part of x => x.Name, retain .Name

            var valueAccess = (valueSelector.Body as MemberExpression)?.Member;

            if(keyAccess == null || valueAccess == null)

            {

                throw new ArgumentException("keySelector and valueSelector must be of the form 'x => x.MyProperty'");

            }

            var keyType = (keySelector.Body as MemberExpression).Type;

            var valueType = (valueSelector.Body as MemberExpression).Type;

            // Make a new expression, x => new Pair<keyType,valueType> { Key = x.Key, Value = x.Value }

            var pairType = typeof(Pair<,>).MakeGenericType(keyType, valueType);

            var xParam = Expression.Parameter(typeof(T));

            var newExpression =

                Expression.Lambda(

                    Expression.MemberInit(

                        Expression.New(pairType),

                        Expression.Bind(pairType.GetProperty("Key"), Expression.MakeMemberAccess(xParam, keyAccess)),

                        Expression.Bind(pairType.GetProperty("Value"), Expression.MakeMemberAccess(xParam, valueAccess))),

                    xParam)

                as Expression<Func<T, Pair<KeyType, ValueType>>>;

            var pParam = Expression.Parameter(pairType);

            return input.Select(newExpression).AsEnumerable().ToDictionary(kv => kv.Key, kv => kv.Value);

        }

        private class Pair<K, V>

        {

            public K Key { get; set; }

            public V Value { get; set; }

        }

    }


--
If I esteem mankind to be in error, shall I bear them down? No. I will lift them up, and in their own way too, if I cannot persuade them my way is better; and I will not seek to compel any man to believe as I do, only by the force of reasoning, for truth will cut its own way.

I could not love thee, dear, so much,
Loved I not Honor more.

No comments: