内存数据库中EF Core按byte[] Hash分组的单元测试问题解决方案
在SQL Server环境下运行正常的EF Core查询方法,改用内存数据库做单元测试时遇到分组失效问题:由于byte[]类型的Hash属性默认按引用比较,值完全相同的数组无法被正确归为同一分组。
原查询方法代码:
public async Task<IEnumerable<HashRecord>> GetLatestHashes(long jobId, CancellationToken token) { var context = _contextFactory.Create(); var hashes = await context.Set<PRH>() .AsNoTracking() .Where(x => x.JobId == jobId) .GroupBy(x => x.Hash) .Select(g => new HashRecord { Hash = g.Key, Status = g.Any(x => x.Status == RPStatus.Successful) ? RPStatus.Successful : g.Any(x => x.Status == RPStatus.Failed) ? RPStatus.Failed : RPStatus.Skipped }) .ToArrayAsync(token); return hashes; }
PRH实体类定义:
public class PRH { public long JobId { get; set; } public RPStatus Status { get; set; } public byte[] Hash { get; set; } = Array.Empty<byte>(); }
1. 为byte[]注册自定义相等比较器
EF Core内存数据库支持为特定类型替换默认比较逻辑,只需创建一个按值比较的byte[]比较器,再配置到内存数据库选项中:
首先定义比较器:
public class ByteArrayEqualityComparer : IEqualityComparer<byte[]> { public bool Equals(byte[] x, byte[] y) { if (x == null && y == null) return true; if (x == null || y == null) return false; return StructuralComparisons.StructuralEqualityComparer.Equals(x, y); } public int GetHashCode(byte[] obj) { if (obj == null) return 0; int hash = 17; foreach (byte b in obj) { hash = hash * 31 + b.GetHashCode(); } return hash; } }
然后在测试中配置内存数据库:
var options = new DbContextOptionsBuilder<YourDbContext>() .UseInMemoryDatabase(databaseName: "TestDb") .ReplaceService<IEqualityComparer<byte[]>, ByteArrayEqualityComparer>() .Options;
配置完成后,内存数据库会使用该比较器判断byte[]的相等性,分组逻辑即可正常工作。
2. 转换byte[]为字符串作为分组依据
修改查询逻辑,将byte[]转换为Base64字符串(或十六进制字符串),以此作为分组键——字符串默认按值比较,能解决内存数据库的分组问题:
var hashes = await context.Set<PRH>() .AsNoTracking() .Where(x => x.JobId == jobId) .Select(x => new { x.Status, HashString = Convert.ToBase64String(x.Hash) }) .GroupBy(x => x.HashString) .Select(g => new HashRecord { Hash = context.Set<PRH>() .Where(x => x.JobId == jobId && Convert.ToBase64String(x.Hash) == g.Key) .Select(x => x.Hash) .First(), Status = g.Any(x => x.Status == RPStatus.Successful) ? RPStatus.Successful : g.Any(x => x.Status == RPStatus.Failed) ? RPStatus.Failed : RPStatus.Skipped }) .ToArrayAsync(token);
注意:需验证Convert.ToBase64String在SQL Server中能被EF Core正确翻译(实际测试中支持良好),确保生产环境与测试环境逻辑一致。
3. 调整实体类设计,让Hash属性支持值比较
方案3.1:使用自定义结构体封装byte[]
定义一个按值比较的结构体来替代原生byte[],让Hash属性成为值类型:
public readonly struct HashValue : IEquatable<HashValue> { private readonly byte[] _value; public HashValue(byte[] value) { _value = value ?? Array.Empty<byte>(); } public byte[] ToByteArray() => (byte[])_value.Clone(); public bool Equals(HashValue other) { return StructuralComparisons.StructuralEqualityComparer.Equals(_value, other._value); } public override bool Equals(object obj) { return obj is HashValue other && Equals(other); } public override int GetHashCode() { int hash = 17; foreach (byte b in _value) { hash = hash * 31 + b.GetHashCode(); } return hash; } public static bool operator ==(HashValue left, HashValue right) => left.Equals(right); public static bool operator !=(HashValue left, HashValue right) => !left.Equals(right); }
修改PRH类:
public class PRH { public long JobId { get; set; } public RPStatus Status { get; set; } public HashValue Hash { get; set; } }
EF Core可将该结构体映射为数据库中的byte[]列,同时内存数据库会按值比较结构体实例,分组逻辑自然生效。
方案3.2:重写PRH类的Equals和GetHashCode
通过重写实体类的相等性方法,让byte[]按值参与比较:
public class PRH { public long JobId { get; set; } public RPStatus Status { get; set; } public byte[] Hash { get; set; } = Array.Empty<byte>(); public override bool Equals(object obj) { if (obj is not PRH other) return false; return JobId == other.JobId && Status == other.Status && StructuralComparisons.StructuralEqualityComparer.Equals(Hash, other.Hash); } public override int GetHashCode() { int hash = JobId.GetHashCode() ^ Status.GetHashCode(); foreach (byte b in Hash) { hash = hash * 31 + b.GetHashCode(); } return hash; } }
此方法需配合方案1的自定义比较器使用,才能确保EF Core内存数据库在分组时使用该逻辑。
4. 测试中手动模拟分组逻辑
若不想修改生产代码或EF Core配置,可在测试中先获取所有数据,再用自定义比较器在内存中完成分组:
// 测试方法内获取原始数据 var allPrhs = await context.Set<PRH>().Where(x => x.JobId == jobId).ToListAsync(token); // 手动按值分组 var grouped = allPrhs.GroupBy(x => x.Hash, new ByteArrayEqualityComparer()) .Select(g => new HashRecord { Hash = g.Key, Status = g.Any(x => x.Status == RPStatus.Successful) ? RPStatus.Successful : g.Any(x => x.Status == RPStatus.Failed) ? RPStatus.Failed : RPStatus.Skipped }) .ToArray(); // 断言分组结果符合预期
需确保手动分组逻辑与生产环境EF Core生成的SQL逻辑完全一致,避免测试结果失真。
内容的提问来源于stack exchange,提问作者JaimeCamargo

