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.
- 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;
}
- 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;
}
- 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
- Allow forcing DateTime parameters to DateTime or DateTime2 Fixes #578 Introduces an attribute which can be placed on a `DbContext`: ```C# [ForceDateTimeType(DbType.DateTime)] public class BloggingC... — committed to dotnet/ef6 by ajcvickers 5 years ago
- Allow forcing DateTime parameters to DateTime or DateTime2 Fixes #578 Introduces an attribute which can be placed on a `DbContext`: ```C# [ForceDateTimeType(DbType.DateTime)] public class BloggingC... — committed to dotnet/ef6 by ajcvickers 5 years ago
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:
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
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.
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.
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.
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.
I’m decorating the DbContexts, in my case all DateTime properties of the database will be DATETIME.
@adolfo1981 , yes, just made a little change to check for a Attribute so I can have both scenarios simultaneously.
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:
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).