question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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;
    }
}

Issue Analytics

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

github_iconTop GitHub Comments

10reactions
ImGonaRotcommented, Jul 14, 2018

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.

6reactions
stasonescommented, Mar 27, 2020

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 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

The conversion of a datetime2 data type to a datetime ...
This occurs when the entity associated with the table being saved has a mandatory datetime field and you do not set it with...
Read more >
Fix Entity Framework so it does not "assume" the SQL ...
The EF code assumes that all .NET DateTime properties should be cast as DateTime2 SQL data types when the SQL version is 2008...
Read more >
Date and Time Data - SQL Server and ADO.NET
Learn about data types for handling date and time information in the .NET Framework Data Provider for SQL Server.
Read more >
Assistance with datetime2 vs datetime in SQL2016 and ...
Issue stands from C# world reaching out through EntityFramework(EF) with IQueryable. Code generated by EF for WHERE clauseconverts all C# ...
Read more >
sql server - Incorrect comparison of datetime and datetime2
I know that it is not a good practice to have an implicit type convert. But this is really unexpected behavior when a...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found