【问题标题】:Different results when using IQueryable and IEnumerable使用 IQueryable 和 IEnumerable 时的不同结果
【发布时间】:2021-03-26 13:22:06
【问题描述】:

我注意到使用 IEnumerable 和 IQueryable 时 DateTime 类型的比较结果不同。

var messages = _dbContext.Messages.ToList();
var test = messages.Any(m => m.CreatedOn == message.CreatedOn); // true - correct
var messages1 = _dbContext.Messages;
var test1 = messages1.Any(m => m.CreatedOn == message.CreatedOn); // false - not correct

我的模型有以下转变:

public DateTime CreatedOn { get => _createdOn.ToLocalTime(); set => _createdOn = value.ToUniversalTime(); }

我在比较 DateTime 类型。在查找调试器结果(m.CreatedOn 实体之一)时,来自 db 的结果在两种情况下都是相同的(相同的类型/值/种类/刻度等)。所以, message.CreatedOn 应该匹配。并且在查询 IEnumerable 时匹配,但在查询 IQueryable 时不匹配。对此有何解释?

是不是 IQueryable 在 db SQL 查询上运行?那么它就无法匹配 message.CreatedOn - 在我的情况下。

误导的是,VS degugger 中的查找从两个界面显示相同的结果。必须是,调试器使用模型中定义的转换。我说的对吗?

【问题讨论】:

    标签: c# .net-core entity-framework-core


    【解决方案1】:

    这清楚地表明拥有properties with side effects 是多么棘手。当然,设置属性是一种副作用,但从狭义上讲,它意味着:做的比预期的要多。 IE。违反最小意外原则

    在您的情况下,它会导致两种风格的 LINQ(对象和转换为 SQL)表现不同。 EF 对这种转换感到“惊讶”并且不处理它。关键是通过value conversion 与 EF 共享秘密。

    我有一个简单而无聊的类,它有两个属性,其中两个 DateTime 属性都映射到 Sql Server 中的 DateTime(2) 列:

    class Class1
    {
        DateTime _value;
        public int ID { get; set; }
        public DateTime Value
        { 
            get => _value.ToLocalTime(); 
            set => _value = value.ToUniversalTime();
        }
        public DateTime ConvValue { get ; set ; }
    }
    

    我希望ConvValue 显示当地时间,就像Value 一样,但现在循环中有 EF。所以我添加了这个配置:

    class Class1Config : IEntityTypeConfiguration<Class1>
    {
        public void Configure(EntityTypeBuilder<Class1> builder)
        {
            builder.Property(c => c.ConvValue)
                .HasConversion(v => v.ToUniversalTime(), v => v.ToLocalTime());
                                    // ^^ to database            ^^ from database
        }
    }
    

    然后我将此内容添加到数据库中(注意毫秒和00 小时):

    ID Value ConvValue
    1 2020-01-01 00:01:01.0010000 2020-01-01 00:01:01.0010000
    2 2020-01-01 00:01:01.0020000 2020-01-01 00:01:01.0020000
    3 2020-01-01 00:01:01.0030000 2020-01-01 00:01:01.0030000

    我通过执行这些查询(在 Linqpad 中)解决了您的问题:

    DateTime refd;
    using (var db = getContext())
    {
        refd = db.Classes.First().Value;
    }
    using (var db = getContext())
    {
        db.Classes.Count(c => c.Value == refd).Dump();                 // 0
        db.Classes.AsEnumerable().Count(c => c.Value == refd).Dump();  // 1
    }
    

    0 是因为第一个 Count 查询在我的时区运行此 SQL 代码:

    exec sp_executesql N'SELECT COUNT(*)
    FROM [Classes] AS [c]
    WHERE [c].[Value] = @__refd_0',N'@__refd_0 datetime2(7)',
        @__refd_0='2020-01-01 01:01:01.0010000'
    

    如您所见,参数是我的本地时间 (+ 1h),因为这是第一个查询生成它的方式(refd2020-01-01 01:01:01.001)。 EF 直接从它获得的模型信息中转换查询。

    基于ConvValue 的相同查询给出预期结果:

    db.Classes.Count(c => c.ConvValue == refd).Dump();                 // 1
    db.Classes.AsEnumerable().Count(c => c.ConvValue == refd).Dump();  // 1
    

    现在 EF 知道转换并将参数转换为 UTC,然后再将其转换为 SQL(00 小时):

    exec sp_executesql N'SELECT COUNT(*)
    FROM [Classes] AS [c]
    WHERE [c].[ConvValue] = @__refd_0',N'@__refd_0 datetime2(7)',
        @__refd_0='2020-01-01 00:01:01.0010000'
    

    【讨论】:

    • 顺便说一句,我不是这种转变的粉丝。我宁愿有一个像LocalValue 这样的未映射的getter-only 属性,或者在视图模型中进行转换,并且按照惯例只设置UTC 值。 DateTime 转换很容易出错。
    【解决方案2】:

    当您使用.ToList() 时,您的查询将在数据库上执行,因此后面的.Any(...) 将只检查已加载到内存中的属性。

    在您的第二个示例中,.Any(...) 扩展了 IQueryable,并且还将被转换为发送到数据库的 SQL,并且数据库实际上将根据列数据进行 CreatedOn 比较。

    您始终可以使用 SQL Server Profiler(包含在 SSMS 中)来捕获进入数据库的实际查询,它通常会提供一些额外的洞察力。

    顺便说一句,数据库对于日期时间字段的精度可能与 C# 不同,因此会产生不同的结果。

    【讨论】:

      猜你喜欢
      • 2021-09-04
      • 1970-01-01
      • 1970-01-01
      • 2020-04-28
      • 1970-01-01
      • 2011-06-09
      • 2015-07-30
      • 2014-07-14
      • 1970-01-01
      相关资源
      最近更新 更多