EFCore中使用Where查詢時進行多個字段匹配


背景

  在EFCore中我們經常有這樣一種需求那就是我們需要同時匹配一個實體中的多個字段,這個在EFCore中一般的代碼都是匹配特定的字段,如果遇到這種情況我們該如何准確進行匹配呢?這個時候就需要用到我們今天提到的擴展方法。

查詢實例

  在下面的例子中toAddVehicleOrderPlans是我們前面已經查詢並放到內存中的一個集合對象,這里我們將這個集合中的DealerId、YearOfPlan、WeekOfPlan這幾個對象放到一個匿名集合中,然后用這個集合來匹配_weeklyOrderPlanRepository中的對象,這里我們用到了一個WhereByMultiOr的擴展方法,這個方法能夠將最終的Linq查詢轉變成我們想要的結果。

  var filterPlans = toAddVehicleOrderPlans.Select(r => new { r.DealerId, r.YearOfPlan, r.WeekOfPlan }).ToArray();
            var toDeleteEntities = await _weeklyOrderPlanRepository.GetAll()
                .WhereByMultiOr(filterPlans, (d, f) => d.DealerId == f.DealerId && d.YearOfPlan == f.YearOfPlan && d.WeekOfPlan == f.WeekOfPlan)
                .ToListAsync();

過程分析

  我們首先來看看我們是怎么來定義這個方法的。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Sunlight.EFCore.Extensions {
    /// <summary>
    /// 拼接生成多個 Or 的Where語句
    /// </summary>
    public static class WhereByMultiOrExtend {
        /// <summary>
        /// 多個or條件過濾, 如 Boms.Where(b=> (b.Code == "a" and b.Name == "A") or (b.Code == "b" and b.Name == "B"))
        /// <para> filters 可以是查詢出來的。</para>
        /// <code>
        /// var filters = new Dictionary<string, string>();
        /// filters.Add("C70GCCC4A004YAC", "1.0");
        /// filters.Add("C30DABC1Q003KAF", "1.0");
        /// ObjectContext.DomEditions.WhereByMultiOr(filters.ToList(), (b, f) => b.ProductCode == f.Key && b.BatchCode == f.Value);
        /// </code>
        /// </summary>
        /// <typeparam name="T">被過濾的數據類型</typeparam>
        /// <typeparam name="TFilter">條件的類型</typeparam>
        /// <param name="entitySet">數據集</param>
        /// <param name="filters">過濾條件集合</param>
        /// <param name="predicate"></param>
        /// <returns></returns>
        public static IQueryable<T> WhereByMultiOr<T, TFilter>(this IQueryable<T> entitySet, IEnumerable<TFilter> filters,
            Expression<Func<T, TFilter, bool>> predicate) {
            var innerFilters = filters.ToArray();
            if (innerFilters == null || innerFilters.Length == 0)
                throw new ArgumentOutOfRangeException(nameof(filters));
            //條件參數數據常量化,所以參數只保留 被過濾的數據
            var pe = predicate.Parameters.First();
            var orElseExpressions = innerFilters.Select(filter => new ParameterModifier<TFilter>().Modify(predicate, filter))
                .ToList();
            var predicateBody = orElseExpressions.First();
            if (orElseExpressions.Count > 1) {
                for (var i = 1; i < orElseExpressions.Count; i++) {
                    predicateBody = Expression.OrElse(predicateBody, orElseExpressions[i]);
                }
            }

            var whereCallExpression = Expression.Call(
                typeof(Queryable),
                "Where",
                new[] { typeof(T) },
                entitySet.Expression,
                Expression.Lambda<Func<T, bool>>(predicateBody, pe)
            );
            return entitySet.Provider.CreateQuery<T>(whereCallExpression);
        }

        private class ParameterModifier<TFilter> : ExpressionVisitor {
            private TFilter _localParam;

            /// <summary>
            /// 將predicate中用到的F里的數據作為常量放到表達式里。如 TF 是 KeyValueItem, key="abc", value = "1.0",
            /// <para> 則表達式 (b, f) => b.ProductCode == f.Key && b.BatchCode == f.Value)</para>
            /// 變成 (b.ProductCode == "abc" && b.BatchCode == "1.0")
            /// </summary>
            /// <param name="expression">表達式</param>
            /// <param name="param">條件對象</param>
            /// <returns></returns>
            public Expression Modify(Expression expression, TFilter param) {
                _localParam = param;
                return Visit(expression);
            }

            protected override Expression VisitBinary(BinaryExpression node) {
                var left = Visit(node.Left);
                var right = Visit(node.Right);
                string memberName;
                if (CheckForParameter(left))
                    left = Expression.Constant(_localParam);
                else if (CheckForProperty(left, out memberName))
                    left = Expression.Constant(_localParam.GetType().GetProperty(memberName).GetValue(_localParam, null));
                if (CheckForParameter(right))
                    right = Expression.Constant(_localParam);
                else if (CheckForProperty(right, out memberName))
                    right = Expression.Constant(_localParam.GetType().GetProperty(memberName)
                        .GetValue(_localParam, null));
                // right為常量的時候,需要轉換成和left一樣的類型,比如 int 轉換成 Nullable<int>
                right = Expression.Convert(right, left.Type);
                return Expression.MakeBinary(node.NodeType, left, right);
            }

            /// <summary>
            /// 常量訪問判斷
            /// </summary>
            /// <param name="e"></param>
            /// <returns></returns>
            private static bool CheckForParameter(Expression e) {
                return e is ParameterExpression;
            }

            /// <summary>
            /// 屬性訪問判斷
            /// </summary>
            /// <param name="e"></param>
            /// <param name="memberName"></param>
            /// <returns></returns>
            private static bool CheckForProperty(Expression e, out string memberName) {
                memberName = string.Empty;
                if (!(e is MemberExpression me) || me.Expression.Type != typeof(TFilter))
                    return false;
                memberName = me.Member.Name;
                return true;
            }

            /// <summary>
            /// 因為要去掉參數,這里只取Body
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="node"></param>
            /// <returns></returns>
            protected override Expression VisitLambda<T>(Expression<T> node) {
                return Visit(node.Body);
            }
        }
    }
}

  這個里面的核心是如何按照我們的想法拼接自定義的Expression,這里我們創建泛型ParameterModifier<TFilter> 繼承自 ExpressionVisitor,然后最核心的就是要重寫這個基類里面的VisitBinary方法,后面為了方便進行測試我們使用了XUnit的框架對我們寫的這個類進行測試,測試用例覆蓋越全面整個方法就越准確,我們來看看我們的測試用例,這里以WhereByMultiOr_FilterByStringAndInt_Success這個單元測試為例,我們來看看這些方法都生成了些什么?這個方法中的核心是創建ParameterModifier的實例,然后調用Modify方法,所以這個Modify方法最核心的功能,就像注釋說的一樣:表達式 (b, f) => b.ProductCode == f.Key && b.BatchCode == f.Value)轉變成(b.ProductCode == "abc" && b.BatchCode == "1.0")這個是最后轉成SQL的關鍵,其它部分的內容可以通過調試每一個方法來看看生成了些什么,這樣就能夠加深對整個過程的理解。

