LINQ查询未调用自定义Equals方法导致返回空数组的原因排查
我定义了如下AgentTask数据类,实现了IEquatable<AgentTask>与IComparable<AgentTask>接口,并重写了Equals、GetHashCode等方法:
using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using static iText.StyledXmlParser.Jsoup.Select.Evaluator; namespace WebApi.Entities { public class AgentTask : IEquatable<AgentTask>, IComparable<AgentTask> { [DefaultValue("1")] [Key] public int FunctionId { get; set; } public string PreferredTime { get; set; } public string UserFunction { get; set; } public string Group { get; set; } public Boolean IsGroup { get; set; } = false; public int CompareTo(AgentTask other) { if (UserFunction.CompareTo(other) != 0) { return UserFunction.CompareTo(other); } else { return Group.CompareTo(other); } } public override bool Equals(object obj) { var newObj = obj as AgentTask; if (null != newObj) { return Equals(newObj); } else { return base.Equals(obj); } } public bool Equals(AgentTask other) { if (null != other) { return this.UserFunction == other.UserFunction //&& this.PreferredTime == newObj.PreferredTime && this.Group == other.Group && this.GetHashCode() == other.GetHashCode(); } else { return base.Equals(other); } } public override int GetHashCode() { int hash = 19; unchecked { // allow "wrap around" in the int hash = hash * 31 + this.UserFunction.GetHashCode(); //hash = hash * 31 + this.PreferredTime.GetHashCode(); // assuming integer hash = hash * 31 + this.Group.GetHashCode(); } return hash; } public override string ToString() { return base.ToString(); } } }
另有包含AgentTask集合的Account类:
public class Account : IdentityUser { ... public List<AgentTask> UserFunctions { get; set; } }
我使用以下LINQ查询,期望返回UserFunctions包含预定义AgentTask实例task的Account数组:
var accounts = _context.Accounts.Include(x => x.UserFunctions) .Where((a) => a.UserFunctions.Any((t) => t.Equals(task))).ToArray();
但accounts始终是空数组。生成的SQL语句如下:
SELECT "a"."Id", "a"."AcceptTerms", "a"."AccessFailedCount", "a"."ConcurrencyStamp", "a"."Created", "a"."DOB", "a"."Email", "a"."EmailConfirmed", "a"."FirstName", "a"."LastName", "a"."LockoutEnabled", "a"."LockoutEnd", "a"."NormalizedEmail", "a"."NormalizedUserName", "a"."NotifyThreeDaysBefore", "a"."NotifyWeekBefore", "a"."PasswordHash", "a"."PasswordReset", "a"."PhoneNumber", "a"."PhoneNumberConfirmed", "a"."ResetToken", "a"."ResetTokenExpires", "a"."Role", "a"."SecurityStamp", "a"."Title", "a"."TwoFactorEnabled", "a"."Updated", "a"."UserName", "a"."VerificationToken", "a"."Verified", "u0"."FunctionId", "u0"."AccountId", "u0"."Group", "u0"."IsGroup", "u0"."PreferredTime", "u0"."UserFunction" FROM "AspNetUsers" AS "a" LEFT JOIN "UserFunctions" AS "u0" ON "a"."Id" = "u0"."AccountId" WHERE EXISTS ( SELECT 1 FROM "UserFunctions" AS "u" WHERE "a"."Id" = "u"."AccountId" AND "u"."FunctionId" = @__entity_equality_task_0_FunctionId) ORDER BY "a"."Id"
请问为何自定义的Equals方法未被LINQ表达式调用,导致查询返回空数组?
核心原因
EF Core无法将自定义的Equals方法转换为SQL语句。EF Core处理LINQ查询时,会把表达式树解析成对应的SQL,而自定义的实例方法(包括重写的Equals)不在EF Core的可转换方法列表内。从生成的SQL能看到,EF Core默认使用实体的主键FunctionId做相等判断,而非你定义的UserFunction和Group字段,这就导致查询逻辑和预期完全不符,最终返回空数组。
另外你的Equals方法存在冗余:判断GetHashCode() == other.GetHashCode()完全没必要,哈希码相等不代表对象相等,且你已经通过UserFunction和Group做了相等性校验,这一步属于画蛇添足。
解决办法
方法一:直接在LINQ中显式指定相等逻辑
把Equals里的判断条件直接写到LINQ查询中,让EF Core能正确解析为SQL:
var accounts = _context.Accounts.Include(x => x.UserFunctions) .Where(a => a.UserFunctions.Any(t => t.UserFunction == task.UserFunction && t.Group == task.Group)) .ToArray();
方法二:配置为EF Core值对象
如果希望AgentTask的相等判断逻辑能被EF Core识别,可以将其配置为值对象。在DbContext的OnModelCreating方法中添加配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Account>() .OwnsMany(a => a.UserFunctions, uf => { uf.Property(t => t.UserFunction).IsRequired(); uf.Property(t => t.Group).IsRequired(); // 可按需配置其他字段,如PreferredTime、IsGroup }); }
配置为值对象后,EF Core会使用你定义的相等逻辑处理查询,但注意值对象没有独立主键,会被嵌入到Account的关联表中,或作为JSON存储(取决于EF Core版本和配置)。
方法三:修正自定义Equals方法(仅适用于内存操作)
如果只是希望Equals在内存操作(如数据库加载数据后判断相等)中生效,可以去掉多余的哈希码判断,修正后的方法如下:
public bool Equals(AgentTask other) { if (other == null) return false; return string.Equals(UserFunction, other.UserFunction, StringComparison.Ordinal) && string.Equals(Group, other.Group, StringComparison.Ordinal); } public override bool Equals(object obj) { return Equals(obj as AgentTask); } public override int GetHashCode() { unchecked { int hash = 19; hash = hash * 31 + (UserFunction?.GetHashCode() ?? 0); hash = hash * 31 + (Group?.GetHashCode() ?? 0); return hash; } }
注意:修正后的方法依然无法被EF Core转换为SQL,仅能用于内存中的集合操作。
内容的提问来源于stack exchange,提问作者janci

