比较Linq查询中的byte []

时间:2014-10-13 17:15:57

标签: c# unit-testing mocking

我的SQL表中有一个二进制列,我使用以下C#代码成功查询了该表:

var hash = "http://www.whatever.com".ToSHA256HashBytes();
var landingPage = context.LandingPages.FirstOrDefault(lp => lp.UrlHash == hash);
  • 请注意:" ToSHA256HashBytes"是我写的一个扩展方法,它返回一个byte []

这很有用,因为SQL会比较byte []的内容并将记录与匹配的" UrlHash"返回。

然而,这在我的单元测试中不起作用,因为比较是在内存中执行的,比较byte []的规则显然是不同的。如果它们位于内存中的相同位置而不是通过比较数组的内容,C#似乎认为两个字节数组是相等的。

这意味着以下单元测试将失败

var data = new[]
{
    new LandingPage() { UrlHash = "http://www.whatever.com".ToSHA256HashBytes() },
    new LandingPage() { UrlHash = "http://mycompany.com/another/folder/page.php"".ToSHA256HashBytes() },
    new LandingPage() { UrlHash = "http://someothercompany.com/folder/somepage.html"".ToSHA256HashBytes() }
};
var mockData = new Mock<DbSet<T>>();
var queryableData = data.AsQueryable();
mockData.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryableData.Provider);
mockData.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryableData.Expression);
mockData.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryableData.ElementType);
mockData.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryableData.GetEnumerator());

var mockContext = new Mock<MyContext>();
mockContext.Setup(m => m.LandingPages).Returns(mockData.Object);

var hash = "http://www.whatever.com".ToSHA256HashBytes();
var landingPage = mockContext.Object.LandingPages.FirstOrDefault(lp => lp.UrlHash == hash);
Assert.IsNotNull(landingPage);

有没有办法可以编写Linq查询,以便在单元测试和查询数据库时都能正常工作?

我发现了一个非常similar question但是OP通过更改他的查询(很遗憾不是我的选项)解决了他的问题,而不是实际找到他原来问题的解决方案。

2 个答案:

答案 0 :(得分:2)

您可以使用Enumerable.SequenceEqual扩展名方法:

var landingPage = context.LandingPages
    .FirstOrDefault(lp => lp.UrlHash.SequenceEqual(hash));

当且仅当两个源序列长度相等且其对应元素根据其类型的默认相等比较器相等时,SequenceEqual的返回值为true

答案 1 :(得分:0)

好的,我花了一些时间,这可能会对你有所帮助。

首先我创建了自定义IQueryable实现(像适配器一样工作:简单地将调用转换为IQueryable - 实例(属性&#39; Origin&#39;),作为构造函数参数传递)。 唯一的区别是在执行之前会转换对CreateQueryExecute的调用。我们访问表达式树的每个节点,并用Equals(byteArray1, byteArray2)

替换Enumerable.SequenceEquals(byteArray1, byteArray2)的所有节点

首先,这是用法示例:

var data = new[]
{
    new LandingPage() { UrlHash = "http://www.whatever.com".ToSHA256HashBytes() },
    new LandingPage() { UrlHash = "http://mycompany.com/another/folder/page.php"".ToSHA256HashBytes() },
    new LandingPage() { UrlHash = "http://someothercompany.com/folder/somepage.html"".ToSHA256HashBytes() }
}

var binaryCompareQuery = data
      .AsQueryable()        // Get simple queryable  
      .WithBinaryCompare(); // Use SequentalEquals for byte arrays

实施

这是IQueryable适配器:

public class BinaryCompareQuery<T> : IQueryable<T>, IQueryProvider
{
    private EqualsReplacer Replacer { get; }
    private IQueryable<T> Origin { get; }

    public BinaryCompareQuery(IQueryable<T> origin)
    {
        Replacer = new EqualsReplacer();
    }

    #region IQueryable implementation

    public IEnumerator<T> GetEnumerator()
        => Origin.GetEnumerator();

    public IQueryProvider Provider
        => this;

    public Expression Expression
        => Origin.Expression;

    IEnumerator IEnumerable.GetEnumerator()
        => Origin.GetEnumerator();

    public Type ElementType
        => Origin.ElementType;

    #endregion

    #region IQueryProvider implementation

    IQueryable IQueryProvider.CreateQuery(Expression expression)
        => Origin.Provider.CreateQuery(Replacer.Visit(expression));

    IQueryable<TResult> IQueryProvider.CreateQuery<TResult>(Expression expression)
        => Origin.Provider.CreateQuery<TResult>(Replacer.Visit(expression));

    object IQueryProvider.Execute(Expression expression)
        => Origin.Provider.Execute(Replacer.Visit(expression));

    TResult IQueryProvider.Execute<TResult>(Expression expression)
        => Origin.Provider.Execute<TResult>(Replacer.Visit(expression));

    #endregion
}

表达式树访问者:将Equals替换为SequenceEquals

internal class EqualsReplacer : ExpressionVisitor
{
    // public static bool Enumerable.SequenceEqual<byte>(this IEnumerable<byte> first, IEnumerable<byte> second)
    private static readonly MethodInfo SequenceEqualMethod = typeof(Enumerable)
        .GetMethods(BindingFlags.Static | BindingFlags.Public)
        .Where(x => x.Name == "SequenceEqual")
        .First(x => x.GetParameters().Length == 2)
        .MakeGenericMethod(typeof(byte));

    protected override Expression VisitBinary(BinaryExpression node)
    {
        // Skip all nodes except 'Equal' nodes
        if (node.NodeType != ExpressionType.Equal)
            return base.VisitBinary(node);

        // Skip all 'Equal' nodes with arguments other than byte[]
        if (node.Left.Type != typeof(byte[]) || node.Right.Type != typeof(byte[]))
            return base.VisitBinary(node);

        // Apply rewrite for all inner nodes
        var left = Visit(node.Left);
        var right = Visit(node.Right);

        // Rewrite expression, changing Equals
        return Expression.Call(SequenceEqualMethod, left, right);
    }
}

Bonus:扩展方法,允许将我们的行为添加到任何IQueryable

public static class BinaryCompareQueryExtensions
{
    public static BinaryCompareQuery<T> WithBinaryCompare<T>(this IEnumerable<T> enumerable)
    {
        var queryable = (enumerable as IQueryable<T>) ?? enumerable.AsQueryable();

        return new BinaryCompareQuery<T>(queryable);
    }
}

全部;)