Question
For the following SQL Server data types, which corresponding C# types should be used when reading values from SQL Server or defining application models?
- Exact numerics:
bigint,numeric,bit,smallint,decimal,smallmoney,int,tinyint,money - Approximate numerics:
float,real - Date and time:
date,datetimeoffset,datetime2,smalldatetime,datetime,time - Character strings:
char,varchar,text - Unicode character strings:
nchar,nvarchar,ntext - Binary strings:
binary,varbinary,image - Other types:
cursor,timestamp,hierarchyid,uniqueidentifier,sql_variant,xml, andtable
Short Answer
By the end of this page, you will be able to choose appropriate C# types for common SQL Server columns, handle database NULL values safely, and recognize SQL Server types that need special handling rather than a simple C# property type.
Concept
SQL Server and C# use different type systems:
- SQL Server types describe how data is stored in a database.
- C# types describe values used while your application runs.
When data is read through ADO.NET, an ORM such as Entity Framework, or a stored procedure, the database value must be represented by a compatible .NET type.
The mapping is often direct:
- SQL Server
intbecomes C#int. - SQL Server
nvarcharbecomes C#string. - SQL Server
uniqueidentifierbecomes C#Guid.
Some mappings require care:
- SQL
NULLneeds a nullable C# value type, such asint?orDateTime?. - SQL Server
floatnormally maps to C#double, because SQL Serverfloatdefaults to 53-bit precision. - SQL Server
timestampis not a date or time. It is a row-versioning value stored as bytes. - Types such as
table,cursor, andhierarchyidare database-specific and do not have a universal ordinary C# primitive equivalent.
Choosing compatible types prevents overflow, precision loss, conversion exceptions, and incorrect application behavior.
Mental Model
Think of a database column as a labeled storage container and a C# property as the application-side container used to receive its contents.
The labels must be compatible:
- A large-number SQL container such as
bigintneeds a large-number C# container:long. - A text SQL container such as
nvarcharneeds a text C# container:string. - A possibly empty SQL container needs a C# container that permits no value, such as
decimal?.
Using the wrong container is like trying to pour a large bottle into a small cup: it may spill (overflow), lose detail (precision loss), or fail completely (conversion error).
Syntax and Examples
The following table shows common SQL Server to C# mappings.
| SQL Server type | Recommended C# type | Notes |
|---|---|---|
bigint | long | 64-bit signed integer |
numeric(p, s) | decimal | Same guidance as decimal(p, s) |
bit | bool | Use bool? if the column allows NULL |
smallint | short | 16-bit signed integer |
decimal(p, s) | decimal | Good for fixed-precision values |
smallmoney | decimal | Prefer decimal in application code |
int | int | 32-bit signed integer |
tinyint | byte | SQL Server range is 0 to 255 |
money | decimal | decimal is usually clearer for calculations |
float | double | SQL Server float normally means float(53) |
real | float | C# float is System.Single |
date | DateOnly | In modern .NET; use DateTime in older targets |
datetimeoffset | DateTimeOffset | Includes offset information |
datetime2 | DateTime | Higher precision than old datetime |
smalldatetime | DateTime | Lower SQL Server precision |
datetime | DateTime | Legacy SQL Server date/time type |
time | TimeOnly or TimeSpan | TimeOnly is preferred in modern .NET |
char, varchar, text | string | text is deprecated in SQL Server |
nchar, nvarchar, ntext | string | ntext is deprecated in SQL Server |
binary, varbinary, image | byte[] | image is deprecated in SQL Server |
timestamp / rowversion | byte[] | A concurrency/version token, not a time |
hierarchyid | SqlHierarchyId | From Microsoft.SqlServer.Types |
uniqueidentifier | Guid | 128-bit identifier |
sql_variant | object | Actual runtime type varies |
xml | string, SqlXml, or XML APIs | Choice depends on how it is processed |
table | DataTable or structured parameter | Used for table-valued parameters/results |
A model class might look like this:
public sealed class Customer
{
public int Id { get; set; } // SQL: int NOT NULL
public string Name { get; set; } = ""; // SQL: nvarchar(...)
public decimal? CreditLimit { get; set; } // SQL: decimal(...) NULL
public DateOnly BirthDate { get; set; } // SQL: date NOT NULL
public Guid PublicId { get; set; } // SQL: uniqueidentifier
public byte[] RowVersion { get; set; } = []; // SQL: rowversion
}
If a column can contain SQL NULL, use a nullable value type:
public int? ManagerId { get; set; }
public DateTimeOffset? DeletedAt { get; set; }
public bool? IsReviewed { get; set; }
Reference types such as string and byte[] can also represent missing data with null. In projects using nullable reference types, write string? when a database string column allows NULL.
Step by Step Execution
Consider a row returned from this SQL query:
SELECT
42 AS OrderId,
CAST(19.95 AS decimal(10, 2)) AS Total,
CAST(NULL AS datetime2) AS PaidAt,
CAST(1 AS bit) AS IsPaid;
A C# representation is:
int orderId = 42;
decimal total = 19.95m;
DateTime? paidAt = null;
bool isPaid = true;
Step by step:
OrderIdhas SQL typeint, so it maps to C#int.Totalhas SQL typedecimal(10, 2), so it maps to C#decimal. Themsuffix makes19.95ma decimal literal.PaidAtis SQLdatetime2, but its value isNULL. C#DateTimecannot holdnull, so the property must beDateTime?.IsPaidis SQLbitwith value1, so it maps to C#boolwith valuetrue.
When manually reading through SqlDataReader, check for SQL NULL before calling a typed getter:
DateTime? paidAt = reader.IsDBNull(reader.GetOrdinal("PaidAt"))
? null
: reader.GetDateTime(reader.GetOrdinal("PaidAt"));
Calling GetDateTime for a database NULL would throw an exception.
Real World Use Cases
- Prices and invoices: Map
decimal(12, 2)todecimalfor totals, tax, and account balances. - User records: Map
uniqueidentifiertoGuidfor public IDs andnvarchartostringfor names and email addresses. - Scheduling: Map
datetoDateOnly,timetoTimeOnly, anddatetimeoffsettoDateTimeOffsetwhen timezone offsets matter. - File uploads: Map
varbinary(max)tobyte[]when storing a small document or image in SQL Server. - Optimistic concurrency: Map a
rowversioncolumn tobyte[]so updates can detect whether another user changed the row. - Stored procedure bulk input: Send a
DataTableas a table-valued parameter for many rows, such as importing order items. - Hierarchical data: Use
hierarchyidandSqlHierarchyIdfor structures such as organization charts or category trees.
Real Codebase Usage
In production code, mappings are usually expressed in entity classes, DTOs, and database access code.
Entity property mappings
public sealed class Invoice
{
public long Id { get; set; }
public decimal Amount { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? PaidAt { get; set; }
public string? Notes { get; set; }
}
The nullable properties match columns that allow SQL NULL.
Validation before writing data
Applications commonly validate values before inserting or updating data:
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}
if (string.IsNullOrWhiteSpace(customerName))
{
throw new ArgumentException("A customer name is required.", nameof(customerName));
}
This prevents invalid values from reaching the database and produces clearer errors.
Parameterized SQL
Use parameters rather than concatenating values into SQL text:
using var command = new SqlCommand(
"INSERT INTO Payments (Amount, PaidAt) VALUES (@amount, @paidAt)",
connection);
command.Parameters.Add("@amount", System.Data.SqlDbType.Decimal).Value = amount;
command.Parameters.Add("@paidAt", System.Data.SqlDbType.DateTime2).Value =
paidAt is null ? DBNull.Value : paidAt.Value;
Parameters preserve type information, handle escaping safely, and avoid SQL injection.
Rowversion concurrency checks
A common update statement includes the old row version:
UPDATE Products
SET Name = @name
WHERE Id = @id AND RowVersion = @rowVersion;
If no row is updated, the application knows the record changed after it was originally loaded.
Common Mistakes
Treating SQL NULL as an ordinary C# value
This fails when PaidAt is NULL:
// Broken if the database value is NULL.
DateTime paidAt = reader.GetDateTime(0);
Use a nullable type and test for DBNull:
DateTime? paidAt = reader.IsDBNull(0) ? null : reader.GetDateTime(0);
Mapping bigint to int
// Risky: SQL bigint values can exceed int's range.
int id = reader.GetInt32(0);
Use long and GetInt64 for SQL bigint.
Using double for currency
// Floating-point values may not represent decimal fractions exactly.
double total = 0.1 + 0.2;
Use decimal for fixed-precision financial amounts:
decimal total = 0.1m + 0.2m;
Confusing SQL Server float with C# float
SQL Server float defaults to float(53), which normally maps to C# double. SQL Server real maps to C# float.
Treating timestamp as a date/time
Despite its name, SQL Server timestamp is a deprecated synonym for rowversion. It is a binary version value:
public byte[] RowVersion { get; set; } = [];
It should not be mapped to DateTime.
Forgetting deprecated SQL Server types
text, ntext, and image are deprecated. For new schemas, prefer:
varchar(max)instead oftextnvarchar(max)instead ofntextvarbinary(max)instead ofimage
Their usual C# mappings remain string and byte[].
Assuming sql_variant has one fixed C# type
sql_variant can store different underlying SQL types. object is the broad mapping, but code must inspect and validate the actual returned type.
Comparisons
| Need | SQL Server choice | C# choice | Why |
|---|---|---|---|
| Whole number up to about 2.1 billion | int | int | Standard 32-bit integer |
| Very large whole number | bigint | long | 64-bit integer range |
| Non-negative byte-sized number | tinyint | byte | SQL Server tinyint is 0–255 |
| Exact money or quantity | decimal(p, s) | decimal | Fixed decimal precision |
| Scientific/measurement value | float or real | double or float | Approximate floating-point storage |
| Calendar date only | date | DateOnly | Avoids an invented time value |
| Time plus UTC offset | datetimeoffset | DateTimeOffset | Preserves the offset |
| Ordinary text | varchar / nvarchar | string | nvarchar supports Unicode |
| Raw bytes | varbinary | byte[] | Represents a sequence of bytes |
| Identifier | uniqueidentifier | Guid | Both represent a GUID |
DateTime versus DateTimeOffset
| Type | Use it when |
|---|---|
DateTime | The SQL value is datetime or datetime2, and an offset is not stored. |
DateTimeOffset | The SQL value is datetimeoffset, or preserving the recorded offset matters. |
DateOnly and TimeOnly versus older alternatives
| SQL Server type | Modern .NET | Older compatible approach |
|---|---|---|
date | DateOnly | DateTime with only the date portion used |
time | TimeOnly | TimeSpan |
Use the modern type when your target framework and data provider support it; otherwise use the established alternative.
Cheat Sheet
SQL Server C#
------------------------------------------------
bigint long
int int
smallint short
tinyint byte
bit bool
numeric(p, s), decimal(p,s) decimal
money, smallmoney decimal
float double
real float
date DateOnly (or DateTime)
datetime, datetime2 DateTime
datetimeoffset DateTimeOffset
time TimeOnly (or TimeSpan)
char, varchar, text string
nchar, nvarchar, ntext string
binary, varbinary, image byte[]
uniqueidentifier Guid
timestamp / rowversion byte[]
hierarchyid SqlHierarchyId
sql_variant object
xml string, SqlXml, or XML API type
table DataTable / structured parameter
Rules to remember:
- Add
?to C# value types for nullable SQL columns:int?,bool?,decimal?,DateTime?. - For nullable reference-type columns, use
string?orbyte[]?when nullable reference types are enabled. - Use
decimal, notfloatordouble, for currency and exact decimal values. - SQL Server
floatusually maps to C#double; SQL Serverrealmaps to C#float. timestampmeansrowversion, not a date/time value.- Prefer
varchar(max),nvarchar(max), andvarbinary(max)instead of deprecatedtext,ntext, andimage.
FAQ
What is the C# equivalent of SQL Server int?
Use C# int (System.Int32). If the SQL column permits NULL, use int?.
What is the C# equivalent of SQL Server bigint?
Use C# long (System.Int64). Do not use int, because a bigint can exceed the int range.
Should SQL Server money map to C# decimal?
Yes. decimal is the usual C# choice for money, smallmoney, and SQL decimal values.
Why does SQL Server float map to C# double?
SQL Server float defaults to 53 bits of precision, which corresponds to C# double. SQL Server real corresponds to C# float.
What C# type should I use for a nullable SQL column?
Use nullable value types for value columns, such as int?, decimal?, bool?, and DateTime?. Use string? or byte[]? for nullable reference columns when nullable reference types are enabled.
Is SQL Server timestamp a C# DateTime?
No. SQL Server timestamp is a deprecated name for rowversion, a binary concurrency token. Map it to byte[].
What is the C# equivalent of uniqueidentifier?
Use Guid.
What should I use for SQL Server date and time?
Use DateOnly for date and TimeOnly for time in modern .NET. DateTime and TimeSpan are common alternatives for older frameworks or providers.
Mini Project
Description
Build a small C# order model that represents common SQL Server column types. This demonstrates direct mappings, nullable database fields, a GUID, date-only data, exact currency, and a rowversion concurrency token.
Goal
Create and print an order object whose C# properties correctly match a typical SQL Server Orders table.
Requirements
Create an Order class with bigint, decimal, date, datetimeoffset, nvarchar, uniqueidentifier, and rowversion equivalents.
Allow the optional paid date to have no value.
Create one sample order and print its key values.
Use decimal for the monetary amount.
Represent the row version as a byte array.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.