You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

内存数据库中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 06:37:05