Skip to content

Instantly share code, notes, and snippets.

@RichardD2
Forked from ErikEJ/IQueryableExtensions.cs
Last active September 14, 2021 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RichardD2/43b3785535b51578f55b7d83745e067d to your computer and use it in GitHub Desktop.
Save RichardD2/43b3785535b51578f55b7d83745e067d to your computer and use it in GitHub Desktop.
Replacement for EF Core .Contains, that avoids SQL Server plan cache pollution
public static class IQueryableExtensions
{
private readonly struct HalfList<T>
{
private readonly IReadOnlyList<T> _list;
private readonly int _startIndex;
private HalfList(IReadOnlyList<T> list, int startIndex, int count)
{
_list = list ?? throw new ArgumentNullException(nameof(list));
_startIndex = startIndex;
Count = count;
}
public HalfList(IReadOnlyList<T> list) : this(list, 0, list.Count)
{
}
public int Count { get; }
public T Item => Count == 1 ? _list[_startIndex] : throw new InvalidOperationException();
public (HalfList<T> left, HalfList<T> right) Split()
{
if (Count < 2) throw new InvalidOperationException();
int pivot = Count >> 1;
var left = new HalfList<T>(_list, _startIndex, pivot);
var right = new HalfList<T>(_list, _startIndex + pivot, Count - pivot);
return (left, right);
}
}
private static Expression CombinePredicates(IReadOnlyList<Expression> parts, Func<Expression, Expression, Expression> fn)
{
if (parts.Count == 0) throw new ArgumentException("At least one part is required.", nameof(parts));
if (parts.Count == 1) return parts[0];
var segment = new HalfList<Expression>(parts);
return CombineCore(segment.Split(), fn);
static Expression CombineCore((HalfList<Expression> left, HalfList<Expression> right) x, Func<Expression, Expression, Expression> fn)
{
var left = x.left.Count == 1 ? x.left.Item : CombineCore(x.left.Split(), fn);
var right = x.right.Count == 1 ? x.right.Item : CombineCore(x.right.Split(), fn);
return fn(left, right);
}
}
public static IQueryable<TQuery> In<TKey, TQuery>(
this IQueryable<TQuery> queryable,
IEnumerable<TKey> values,
Expression<Func<TQuery, TKey>> keySelector)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
if (keySelector == null)
{
throw new ArgumentNullException(nameof(keySelector));
}
if (!values.Any())
{
return queryable.Take(0);
}
var distinctValues = Bucketize(values);
if (distinctValues.Length > 2048)
{
throw new ArgumentException("Too many parameters for SQL Server, reduce the number of parameters", nameof(keySelector));
}
var predicates = distinctValues
.Select(v =>
{
// Create an expression that captures the variable so EF can turn this into a parameterized SQL query
Expression<Func<TKey>> valueAsExpression = () => v;
return Expression.Equal(keySelector.Body, valueAsExpression.Body);
})
.ToList();
var body = CombinePredicates(predicates, Expression.OrElse);
var clause = Expression.Lambda<Func<TQuery, bool>>(body, keySelector.Parameters);
return queryable.Where(clause);
}
private static TKey[] Bucketize<TKey>(IEnumerable<TKey> values)
{
var distinctValueList = values.Distinct().ToList();
// Calculate bucket size as 1,2,4,8,16,32,64,...
var bucket = 1;
while (distinctValueList.Count > bucket)
{
bucket *= 2;
}
// Fill all slots.
var lastValue = distinctValueList.Last();
for (var index = distinctValueList.Count; index < bucket; index++)
{
distinctValueList.Add(lastValue);
}
var distinctValues = distinctValueList.ToArray();
return distinctValues;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment