ADO.NET · Architecture

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.

Advanced 5 min Lesson 26 of 32Examples use Microsoft.Data.SqlClient on .NET 9 with SQL Server 2022.
Before you start:SqlDataReader
0/32 in ADO.NET
On this page · 5 min · Advanced

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#

C#CategoryMapper.cs Illustrative
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))}");
Expected output
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#

  1. The JOIN returns one row per product, repeating category columns.
  2. The dictionary ensures each category is created once.
  3. LEFT JOIN rows with NULL product columns are skipped.
  4. 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#

One query per parent row (N+1)

50 categories = 51 queries. Use a JOIN or multiple result sets.

Mapping by column name strings everywhere

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
C# Illustrative
static ProductDto Map(SqlDataReader r) =>
    new(r.GetInt32(0), r.GetString(1), r.GetDecimal(2));

Mini quiz#

Mapping Rows to Objects quizMultiple choice Question 1 of 1

What is the N+1 query problem?

Score so far: 0/0

Want more? Practise all ADO.NET MCQs · ADO.NET interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.