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