DevArt MySql EF6 驱动程序:可空 BIT(1) 列的空值行在 'COLUMN_NAME ?? false' 中始终评估为真

DevArt MySql EF6 Drivers: Nullable BIT(1) columns have their null-value rows always evaluated to true for 'COLUMN_NAME ?? false'

雇佣的图书馆员:

  Devart.Data.dll => 5.0.1878.0
  Devart.Data.MySql.dll => 8.10.1086.0
  Devart.Data.MySql.Design.dll => 8.10.1086.0
  Devart.Data.MySql.Entity.EF6.dll => 8.10.1086.0
  EntityFramework => 6.2.0

我们的目标是来自 ASP.NET MVC5 项目的 .net4.7.1。我们的存储库构建在 EF .edmx(数据库优先方法)之上。我们的查询如下所示:

  var results = _db.NB_FILTERS
        .Select(x => new ReportFiltersDTO { IsHiddenSubFilter = x.NFS_SUBFILTER_YN ?? false })
        .ToList();

NFS_SUBFILTER_YN 在我们的 .edmx 中被声明为布尔值:

  <Property Name="NFS_SUBFILTER_YN" Type="boolean" />

NB_FILTERS ddl 是这样的:

  CREATE TABLE NB_FILTERS (
        [...]
        NFS_SUBFILTER_YN BIT(1) NULL,
        [...]
  )

在这种情况下,如果某些行为空,则给定的 linq 表达式 returns 'true' 代替 'false'。罪魁祸首似乎在自动生成的 sql:

  actual => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 0 ELSE Extent1.NFS_SUBFILTER_YN END

  corrected => CASE WHEN Extent1.NFS_SUBFILTER_YN IS NULL THEN 1 ELSE CAST(Extent1.NFS_SUBFILTER_YN AS SIGNED) END

如何在不更改基础表的情况下解决此错误and/or linq 语句本身,直到 Devart 推出实际修复程序?

旁注:

作为临时 stop-gap 解决方案,我们选择在 mysql 上使用 EF6 拦截器来检测和更正针对布尔列的 on-the-fly sql 语句(幸好大多数如果不是我们所有的布尔列都有 _YN 后缀,这有很大帮助):

using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Text.RegularExpressions;

namespace Some.Namespace
{
    //to future maintainers     the devart mysql drivers for ef6 ver8.10.1086.0 suffer from what appears to be a very annoying b.ug which cause nullable bit(1) columns set to null
    //to future maintainers     to always be materialized to true when we evaluate them like so
    //to future maintainers
    //to future maintainers                                                  x.SOME_BOOLEAN_COLUMN ?? false
    //to future maintainers
    //to future maintainers     this is obviously flat out wrong and to remedy this issue we intercept the offending sql snippets and hotfix them like so
    //to future maintainers    
    //to future maintainers                before => CASE WHEN Extent1.NFS_SUBFILTER_YN    IS NULL THEN 1 ELSE             Extent1.NFS_SUBFILTER_YN            END
    //to future maintainers                after  => CASE WHEN Extent1.NFS_SUBFILTER_YN    IS NULL THEN 1 ELSE     CAST(Extent1.NFS_SUBFILTER_YN AS SIGNED)    END
    //to future maintainers
    public sealed class MySqlHotFixerCommandInterceptor : IDbCommandInterceptor
    {
        //beforeexecution
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => LogWarningBeforeExecutionIfSynchronousExecutionIsFound(command, interceptionContext);

        //afterexecution
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) {}
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) {}
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) {}

        static private void LogWarningBeforeExecutionIfSynchronousExecutionIsFound<TResult>(IDbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
        {
            if (interceptionContext.Exception != null)
                return;

            command.CommandText = FaultySwitchCaseSpotter1.Replace(command.CommandText, "CAST( AS SIGNED)");
            command.CommandText = FaultySwitchCaseSpotter2.Replace(command.CommandText, "CAST( AS SIGNED)");
        }

        private static readonly Regex FaultySwitchCaseSpotter1 = new Regex(@"(?<=CASE\s+WHEN\s+(`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?`?\s+IS\s+NULL\s+THEN\s+(0|1)\s+ELSE\s+)((`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?`?)(?=\s*END)", RegexOptions.IgnoreCase);
        private static readonly Regex FaultySwitchCaseSpotter2 = new Regex(@"(?<=CASE\s+WHEN\s+(`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?_YN`?\s+IS\s+NULL\s+THEN\s+(\d+|.[a-zA-Z0-9_]+?|`?[a-zA-Z0-9_]+?`?[.]`?[a-zA-Z0-9_]+?`?)\s+ELSE\s+)((`?[a-zA-Z0-9_]+?`?[.])?`?[a-zA-Z0-9_]+?_YN`?)(?=\s*END)", RegexOptions.IgnoreCase);
    }
}

web.config/app.config 也需要像这样调整:

<configuration>

  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
       [...]
    </defaultConnectionFactory>

    <providers>
       [...]
    </providers>

    <interceptors>
      <!-- keep first -->
      <interceptor type="Some.Namespace.MySqlHotFixerCommandInterceptor, Organotiki.Infrastructure.Core">
      </interceptor>
      [...]
    </interceptors>
  </entityFramework>
</configuration>