Question

SQL Server to C# Data Type Mapping Guide

csharp.netsql-server

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, and table

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 int becomes C# int.
  • SQL Server nvarchar becomes C# string.
  • SQL Server uniqueidentifier becomes C# Guid.

Some mappings require care:

  • SQL NULL needs a nullable C# value type, such as int? or DateTime?.
  • SQL Server float normally maps to C# double, because SQL Server float defaults to 53-bit precision.
  • SQL Server timestamp is not a date or time. It is a row-versioning value stored as bytes.
  • Types such as table, cursor, and hierarchyid are 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 bigint needs a large-number C# container: long.
  • A text SQL container such as nvarchar needs 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).

Take Quiz

Syntax and Examples

The following table shows common SQL Server to C# mappings.

SQL Server typeRecommended C# typeNotes
bigintlong64-bit signed integer
numeric(p, s)decimalSame guidance as decimal(p, s)
bitboolUse bool? if the column allows NULL
smallintshort16-bit signed integer
decimal(p, s)decimalGood for fixed-precision values
smallmoneydecimalPrefer decimal in application code
intint32-bit signed integer
tinyintbyteSQL Server range is 0 to 255
moneydecimaldecimal is usually clearer for calculations
floatdoubleSQL Server float normally means float(53)
realfloatC# float is System.Single
dateDateOnlyIn modern .NET; use DateTime in older targets
datetimeoffsetDateTimeOffsetIncludes offset information
datetime2DateTimeHigher precision than old datetime
smalldatetimeDateTimeLower SQL Server precision
datetimeDateTimeLegacy SQL Server date/time type
timeTimeOnly or TimeSpanTimeOnly is preferred in modern .NET
char, varchar, textstringtext is deprecated in SQL Server
nchar, nvarchar, ntextstringntext is deprecated in SQL Server
binary, varbinary, imagebyte[]image is deprecated in SQL Server
timestamp / rowversionbyte[]A concurrency/version token, not a time
hierarchyidSqlHierarchyIdFrom Microsoft.SqlServer.Types
uniqueidentifierGuid128-bit identifier
sql_variantobjectActual runtime type varies
xmlstring, SqlXml, or XML APIsChoice depends on how it is processed
tableDataTable or structured parameterUsed 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:

  1. OrderId has SQL type int, so it maps to C# int.
  2. Total has SQL type decimal(10, 2), so it maps to C# decimal. The m suffix makes 19.95m a decimal literal.
  3. PaidAt is SQL datetime2, but its value is NULL. C# DateTime cannot hold null, so the property must be DateTime?.
  4. IsPaid is SQL bit with value 1, so it maps to C# bool with value true.

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) to decimal for totals, tax, and account balances.
  • User records: Map uniqueidentifier to Guid for public IDs and nvarchar to string for names and email addresses.
  • Scheduling: Map date to DateOnly, time to TimeOnly, and datetimeoffset to DateTimeOffset when timezone offsets matter.
  • File uploads: Map varbinary(max) to byte[] when storing a small document or image in SQL Server.
  • Optimistic concurrency: Map a rowversion column to byte[] so updates can detect whether another user changed the row.
  • Stored procedure bulk input: Send a DataTable as a table-valued parameter for many rows, such as importing order items.
  • Hierarchical data: Use hierarchyid and SqlHierarchyId for structures such as organization charts or category trees.
Take Quiz

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 of text
  • nvarchar(max) instead of ntext
  • varbinary(max) instead of image

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

NeedSQL Server choiceC# choiceWhy
Whole number up to about 2.1 billionintintStandard 32-bit integer
Very large whole numberbigintlong64-bit integer range
Non-negative byte-sized numbertinyintbyteSQL Server tinyint is 0–255
Exact money or quantitydecimal(p, s)decimalFixed decimal precision
Scientific/measurement valuefloat or realdouble or floatApproximate floating-point storage
Calendar date onlydateDateOnlyAvoids an invented time value
Time plus UTC offsetdatetimeoffsetDateTimeOffsetPreserves the offset
Ordinary textvarchar / nvarcharstringnvarchar supports Unicode
Raw bytesvarbinarybyte[]Represents a sequence of bytes
IdentifieruniqueidentifierGuidBoth represent a GUID

DateTime versus DateTimeOffset

TypeUse it when
DateTimeThe SQL value is datetime or datetime2, and an offset is not stored.
DateTimeOffsetThe SQL value is datetimeoffset, or preserving the recorded offset matters.

DateOnly and TimeOnly versus older alternatives

SQL Server typeModern .NETOlder compatible approach
dateDateOnlyDateTime with only the date portion used
timeTimeOnlyTimeSpan

Use the modern type when your target framework and data provider support it; otherwise use the established alternative.

Take Quiz

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? or byte[]? when nullable reference types are enabled.
  • Use decimal, not float or double, for currency and exact decimal values.
  • SQL Server float usually maps to C# double; SQL Server real maps to C# float.
  • timestamp means rowversion, not a date/time value.
  • Prefer varchar(max), nvarchar(max), and varbinary(max) instead of deprecated text, ntext, and image.

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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more