SqlDataReader
SqlDataReader streams rows from SQL Server one at a time. It is the fastest way to read data in .NET.
On this page · 5 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Intermediate
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
A SqlDataReader is a ticket counter queue: rows come one at a time, forward only. Read each, then move to the next — fast and light.
- Read()
- Move to the next row; false at the end.
- GetInt32 / GetString
- Typed getters for columns.
- Forward-only
- Cannot go back to earlier rows.
Learning objectives#
- Loop through rows with Read/ReadAsync
- Read columns with typed getters
- Use GetOrdinal for clear, fast column access
- Map rows into C# records
Explanation#
A reader is forward-only and read-only: you move from the first row to the last, once.
Typed getters (GetInt32, GetString, GetDecimal, GetDateTime) avoid boxing and conversions. GetFieldValue<T> works for any type.
Look up column positions once with GetOrdinal("Name") before the loop: clearer than magic numbers and faster than reader["Name"] on every row.
Check IsDBNull before reading nullable columns.
Code example: Mapping rows to records#
public record Product(int Id, string Name, decimal Price, int Stock, string? Description);
static async Task<List<Product>> GetProductsAsync(string cs)
{
await using var conn = new SqlConnection(cs);
await conn.OpenAsync();
await using var cmd = new SqlCommand("SELECT Id, Name, Price, Stock, Description FROM Products ORDER BY Name", conn);
await using var r = await cmd.ExecuteReaderAsync();
int id = r.GetOrdinal("Id"), name = r.GetOrdinal("Name"), price = r.GetOrdinal("Price"),
stock = r.GetOrdinal("Stock"), desc = r.GetOrdinal("Description");
var list = new List<Product>();
while (await r.ReadAsync())
{
list.Add(new Product(
r.GetInt32(id), r.GetString(name), r.GetDecimal(price), r.GetInt32(stock),
r.IsDBNull(desc) ? null : r.GetString(desc)));
}
return list;
}
foreach (var p in await GetProductsAsync(cs)) Console.WriteLine(quot;{p.Name,-10} ₹{p.Price,9} stock {p.Stock}");
Keyboard ₹ 2200.00 stock 40 Laptop ₹ 55000.00 stock 8 Mouse ₹ 1400.00 stock 120
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- GetOrdinal finds each column’s position once.
- ReadAsync moves to the next row without blocking a thread.
- Typed getters read values directly.
- Description can be NULL, so IsDBNull decides between null and the string.
- The method returns a list — the reader and connection are closed before it returns.
Real-world use case#
A Web API GET /products endpoint uses this method; the controller returns the list as JSON.
Common mistakes#
It throws SqlNullValueException. Check IsDBNull first.
GetInt32 on a BIGINT or GetDouble on a DECIMAL throws InvalidCastException. Match types: INT→GetInt32, BIGINT→GetInt64, DECIMAL→GetDecimal.
Best practices#
- Use GetOrdinal once, outside the loop
- Use typed getters
- Map to records or DTOs inside the method
- Select only the columns you use
Remember this#
Forward-only, read-only, fastest.
GetOrdinal once → typed getters in the loop.
IsDBNull before reading nullable columns.
Practice exercise#
Your task: Read Id and Name of products with Price above a given amount into a List<(int, string)>.
Show solution
await using var cmd = new SqlCommand("SELECT Id, Name FROM Products WHERE Price > @min", conn);
cmd.Parameters.Add("@min", SqlDbType.Decimal).Value = min;
await using var r = await cmd.ExecuteReaderAsync();
var result = new List<(int, string)>();
while (await r.ReadAsync()) result.Add((r.GetInt32(0), r.GetString(1)));
Mini quiz#
Which getter reads a SQL DECIMAL column?
Want more? Practise all ADO.NET MCQs · ADO.NET interview questions
My notes#
Saved in this browser.