Mapping Rows to Objects
Your app works with objects, the database with rows. A clean mapping layer keeps ADO.NET details out of the rest of the code.
On this page · 5 min · Advanced
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
- Advanced
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
Rows from SQL are like raw form entries; mapping copies them neatly into C# objects (records) your app understands.
- Mapping
- Row columns → object properties.
- DTO / record
- Simple object to carry data.
- N+1 problem
- One query per item instead of one query for all.
Learning objectives#
- Write reusable mapper methods
- Map to records and DTOs
- Map one-to-many results
- Keep readers inside the data layer
Explanation#
Write one static Map method per type that reads a row into an object. Every query for that type reuses it.
Records are ideal for read models: concise, immutable and with value equality.
For a JOIN that returns a category with its products, use a Dictionary keyed by category Id so each category object is created once.
Return lists or objects from your data layer — never SqlDataReader or DataTable — so callers do not depend on ADO.NET.
Code example: Categories with their products from one JOIN#
public record ProductDto(int Id, string Name, decimal Price);
public record CategoryDto(int Id, string Name, List<ProductDto> Products);
static async Task<List<CategoryDto>> GetCatalogAsync(SqlConnection conn)
{
await using var cmd = new SqlCommand("""
SELECT c.Id, c.Name, p.Id, p.Name, p.Price
FROM Categories c LEFT JOIN Products p ON p.CategoryId = c.Id
ORDER BY c.Name, p.Name
""", conn);
await using var r = await cmd.ExecuteReaderAsync();
var byId = new Dictionary<int, CategoryDto>();
while (await r.ReadAsync())
{
int catId = r.GetInt32(0);
if (!byId.TryGetValue(catId, out var cat))
byId[catId] = cat = new CategoryDto(catId, r.GetString(1), new());
if (!r.IsDBNull(2)) // LEFT JOIN: category may have no products
cat.Products.Add(new ProductDto(r.GetInt32(2), r.GetString(3), r.GetDecimal(4)));
}
return byId.Values.ToList();
}
foreach (var c in await GetCatalogAsync(conn))
Console.WriteLine(quot;{c.Name}: {string.Join(", ", c.Products.Select(p => p.Name))}");
Accessories: Keyboard, Mouse Computers: Laptop Gifts:
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#
- The JOIN returns one row per product, repeating category columns.
- The dictionary ensures each category is created once.
- LEFT JOIN rows with NULL product columns are skipped.
- The method returns plain DTOs.
Real-world use case#
A menu API returns categories with nested products in one query instead of one query per category (the N+1 problem).
Common mistakes#
50 categories = 51 queries. Use a JOIN or multiple result sets.
Typos break at runtime. Centralise mapping code.
Best practices#
- One mapper per type
- Return DTOs/records
- Use ordinals for speed
- Avoid N+1 queries
Remember this#
Rows in, objects out.
Dictionary for one-to-many.
Never leak readers out of the data layer.
Practice exercise#
Your task: Write a static Map method for ProductDto from a reader positioned on a row with Id, Name, Price.
Show solution
static ProductDto Map(SqlDataReader r) =>
new(r.GetInt32(0), r.GetString(1), r.GetDecimal(2));
Mini quiz#
What is the N+1 query problem?
Want more? Practise all ADO.NET MCQs · ADO.NET interview questions
My notes#
Saved in this browser.