| | 1 | | using System.Data; |
| | 2 | | using System.Diagnostics; |
| | 3 | | using System.Reflection; |
| | 4 | |
|
| | 5 | | namespace Pozitron.QuerySpecification; |
| | 6 | |
|
| | 7 | | internal static class LikeExtension |
| | 8 | | { |
| | 9 | | private static readonly MethodInfo _likeMethodInfo = typeof(DbFunctionsExtensions) |
| | 10 | | .GetMethod(nameof(DbFunctionsExtensions.Like), [typeof(DbFunctions), typeof(string), typeof(string)])!; |
| | 11 | |
|
| | 12 | | private static readonly MemberExpression _functions = Expression.Property(null, typeof(EF).GetProperty(nameof(EF.Fun |
| | 13 | |
|
| | 14 | | // It's required so EF can generate parameterized query. |
| | 15 | | // In the past I've been creating closures for this, e.g. var patternAsExpression = ((Expression<Func<string>>)(() = |
| | 16 | | // But, that allocates 168 bytes. So, this is more efficient way. |
| | 17 | | private static MemberExpression StringAsExpression(string value) => Expression.Property( |
| | 18 | | Expression.Constant(new StringVar(value)), |
| | 19 | | typeof(StringVar).GetProperty(nameof(StringVar.Format))!); |
| | 20 | |
|
| | 21 | | // We'll name the property Format just so we match the produced SQL query parameter name (in case of interpolated st |
| | 22 | | private record StringVar(string Format); |
| | 23 | |
|
| | 24 | | public static IQueryable<T> ApplyLikesAsOrGroup<T>(this IQueryable<T> source, ReadOnlySpan<SpecItem> likeItems) |
| | 25 | | { |
| | 26 | | Debug.Assert(_likeMethodInfo is not null); |
| | 27 | |
|
| | 28 | | Expression? combinedExpr = null; |
| | 29 | | ParameterExpression? mainParam = null; |
| | 30 | | ParameterReplacerVisitor? visitor = null; |
| | 31 | |
|
| | 32 | | foreach (var item in likeItems) |
| | 33 | | { |
| | 34 | | if (item.Reference is not SpecLike<T> specLike) continue; |
| | 35 | |
|
| | 36 | | mainParam ??= specLike.KeySelector.Parameters[0]; |
| | 37 | |
|
| | 38 | | var selectorExpr = specLike.KeySelector.Body; |
| | 39 | | if (mainParam != specLike.KeySelector.Parameters[0]) |
| | 40 | | { |
| | 41 | | visitor ??= new ParameterReplacerVisitor(specLike.KeySelector.Parameters[0], mainParam); |
| | 42 | |
|
| | 43 | | // If there are more than 2 likes, we want to avoid creating a new visitor instance (saving 32 bytes per |
| | 44 | | // We're in a sequential loop, no concurrency issues. |
| | 45 | | visitor.Update(specLike.KeySelector.Parameters[0], mainParam); |
| | 46 | | selectorExpr = visitor.Visit(selectorExpr); |
| | 47 | | } |
| | 48 | |
|
| | 49 | | var patternExpr = StringAsExpression(specLike.Pattern); |
| | 50 | |
|
| | 51 | | var likeExpr = Expression.Call( |
| | 52 | | null, |
| | 53 | | _likeMethodInfo, |
| | 54 | | _functions, |
| | 55 | | selectorExpr, |
| | 56 | | patternExpr); |
| | 57 | |
|
| | 58 | | combinedExpr = combinedExpr is null |
| | 59 | | ? likeExpr |
| | 60 | | : Expression.OrElse(combinedExpr, likeExpr); |
| | 61 | | } |
| | 62 | |
|
| | 63 | | return combinedExpr is null || mainParam is null |
| | 64 | | ? source |
| | 65 | | : source.Where(Expression.Lambda<Func<T, bool>>(combinedExpr, mainParam)); |
| | 66 | | } |
| | 67 | | } |
| | 68 | |
|
| | 69 | | internal sealed class ParameterReplacerVisitor : ExpressionVisitor |
| | 70 | | { |
| | 71 | | private ParameterExpression _oldParameter; |
| | 72 | | private Expression _newExpression; |
| | 73 | |
|
| 11 | 74 | | internal ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression) => |
| 11 | 75 | | (_oldParameter, _newExpression) = (oldParameter, newExpression); |
| | 76 | |
|
| | 77 | | internal void Update(ParameterExpression oldParameter, Expression newExpression) => |
| 10 | 78 | | (_oldParameter, _newExpression) = (oldParameter, newExpression); |
| | 79 | |
|
| | 80 | | protected override Expression VisitParameter(ParameterExpression node) => |
| 13 | 81 | | node == _oldParameter ? _newExpression : node; |
| | 82 | | } |