ef6: Fix Entity Framework so it does not "assume" the SQL datatype is DateTime2 when using SQL 2008 and greater

The EF code assumes that all .NET DateTime properties should be cast as DateTime2 SQL data types when the SQL version is 2008 and greater.

Please adjust EF so that it “assumes” DateTime (this will not break current code since DateTime and DateTime2 are interchangeable up until SQL 2016 https://docs.microsoft.com/en-us/sql/database-engine/breaking-changes-to-database-engine-features-in-sql-server-2016?view=sql-server-2017) and then let the caller add an attribute to the .NET DateTime property to tell EF wither to use DateTime or DateTime2 data type.

Something like…

[Column(TypeName ="datetime2")]
public DateTime TestDate { get; set; }

[Column(TypeName ="datetime")] // Not needed by default
public DateTime AnotherTestDate { get; set; }

Here are the places in the EF code that needs “fixed”: All code is in the “EntityFramework.SqlServr” .NET project.

  1. class SqlFunctionCallHandler
// <summary>
// See <see cref="HandleCanonicalFunctionDateTimeTypeCreation" /> for exact translation
// Pre Katmai creates datetime.
// On Katmai creates datetime2.
// </summary>
private static ISqlFragment HandleCanonicalFunctionCreateDateTime(SqlGenerator sqlgen, DbFunctionExpression e)
{            
    var typeName = (sqlgen.IsPreKatmai) ? "datetime" : "datetime2";
    return HandleCanonicalFunctionDateTimeTypeCreation(sqlgen, typeName, e.Arguments, true, false);
}

// <summary>
// TruncateTime(DateTime X)
// PreKatmai:    TRUNCATETIME(X) => CONVERT(DATETIME, CONVERT(VARCHAR(255), expression, 102),  102)
// Katmai:    TRUNCATETIME(X) => CONVERT(DATETIME2, CONVERT(VARCHAR(255), expression, 102),  102)
// TruncateTime(DateTimeOffset X)
// TRUNCATETIME(X) => CONVERT(datetimeoffset, CONVERT(VARCHAR(255), expression,  102)
// + ' 00:00:00 ' +  Right(convert(varchar(255), @arg, 121), 6),  102)
// </summary>
private static ISqlFragment HandleCanonicalFunctionTruncateTime(SqlGenerator sqlgen, DbFunctionExpression e)
{
    //The type that we need to return is based on the argument type.
    string typeName = null;
    var isDateTimeOffset = false;

    var typeKind = e.Arguments[0].ResultType.GetPrimitiveTypeKind();

    if (typeKind == PrimitiveTypeKind.DateTime)
    {
        typeName = sqlgen.IsPreKatmai ? "datetime" : "datetime2";
    }
    else if (typeKind == PrimitiveTypeKind.DateTimeOffset)
    {
        typeName = "datetimeoffset";
        isDateTimeOffset = true;
    }
    else
    {
        Debug.Assert(true, "Unexpected type to TruncateTime" + typeKind.ToString());
    }

    var result = new SqlBuilder();
    result.Append("convert (");
    result.Append(typeName);
    result.Append(", convert(varchar(255), ");
    result.Append(e.Arguments[0].Accept(sqlgen));
    result.Append(", 102) ");

    if (isDateTimeOffset)
    {
        result.Append("+ ' 00:00:00 ' +  Right(convert(varchar(255), ");
        result.Append(e.Arguments[0].Accept(sqlgen));
        result.Append(", 121), 6)  ");
    }

    result.Append(",  102)");
    return result;
}
  1. class SqlGenerator
// <summary>
// Generate tsql for a constant. Avoid the explicit cast (if possible) when
// the isCastOptional parameter is set
// </summary>
// <param name="e"> the constant expression </param>
// <param name="isCastOptional"> can we avoid the CAST </param>
// <returns> the tsql fragment </returns>
private ISqlFragment VisitConstant(DbConstantExpression e, bool isCastOptional)
{
    // Constants will be sent to the store as part of the generated TSQL, not as parameters
    var result = new SqlBuilder();

    var resultType = e.ResultType;
    // Model Types can be (at the time of this implementation):
    //      Binary, Boolean, Byte, Date, DateTime, DateTimeOffset, Decimal, Double, Guid, Int16, Int32, Int64, Single, String, Time
    if (resultType.IsPrimitiveType())
    {
        var typeKind = resultType.GetPrimitiveTypeKind();
        switch (typeKind)
        {
            case PrimitiveTypeKind.Int32:
                // default sql server type for integral values.
                result.Append(e.Value.ToString());
                break;

            case PrimitiveTypeKind.Binary:
                result.Append(" 0x");
                result.Append(ByteArrayToBinaryString((Byte[])e.Value));
                result.Append(" ");
                break;

            case PrimitiveTypeKind.Boolean:
                // Bugs 450277, 430294: Need to preserve the boolean type-ness of
                // this value for round-trippability
                WrapWithCastIfNeeded(!isCastOptional, (bool)e.Value ? "1" : "0", "bit", result);
                break;

            case PrimitiveTypeKind.Byte:
                WrapWithCastIfNeeded(!isCastOptional, e.Value.ToString(), "tinyint", result);
                break;

            case PrimitiveTypeKind.DateTime:
                result.Append("convert(");
                result.Append(IsPreKatmai ? "datetime" : "datetime2");
                result.Append(", ");
                result.Append(
                    EscapeSingleQuote(
                        ((DateTime)e.Value).ToString(
                            IsPreKatmai ? "yyyy-MM-dd HH:mm:ss.fff" : "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture),
                        false /* IsUnicode */));
                result.Append(", 121)");
                break;

            case PrimitiveTypeKind.Time:
                AssertKatmaiOrNewer(typeKind);
                result.Append("convert(");
                result.Append(e.ResultType.EdmType.Name);
                result.Append(", ");
                result.Append(EscapeSingleQuote(e.Value.ToString(), false /* IsUnicode */));
                result.Append(", 121)");
                break;

            case PrimitiveTypeKind.DateTimeOffset:
                AssertKatmaiOrNewer(typeKind);
                result.Append("convert(");
                result.Append(e.ResultType.EdmType.Name);
                result.Append(", ");
                result.Append(
                    EscapeSingleQuote(
                        ((DateTimeOffset)e.Value).ToString("yyyy-MM-dd HH:mm:ss.fffffff zzz", CultureInfo.InvariantCulture), false
                    /* IsUnicode */));
                result.Append(", 121)");
                break;

            case PrimitiveTypeKind.Decimal:
                var strDecimal = ((Decimal)e.Value).ToString(CultureInfo.InvariantCulture);
                // if the decimal value has no decimal part, cast as decimal to preserve type
                // if the number has precision > int64 max precision, it will be handled as decimal by sql server
                // and does not need cast. if precision is lest then 20, then cast using Max(literal precision, sql default precision)
                var needsCast = -1 == strDecimal.IndexOf('.') && (strDecimal.TrimStart(new[] { '-' }).Length < 20);

                var precision = Math.Max((Byte)strDecimal.Length, DefaultDecimalPrecision);
                Debug.Assert(precision > 0, "Precision must be greater than zero");

                var decimalType = "decimal(" + precision.ToString(CultureInfo.InvariantCulture) + ")";

                WrapWithCastIfNeeded(needsCast, strDecimal, decimalType, result);
                break;

            case PrimitiveTypeKind.Double:
                {
                    var doubleValue = (Double)e.Value;
                    AssertValidDouble(doubleValue);
                    WrapWithCastIfNeeded(true, doubleValue.ToString("R", CultureInfo.InvariantCulture), "float(53)", result);
                }
                break;

            case PrimitiveTypeKind.Geography:
                AppendSpatialConstant(result, ((DbGeography)e.Value).AsSpatialValue());
                break;

            case PrimitiveTypeKind.Geometry:
                AppendSpatialConstant(result, ((DbGeometry)e.Value).AsSpatialValue());
                break;

            case PrimitiveTypeKind.Guid:
                WrapWithCastIfNeeded(true, EscapeSingleQuote(e.Value.ToString(), false /* IsUnicode */), "uniqueidentifier", result);
                break;

            case PrimitiveTypeKind.Int16:
                WrapWithCastIfNeeded(!isCastOptional, e.Value.ToString(), "smallint", result);
                break;

            case PrimitiveTypeKind.Int64:
                WrapWithCastIfNeeded(!isCastOptional, e.Value.ToString(), "bigint", result);
                break;

            case PrimitiveTypeKind.Single:
                {
                    var singleValue = (float)e.Value;
                    AssertValidSingle(singleValue);
                    WrapWithCastIfNeeded(true, singleValue.ToString("R", CultureInfo.InvariantCulture), "real", result);
                }
                break;

            case PrimitiveTypeKind.String:
                bool isUnicode;

                if (!e.ResultType.TryGetIsUnicode(out isUnicode))
                {
                    // If the unicode facet is not specified, if needed force non-unicode, otherwise default to unicode.
                    isUnicode = !_forceNonUnicode;
                }
                result.Append(EscapeSingleQuote(e.Value as string, isUnicode));
                break;

            default:
                // all known scalar types should been handled already.
                throw new NotSupportedException(
                    Strings.NoStoreTypeForEdmType(resultType.EdmType.Name, ((PrimitiveType)(resultType.EdmType)).PrimitiveTypeKind));
        }
    }
    else
    {
        throw new NotSupportedException();
        //if/when Enum types are supported, then handle appropriately, for now is not a valid type for constants.
        //result.Append(e.Value.ToString());
    }

    return result;
}

internal static string GenerateSqlForStoreType(SqlVersion sqlVersion, TypeUsage storeTypeUsage)
{
    Debug.Assert(BuiltInTypeKind.PrimitiveType == storeTypeUsage.EdmType.BuiltInTypeKind, "Type must be primitive type");

    var typeName = storeTypeUsage.EdmType.Name;
    var hasFacet = false;
    var maxLength = 0;
    byte decimalPrecision = 0;
    byte decimalScale = 0;

    var primitiveTypeKind = ((PrimitiveType)storeTypeUsage.EdmType).PrimitiveTypeKind;

    switch (primitiveTypeKind)
    {
        case PrimitiveTypeKind.Binary:
            if (!storeTypeUsage.MustFacetBeConstant(DbProviderManifest.MaxLengthFacetName))
            {
                hasFacet = storeTypeUsage.TryGetMaxLength(out maxLength);
                Debug.Assert(hasFacet, "Binary type did not have MaxLength facet");
                typeName = typeName + "(" + maxLength.ToString(CultureInfo.InvariantCulture) + ")";
            }
            break;

        case PrimitiveTypeKind.String:
            if (!storeTypeUsage.MustFacetBeConstant(DbProviderManifest.MaxLengthFacetName))
            {
                hasFacet = storeTypeUsage.TryGetMaxLength(out maxLength);
                Debug.Assert(hasFacet, "String type did not have MaxLength facet");
                typeName = typeName + "(" + maxLength.ToString(CultureInfo.InvariantCulture) + ")";
            }
            break;

        case PrimitiveTypeKind.DateTime:
            typeName = SqlVersionUtils.IsPreKatmai(sqlVersion) ? "datetime" : "datetime2";
            break;
        case PrimitiveTypeKind.Time:
            AssertKatmaiOrNewer(sqlVersion, primitiveTypeKind);
            typeName = "time";
            break;
        case PrimitiveTypeKind.DateTimeOffset:
            AssertKatmaiOrNewer(sqlVersion, primitiveTypeKind);
            typeName = "datetimeoffset";
            break;

        case PrimitiveTypeKind.Decimal:
            if (!storeTypeUsage.MustFacetBeConstant(DbProviderManifest.PrecisionFacetName))
            {
                hasFacet = storeTypeUsage.TryGetPrecision(out decimalPrecision);
                Debug.Assert(hasFacet, "decimal must have precision facet");
                Debug.Assert(decimalPrecision > 0, "decimal precision must be greater than zero");
                hasFacet = storeTypeUsage.TryGetScale(out decimalScale);
                Debug.Assert(hasFacet, "decimal must have scale facet");
                Debug.Assert(decimalPrecision >= decimalScale, "decimalPrecision must be greater or equal to decimalScale");
                typeName = typeName + "(" + decimalPrecision + "," + decimalScale + ")";
            }
            break;

        default:
            break;
    }

    return typeName;
}
  1. class SqlProviderServices
// <summary>
// Determines SqlDbType for the given primitive type. Extracts facet
// information as well.
// </summary>
private static SqlDbType GetSqlDbType(
    TypeUsage type, bool isOutParam, SqlVersion version, out int? size, out byte? precision, out byte? scale, out string udtName)
{
    // only supported for primitive type
    var primitiveTypeKind = ((PrimitiveType)type.EdmType).PrimitiveTypeKind;

    size = default(int?);
    precision = default(byte?);
    scale = default(byte?);
    udtName = default(string);

    // CONSIDER(CMeek):: add logic for Xml here
    switch (primitiveTypeKind)
    {
        case PrimitiveTypeKind.Binary:
            // for output parameters, ensure there is space...
            size = GetParameterSize(type, isOutParam);
            return GetBinaryDbType(type);

        case PrimitiveTypeKind.Boolean:
            return SqlDbType.Bit;

        case PrimitiveTypeKind.Byte:
            return SqlDbType.TinyInt;

        case PrimitiveTypeKind.Time:
            if (!SqlVersionUtils.IsPreKatmai(version))
            {
                precision = GetKatmaiDateTimePrecision(type, isOutParam);
            }
            return SqlDbType.Time;

        case PrimitiveTypeKind.DateTimeOffset:
            if (!SqlVersionUtils.IsPreKatmai(version))
            {
                precision = GetKatmaiDateTimePrecision(type, isOutParam);
            }
            return SqlDbType.DateTimeOffset;

        case PrimitiveTypeKind.DateTime:
            //For katmai pick the type with max precision which is datetime2
            if (!SqlVersionUtils.IsPreKatmai(version))
            {
                precision = GetKatmaiDateTimePrecision(type, isOutParam);
                return SqlDbType.DateTime2;
            }
            else
            {
                return SqlDbType.DateTime;
            }

        case PrimitiveTypeKind.Decimal:
            precision = GetParameterPrecision(type, null);
            scale = GetScale(type);
            return SqlDbType.Decimal;

        case PrimitiveTypeKind.Double:
            return SqlDbType.Float;

        case PrimitiveTypeKind.Geography:
            {
                udtName = "geography";
                return SqlDbType.Udt;
            }

        case PrimitiveTypeKind.Geometry:
            {
                udtName = "geometry";
                return SqlDbType.Udt;
            }

        case PrimitiveTypeKind.Guid:
            return SqlDbType.UniqueIdentifier;

        case PrimitiveTypeKind.Int16:
            return SqlDbType.SmallInt;

        case PrimitiveTypeKind.Int32:
            return SqlDbType.Int;

        case PrimitiveTypeKind.Int64:
            return SqlDbType.BigInt;

        case PrimitiveTypeKind.SByte:
            return SqlDbType.SmallInt;

        case PrimitiveTypeKind.Single:
            return SqlDbType.Real;

        case PrimitiveTypeKind.String:
            size = GetParameterSize(type, isOutParam);
            return GetStringDbType(type);

        default:
            Debug.Fail("unknown PrimitiveTypeKind " + primitiveTypeKind);
            return SqlDbType.Variant;
    }
}

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 6
  • Comments: 31 (4 by maintainers)

Commits related to this issue

Most upvoted comments

Similar posts here but it is marked as “closed / won’t fix”. https://github.com/aspnet/EntityFramework6/issues/49 https://github.com/aspnet/EntityFramework6/issues/325

Can this be looked at again since this is a “breaking” change in SQL 2016?

There is also another problem with code generated from EF since it creates “DateTime” columns for tables yet compares as “DateTime2” data types.

I also faced with the issue and spent a lot of time on it. For fix that on application data access layer you can use command interceptor:

    /// <summary>
    /// DateTimeInterceptor fixes the incorrect behavior of Entity Framework library when for datetime columns it's generating datetime2(7) parameters 
    /// when using SQL Server 2016 and greater.
    /// Because of that, there were date comparison issues.
    /// Links:
    /// https://github.com/aspnet/EntityFramework6/issues/49
    /// https://github.com/aspnet/EntityFramework6/issues/578
    /// Notes:
    /// Disable it if:
    /// 1) Database DateTime types will be migrating to DateTime2
    /// 2) Entity Framework team will fix the problem in a future version
    /// </summary>
    public class DateTimeInterceptor : IDbCommandInterceptor
    {
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            ChangeDateTime2ToDateTime(command);
        }

        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }

        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        private static void ChangeDateTime2ToDateTime(DbCommand command)
        {
            command.Parameters
                .OfType<SqlParameter>()
                .Where(p => p.SqlDbType == SqlDbType.DateTime2)
                .Where(p => p.Value != DBNull.Value)
                .Where(p => p.Value is DateTime)
                .Where(p => p.Value as DateTime? != DateTime.MinValue)
                .ToList()
                .ForEach(p => p.SqlDbType = SqlDbType.DateTime);
        }
    }

