-
-
Save RichardD2/43b3785535b51578f55b7d83745e067d to your computer and use it in GitHub Desktop.
Replacement for EF Core .Contains, that avoids SQL Server plan cache pollution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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