The traps, with real results
Each result below is what SQL Server returned when the test ran it.
decimal(18,2) is EF Core's silent default
A decimal property without HasPrecision, [Precision] or HasColumnType becomes decimal(18,2), and EF Core only logs a warning. SQL Server rounds extra decimals away and fails for more than 16 digits before the point:
| Expression | Result |
|---|---|
SELECT CAST(1.005 AS decimal(18,2)) | 1.01 |
SELECT CAST(2.675 AS decimal(18,2)) | 2.68 |
SELECT CAST(12345678901234567.0 AS decimal(18,2)) | Error 8115 Arithmetic overflow error converting numeric to data type numeric. |
EF Core 10.0.12 logs this when it builds the model:
No store type was specified for the decimal property 'Decimal' on entity type 'AllTypes'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.
The fix:
modelBuilder.Entity<Product>().Property(p => p.Price).HasPrecision(19, 4);
// or [Precision(19, 4)] on the property, or for every decimal:
protected override void ConfigureConventions(ModelConfigurationBuilder b) =>
b.Properties<decimal>().HavePrecision(19, 4);
datetime rounds; datetime2 does not
datetime stores time in 1/300 second steps, so milliseconds change on the way in, and .999 even moves to the next day. It also rejects dates before 1753, including DateTime.MinValue, which a default(DateTime) is:
| Expression | Result |
|---|---|
SELECT CAST('2026-01-01T12:00:00.001' AS datetime) | 2026-01-01 12:00:00.0000000 |
SELECT CAST('2026-01-01T12:00:00.005' AS datetime) | 2026-01-01 12:00:00.0070000 |
SELECT CAST('2026-01-01T23:59:59.999' AS datetime) | 2026-01-02 00:00:00.0000000 |
SELECT CAST('2026-01-01T23:59:59.9999999' AS datetime2) | 2026-01-01 23:59:59.9999999 |
SELECT CAST('1752-12-31' AS datetime) | Error 242 The conversion of a varchar data type to a datetime data type resulted in an out-of-range value. |
DateTime.MinValue sent as SqlDbType.DateTime | SqlTypeException SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM. |
DateTime.MinValue sent as SqlDbType.DateTime2 | 0001-01-01 00:00:00.0000000 |
AddWithValue guesses the SQL type from the C# value
AddWithValue sends a DateTime as datetime (not datetime2) and every string as nvarchar. Against a varchar column an nvarchar parameter makes SQL Server convert the column, which can turn an index seek into a scan. Set the type explicitly:
| C# | Result |
|---|---|
AddWithValue("@p", /* DateTime */ value) | SqlDbType.DateTime |
AddWithValue("@p", /* string */ value) | SqlDbType.NVarChar |
AddWithValue("@p", /* decimal */ value) | SqlDbType.Decimal |
AddWithValue("@p", /* DateOnly */ value) | SqlDbType.Date |
AddWithValue("@p", /* TimeSpan */ value) | SqlDbType.Time |
AddWithValue("@p", /* double */ value) | SqlDbType.Float |
AddWithValue("@p", /* float */ value) | SqlDbType.Real |
The fix:
cmd.Parameters.Add("@name", SqlDbType.VarChar, 50).Value = name;
cmd.Parameters.Add("@created", SqlDbType.DateTime2).Value = created;
varchar loses characters outside its code page
A varchar column stores one code page, set by its collation (Latin-1 for the default SQL_Latin1_General_CP1_CI_AS). Anything else is replaced with "?" without an error. Emoji need two UTF-16 units even in nvarchar:
| Expression | Result |
|---|---|
SELECT CAST(N'Ђорђе' AS varchar(10)) | ????? |
SELECT CAST(N'café' AS varchar(10)) | café |
SELECT LEN(CAST(N'😀' AS nvarchar(10))) | 2 |
SELECT DATALENGTH(CAST(N'abc' AS nvarchar(10))) | 6 |
SELECT DATALENGTH(CAST('abc' AS char(10))) | 10 |
Values .NET cannot hold
decimal(38,0) holds more digits than System.Decimal. GetDecimal throws; GetSqlDecimal returns it, and SqlDecimal can round or convert it to a string:
| Expression | Result |
|---|---|
reader.GetSqlDecimal(0) | 99999999999999999999999999999999999999 |
reader.GetDecimal(0) | OverflowException Conversion overflows. |
NULL, DBNull and nullable types
A NULL column comes back from ADO.NET as DBNull.Value, not null, and GetInt32 on it throws SqlNullValueException. Check reader.IsDBNull(i) first, and send nulls as DBNull.Value:
int? age = reader.IsDBNull(2) ? null : reader.GetInt32(2);
cmd.Parameters.Add("@age", SqlDbType.Int).Value = (object?)age ?? DBNull.Value;
EF Core and Dapper do this for you: a nullable column maps to int?, DateTime? or string?. With nullable reference types on, EF Core makes string properties NOT NULL and string? properties NULL.
rowversion for optimistic concurrency
rowversion (called timestamp in older scripts and in GetDataTypeName) is 8 bytes that SQL Server changes on every update of the row. Mark a byte[] property with [Timestamp] (or IsRowVersion()) and EF Core adds it to the WHERE clause of updates; if someone else changed the row, SaveChanges throws DbUpdateConcurrencyException.
FAQ
What is the C# equivalent of SQL Server datetime2?
DateTime. Both datetime and datetime2 are read as DateTime, and EF Core creates datetime2 columns for DateTime properties. datetime2 keeps 100 ns precision and dates from year 1; datetime rounds to 1/300 of a second and starts in 1753.
Which C# type should I use for SQL Server decimal and money?
decimal. Set the precision explicitly: EF Core maps an unconfigured decimal property to decimal(18,2) and only logs a warning, so extra decimals are rounded away.
What is the C# type for uniqueidentifier?
Guid (SqlDbType.UniqueIdentifier). SQL Server sorts uniqueidentifier values by their last six bytes first, so its ORDER BY differs from Guid.CompareTo.
Can I map SQL Server date and time to DateOnly and TimeOnly?
Yes. EF Core scaffolds date as DateOnly and time as TimeOnly, and SqlDataReader.GetFieldValue<DateOnly> and GetFieldValue<TimeOnly> work with Microsoft.Data.SqlClient 6. GetFieldType still reports DateTime and TimeSpan.
Should I use nvarchar or varchar?
nvarchar for text people type: it stores any Unicode character. varchar stores one code page and silently turns other characters into question marks, unless the column has a UTF-8 collation (SQL Server 2019 and later).
What is rowversion in C#?
A byte[] of 8 bytes. With [Timestamp] or IsRowVersion() EF Core uses it as a concurrency token and throws DbUpdateConcurrencyException when the row changed since it was read.