Don’t use it if your database has DateTime2 columns too.

Hope this will help someone 😃

The problem is, that EF6 does not work consistantly with datetime / datetime2(7) fields. Ok, we have datetime in database, that one is true. But if EF6 writes (SaveChanges) as datetime2 it should also read the column as datetime2. If EF6 would be consistant, everything would be ok…

Examlpe: exec sp_executesql N'UPDATE [dbo].[setup] SET [modified] = @0, [text] = @1 WHERE (([mykey] = @2) AND ([modified] = @3)) ',N'@0 datetime2(7),@1 varchar(max) ,@2 char(2),@3 datetime2(7)' ,@0='2021-06-09 12:54:46.7531175',@1='20000',@2='02',@3='2021-06-09 11:30:33.1670000'

Parameter @3 is wrong.

select modified, convert(datetime2(7),modified) as dt2 from dbo.setup where mykey='02'

datetime column in DB = 2021-06-09 11:30:33.167, converted to datetime2(7) in DB = 2021-06-09 11:30:33.1666667

in debug I can see that the DateTime object(modified) in Class (setup) has property TimeOfDay = 2021-06-09 11:30:33.1670000. So… If EF6 asumes all DateTime objects(columns) are of type “datetime2” it should also read the column as such. There is no way (as far as I know there is no way how to intercept SaveChanges and adjust value or column (ie.: convert(datetime2(7),modified = @3 ). The only solution is to change compatibility level down to 120 or to change the datetime to datetime2 in database. The second one is dangerous if we other code where we have datetime column (temporary tables, stored procedures etc…)

@adolfo1981

  1. While it would be nice to default to DateTime, I think that by default the EF team will need to use DateTime2 as the default attribute decorate when nothing is supplied since that is already in production and all code base. The developers using EF will need to apply the DateTime attribute when the don’t want it to default to DateTime2.

  2. I think you miss-understood by comment. I was saying that if you are not using DateTime2 in SQL you can download the source code from github, change all the places I mentioned at the top of the post from datetime2 to datetime, recompile, and it will work with SQL 2016.

  3. I have no fix, only a suggestion to the EF team.

Currently I recompiled the code base for DateTime and am using it in our production system with SQL 2016. We have developers at our company actively working on converting our code from EF to EF Core since it does not have this problem.

@ImGonaRot I’ll take another look.

@ajcvickers Sure it could. Any place where it switches to DateTime2, check the .NET property attribute and (If and Only If) the user added “TypeName=DateTime” then let EF use DateTime rather than DateTime2.

Ex.

if (typeKind == PrimitiveTypeKind.DateTime)
    {
        // Check the "TypeName" attribute for DateTime
        // property.GetCustomAttributes(typeof(Column))... more code here
        if (typeNameAttribute == "DateTime")
        {
            typeName = "datetime";
        }
        else
        {
            typeName = sqlgen.IsPreKatmai ? "datetime" : "datetime2";
        }
    }

I would suggest caching the attributes so that we don’t have to use reflection every time since it is slow.

@dgenezini No. The attribute would be on the DbContext type, not the individual properties. In the interceptor we have no information about what entity property the command parameters are bound to, so the we can’t configure on a property-by-property basis. If we could, that would be a better solution.

So you had to decorate all your applicable DateTime properties with the ChangeDateTime2ToDateTimeAttribute? Oh or you put that attribute at the DbContext level instead?

I’m decorating the DbContexts, in my case all DateTime properties of the database will be DATETIME.

@dgenezini Did you try the Interceptor solution from @stasones ? That sounds promising to me and worked in my prototype.

@adolfo1981 , yes, just made a little change to check for a Attribute so I can have both scenarios simultaneously.

    public class DateTimeInterceptor : IDbCommandInterceptor
    {
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            var DbContext = interceptionContext.DbContexts.FirstOrDefault();

            if ((DbContext != null) &&
                (Attribute.IsDefined(DbContext.GetType(), typeof(ChangeDateTime2ToDateTimeAttribute))))
            {
                ChangeDateTime2ToDateTime(command);
            }
        }

        ...
    }

Still, I believe that the [Column(TypeName =“datetime”)] attribute should be considered by entity framework.

Just ran into this issue by updating the compatibility of SQLServer to 130. How is there not a fix with entity framework and this yet?

Following @stasones lead, I have a more sophisticated version that asks SQL Server to suggest the parameter type mapping, and overrides EFs type mapping where the correct parameter type should be DateTime, DateTime2, or Date.

It uses the sp_describe_undeclared_parameters to get the parameter types, and assembles a cache of parameter type recommendations to reuse for subsequent executions of the same query text.

Listing Follows:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Interception;
using System.Data.SqlClient;
using System.Linq;

namespace ef6DatetimeWorkaround
{
    class Foo
    {
        public int Id { get; set; }

        [Column(TypeName = "datetime")]
        public DateTime? DateTimeColumn { get; set; }

        [Column(TypeName = "datetime2")]
        public DateTime? DateTime2Column { get; set; }

        [Column(TypeName = "date")]
        public DateTime? DateColumn { get; set; }

    }

    public class DbConfig : DbConfiguration
    {
        public DbConfig()
        {
            this.SetDatabaseInitializer(new DropCreateDatabaseAlways<Db>());
            this.AddInterceptor(new DateTimeParameterFixer());
        }
    }

    [DbConfigurationType(typeof(DbConfig))]
    class Db : DbContext
    {
        public DbSet<Foo> Foos { get; set; }
    }
    class Program
    {

        static void Main(string[] args)
        {
            DateTime? now = DateTime.Now;

            using (var db = new Db())
            {
                db.Database.Initialize(true);
                var f = new Foo()
                {
                    DateColumn = now,
                    DateTime2Column = now,
                    DateTimeColumn = now
                };
                db.Foos.Add(f);
                db.SaveChanges();
            }
            using (var db = new Db())
            {
                db.Configuration.UseDatabaseNullSemantics = false;
                db.Database.Log = s => Console.WriteLine(s);

                var q = from f in db.Foos
                        where f.DateColumn == now
                        where f.DateTime2Column == now
                        where f.DateColumn == now
                        select f;

                var results = q.Single();

                Console.WriteLine("Success");


            }

        }
    }


    public class DateTimeParameterFixer : IDbCommandInterceptor
    {
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            var dbContexts = interceptionContext.DbContexts.ToList();

            if (dbContexts.Count == 1)
            {
                FixDatetimeParameters(dbContexts[0], command);
            }

        }

        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }

        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        private class SuggestedParameterType
        {
            public int parameter_ordinal { get; set; }
            public string name { get; set; }
            public int suggested_system_type_id { get; set; }
            public string suggested_system_type_name { get; set; }
            public Int16 suggested_max_length { get; set; }
            public byte suggested_precision { get; set; }
            public byte suggested_scale { get; set; }

        }

        private static ConcurrentDictionary<string, List<SuggestedParameterType>> batchCache = new ConcurrentDictionary<string, List<SuggestedParameterType>>();

        enum SqlTypeId
        {
            date = 40,
            datetime = 61,
            datetime2 = 42,
        }

        private static List<SuggestedParameterType> GetSuggestedParameterTypes(DbContext db, string batch, int parameterCount)
        {
            if (parameterCount == 0)
            {
                return new List<SuggestedParameterType>();
            }
            var con = (SqlConnection)db.Database.Connection;
            var conState = con.State;

            if (conState != ConnectionState.Open)
            {
                db.Database.Connection.Open();
            }
            var results = batchCache.GetOrAdd(batch, (sqlBatch) =>
            {
                var pBatch = new SqlParameter("@batch", SqlDbType.NVarChar, -1);
                pBatch.Value = batch;

                var rd = new List<SuggestedParameterType>();
                var cmd = new SqlCommand("exec sp_describe_undeclared_parameters @batch; ", con);
                cmd.Transaction = (SqlTransaction)db.Database.CurrentTransaction?.UnderlyingTransaction;

                cmd.Parameters.Add(pBatch);

                //sp_describe_undeclared_parameters does not support batches that contain multiple instances of the same parameter
                //
                //to workaround a common cause loop and transform on error expressions like:
                //
                // WHERE ([Extent1].[Date_Modified] = @p__linq__0) OR (([Extent1].[Date_Modified] IS NULL) AND (@p__linq__0 IS NULL))'
                //into
                // WHERE ([Extent1].[Date_Modified] = @p__linq__0) OR (([Extent1].[Date_Modified] IS NULL) AND (1=1))'
                // 
                // this works because the @param is null expression is irrelevant to the parameter type discovery.
                while (true)
                {
                    try
                    {
                        using (var rdr = cmd.ExecuteReader())
                        {
                            while (rdr.Read())
                            {
                                var sp = new SuggestedParameterType()
                                {
                                    //https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-describe-undeclared-parameters-transact-sql
                                    parameter_ordinal = rdr.GetInt32(0),
                                    name = rdr.GetString(1),
                                    suggested_system_type_id = rdr.GetInt32(2),
                                    suggested_system_type_name = rdr.GetString(3),
                                    suggested_max_length = rdr.GetInt16(4),
                                    suggested_precision = rdr.GetByte(5),
                                    suggested_scale = rdr.GetByte(6),
                                };


                                if (sp.suggested_system_type_id == (int)SqlTypeId.date || sp.suggested_system_type_id == (int)SqlTypeId.datetime2 || sp.suggested_system_type_id == (int)SqlTypeId.datetime)
                                {
                                    if (!sp.name.EndsWith("IgNoRe"))
                                    {
                                      rd.Add(sp);
                                    }
                                }
                            }
                            break;
                        }
                    }
                    catch (SqlException ex) when (ex.Errors[0].Number == 11508)
                    {
                        //Msg 11508, Level 16, State 1, Line 14
                        //The undeclared parameter '@p__linq__0' is used more than once in the batch being analyzed.
                        var paramName = System.Text.RegularExpressions.Regex.Match(ex.Errors[0].Message, "The undeclared parameter '(?<paramName>.*)' is used more than once in the batch being analyzed.").Groups["paramName"].Value;

                        string sql = (string)pBatch.Value;
                        if (sql.Contains($"{paramName} IS NULL"))
                        {
                            sql = sql.Replace($"{paramName} IS NULL", "1=1");
                            pBatch.Value = sql;
                            continue;
                        }
                        else
                        {
                            throw;
                        }
                       
                    }
                
                }
                return rd;


            });

            if (conState == ConnectionState.Closed)
            {
                con.Close();
            }
            return results;

        }

        private static void FixDatetimeParameters(DbContext db, DbCommand command)
        {
            if (!command.Parameters.OfType<SqlParameter>().Any(p => p.SqlDbType == SqlDbType.DateTime2 || p.SqlDbType == SqlDbType.DateTime))
            {
                return;
            }
            var suggestions = GetSuggestedParameterTypes(db, command.CommandText, command.Parameters.Count);

            if (suggestions.Count == 0)
            {
                return;
            }

            Dictionary<string, SqlParameter> paramLookup = new Dictionary<string, SqlParameter>();
            foreach (var param in command.Parameters.OfType<SqlParameter>())
            {
                if (param.ParameterName[0] == '@')
                {
                    paramLookup.Add(param.ParameterName, param);
                }
                else
                {
                    paramLookup.Add("@" + param.ParameterName, param);
                }
            }
            foreach (var suggestion in suggestions)
            {
                var param = paramLookup[suggestion.name];

                if (suggestion.suggested_system_type_id == (int)SqlTypeId.datetime2)
                {
                    param.SqlDbType = SqlDbType.DateTime2;
                    param.Scale = suggestion.suggested_scale;
                }
                else if (suggestion.suggested_system_type_id == (int)SqlTypeId.datetime)
                {
                    param.SqlDbType = SqlDbType.DateTime;
                }
                else if (suggestion.suggested_system_type_id == (int)SqlTypeId.date)
                {
                    param.SqlDbType = SqlDbType.Date;
                }

            }
        }
    }
}

Is there any other workaround? Tried the workarounds here and none work for only one DbContext. We have a legacy system in Delphi so we can’t convert datetime fields to datetime2 without updating the drivers, which is a BIG change. HasColumnType(“datetime”) would solve.

specifying modelBuilder.Entity<Log>().Property(p => p.Date).HasColumnType("datetime2").HasPrecision(7);

or [Column(TypeName = "datetime2")]

still produce: ((convert (datetime2, convert(varchar(255), [Extent1].[Date], 102) , 102)) >= @p__linq__0)

how to avoid it?

@adolfo1981 You are welcome. I have updated the query: there was an exception when the value of the parameter is DateTime.MinValue.

@adolfo1981 If you download the source code and change all the places I listed above from “datetime2” to “datetime” and recompile, it will work. This is assuming you are not using “datetime2” anywhere in SQL.

Also, need supports for Date datatype (without time).