【问题标题】:Comparing byte[] in Linq query在 Linq 查询中比较 byte[]
【发布时间】:2014-10-13 17:15:56
【问题描述】:

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

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

这很有效,因为 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 通过更改他的查询(不幸的是这对我来说不是一个选项)解决了他的问题,而不是真正找到他原来问题的解决方案。

【问题讨论】:

    标签: c# unit-testing mocking


    【解决方案1】:

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

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

    SequenceEqual 的返回值是true 当且仅当两个源序列长度相等并且它们的对应元素根据它们的类型的默认相等比较器相等。

    【讨论】:

    • 不幸的是,这不会起作用。在我引用的另一个 SO 问题中提到了这个想法,并且如该问题中所述:如果我将此比较重写为 UrlHash.SequenceEqual(hash),那么我的单元测试将通过,但是当我与之交谈时出现异常SQL。 (System.NotSupportedException:不支持查询运算符“SequenceEqual”。)
    • @desautelsj 哦,我错过了。 SQL 使用什么类型来存储您的 byte[] UrlHash
    • SQL 列是 BINARY(32)
    • @desautelsj 我不认为您将能够使用 LINQ-to-SQL 并在服务器端进行 byte[] 比较,因为它只会执行某些功能在传递给 LINQ 方法的 Expression&lt;...&gt; 参数中识别。您链接到的问题的答案似乎为您仅使用 LINQ-to-SQL 的情况提供了解决方案。现在我更好地理解了您的问题,并且我认为不会有一段代码可以用于两种情况(模拟和 LINQ-to-SQL)。我能想到的唯一(不优雅的)解决方案是将哈希存储为字符串。
    • @desautelsj 一个更优雅的解决方案将涉及改进 LINQ-to-SQL 转换器以支持 Expression&lt;...&gt;s,其中包括对 Enumerable.SequenceEqual&lt;byte[]&gt;(a, b) 的调用,因为二进制比较是大多数 SQL 提供程序支持的操作,我相信。
    【解决方案2】:

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

    首先我创建了自定义IQueryable 实现(像适配器一样工作:简单地将调用转换为IQueryable-instance(属性'Origin'),作为构造函数参数传递)。 唯一的区别是对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);
        }
    }
    

    奖励:扩展方法,允许将我们的行为添加到任何 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);
        }
    }
    

    就是这样;)

    【讨论】:

    • 这听起来不错......但它不起作用:(我已经从 ctor 修复了 null ref 设置来源,但无论如何,没有任何东西进入 VisitBinary
    猜你喜欢
    • 2015-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-27
    • 1970-01-01
    • 2014-06-19
    • 2019-05-23
    • 1970-01-01
    相关资源
    最近更新 更多