using System;
using System.Collections.Generic;
using System.Linq;
using Shouldly;
using Sunlight.EFCore.Extensions;
using Xunit;

namespace Sunlight.Framework.EFCore.Tests {
    public class WhereByMultiOrExtend_Tests {
        private class Student {
            public int Id { get; set; }
            public string Code { get; set; }
            public string Name { get; set; }
            public int Age { get; set; }
            public int? Weight { get; set; }
        }

        private IQueryable<Student> GenerateStudents() {
            var entitySet = new List<Student>() {
                new Student {
                    Id = 1,
                    Code = "Stu01",
                    Name = "王二",
                    Age = 22,
                    Weight = 110
                },new Student {
                    Id = 2,
                    Code = "Stu02",
                    Name = "張三",
                    Age = 22,
                    Weight = 120
                },
                new Student {
                    Id = 3,
                    Code = "Stu03",
                    Name = "李四",
                    Age = 24,
                    Weight = 130
                }
            };

            return entitySet.AsQueryable();
        }

        [Fact]
        public void WhereByMultiOr_EmptyFilters_ThrowException() {
            // Arrange
            var students = GenerateStudents();
            var filters = Enumerable.Empty<Student>();
            // Assert
            Assert.Throws<ArgumentOutOfRangeException>(
                // Act
                () => students.WhereByMultiOr(filters, (s, f) => s.Code == f.Code && s.Name == f.Name)
            );
        }

        [Fact]
        public void WhereByMultiOr_FilterByStringAndInt_Success() {
            // Arrange
            var students = GenerateStudents();
            var filters = new[] {
                new {Name = "王二", Age = 22}
            };
            // Act
            var results = students.WhereByMultiOr(filters, (s, f) => s.Name == f.Name && s.Age == f.Age);
            // Assert
            results.Count().ShouldBe(1);
        }

        [Fact]
        public void WhereByMultiOr_FilterByIntAndNullableInt_Success() {
            // Arrange
            var students = GenerateStudents();
            var filters = new[] {
                new {Wight = (int?)120, Age = 22},
                new {Wight = (int?)130, Age = 24}
            };
            // Act
            var results = students.WhereByMultiOr(filters, (s, f) => s.Weight == f.Wight && s.Age == f.Age);
            // Assert
            results.Count().ShouldBe(2);
        }
    }
}

  通過這些單元測試用例我們就能夠准確去理解代碼中每一行的意義然后生成正確SQL語句,最后我們回到一開始我們提出的那個問題,這段代碼我們看看在實際的SQL語句是否符合預期效果。

SELECT
  d.Id,
  d.BranchId,
  d.Code,
  d.ConfirmationTime,
  d.ConfirmorId,
  d.ConfirmorName,
  d.CreateTime,
  d.CreatorId,
  d.CreatorName,
  d.DealerCode,
  d.DealerId,
  d.DealerName,
  d.EndTime,
  d.ModifierId,
  d.ModifierName,
  d.ModifyTime,
  d.MonthOfPlan,
  d.Remark,
  d.RowVersion,
  d.StartTime,
  d.Status,
  d.TotalAmount,
  d.Type,
  d.VehicleSalesOrgCode,
  d.VehicleSalesOrgId,
  d.VehicleSalesOrgName,
  d.VehicleWarehouseCode,
  d.VehicleWarehouseId,
  d.VehicleWarehouseName,
  d.WeekOfPlan,
  d.YearOfPlan
FROM VehicleOrderPlan d
WHERE (((((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 37)) OR
        (((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 38))) OR
       (((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 39))) OR
      (((d.DealerId = 44) AND (d.YearOfPlan = 2020)) AND (d.WeekOfPlan = 40))

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM