What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Dapper is a lightweight, fast micro-ORM for .NET that gives you direct control over SQL while still handling object materialization cleanly. When an application needs to load related data efficiently, two features become especially useful: QueryMulle for reading several result sets from a single database round trip, and multi-mapping for turning joined rows into richer object graphs.
This tutorial walks through practical patterns for loading parent-child and related data models with Dapper, including setup, sample entities, mulle result set handling, split-on mapping, and one-to-many aggregation without duplicate parent objects. It also covers where these techniques work well, where they can become error-prone, and how to structure queries so the resulting C# code stays predictable and maintainable.
As an Amazon Associate I earn from qualifying purchases.
Setting Up Dapper and Sample Database Models
Before using QueryMulle or Dapper multi-mapping, set up a small relational model that is realistic enough to show parent-child and related-entity queries. The examples in this tutorial use SQL Server, but the same Dapper APIs work with other ADO.NET providers such as Npgsql for PostgreSQL or MySqlConnector for MySQL. Install Dapper from NuGet in your data access project:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →dotnet add package Dapper
You also need a database provider package. For SQL Server, install:
#1 Best Overall
- Easily store and access 2TB to content on the go with the Seagate Portable Drive, a USB external hard drive
- Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop
- To get set up, connect the portable hard drive to a computer for automatic recognition no software required
- This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
- The available storage capacity may vary.
dotnet add package Microsoft.Data.SqlClient
The sample schema uses customers, orders, order items, and products. This gives us a common one-to-many relationship from Customer to Order, another one-to-many relationship from Order to OrderItem, and a many-to-one relationship from OrderItem to Product. These relationships are ideal for demonstrating both separate result sets and row-based multi-mapping.
Use the following SQL shape as the basis for the examples:
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Email NVARCHAR(200) NOT NULL
);
CREATE TABLE Orders (
Id INT PRIMARY KEY,
CustomerId INT NOT NULL,
OrderDate DATETIME2 NOT NULL,
Status NVARCHAR(50) NOT NULL,
CONSTRAINT FK_Orders_Customers
FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
);
CREATE TABLE Products (
Id INT PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
UnitPrice DECIMAL(18, 2) NOT NULL
);
CREATE TABLE OrderItems (
Id INT PRIMARY KEY,
OrderId INT NOT NULL,
ProductId INT NOT NULL,
Quantity INT NOT NULL,
UnitPrice DECIMAL(18, 2) NOT NULL,
CONSTRAINT FK_OrderItems_Orders
FOREIGN KEY (OrderId) REFERENCES Orders(Id),
CONSTRAINT FK_OrderItems_Products
FOREIGN KEY (ProductId) REFERENCES Products(Id)
);
In C#, create plain model classes that match the column names returned by your SQL queries. Dapper maps columns to properties by name, so predictable names such as Id, Name, and CustomerId keep the examples simple. Collection properties are not filled automatically; you populate them when processing mulle result sets or grouping multi-mapped rows.
public sealed class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public List<Order> Orders { get; set; } = new();
}
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutepublic sealed class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public string Status { get; set; } = "";
public List<OrderItem> Items { get; set; } = new();
}
public sealed class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public Product? Product { get; set; }
}
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal UnitPrice { get; set; }
}
For connection creation, keep it explicit and short-lived. Dapper extends IDbConnection, so you open a provider connection and call methods such as Query, QueryAsync, QueryMulle, or QueryMultipleAsync on it. In a typical application, the connection string comes from configuration rather than being hard-coded.
using Microsoft.Data.SqlClient;
using System.Data;
public static IDbConnection CreateConnection(string connectionString)
{
return new SqlConnection(connectionString);
}
With this setup in place, the following sections can focus on how the data is read and assembled. The same model will be used to compare two common approaches: returning several result sets in one database round trip, and returning joined rows that Dapper splits into mulle objects with multi-mapping.
Reading Multiple ResultSets with QueryMultiple
Dapper’s QueryMulle method is designed for cases where one database round trip should return several independent result sets. Instead of running separate queries for an order header, its line items, and a lookup list, you can send one SQL batch and read each result set in sequence. This is especially useful for detail screens, dashboards, and API endpoints that need related data but do not necessarily need a single joined row shape.
Assume you have sample models such as Customer, Order, and OrderItem. A common detail query might need the order, the customer, and all items for that order. With QueryMulle, the SQL can return each shape separately:
const string sql = @"
select Id, OrderDate, CustomerId, Status
from Orders
where Id = @OrderId;
Rank #2
- Easily store and access 5TB of content on the go with the Seagate portable drive, a USB external hard Drive
- Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop
- To get set up, connect the portable hard drive to a computer for automatic recognition software required
- This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
- The available storage capacity may vary.
select Id, Name, Email
from Customers
where Id = (
select CustomerId from Orders where Id = @OrderId
);
select Id, OrderId, ProductName, Quantity, UnitPrice
from OrderItems
where OrderId = @OrderId;
";
using var multi = connection.QueryMulle(sql, new { OrderId = orderId });
var order = multi.ReadSingle<Order>();
var customer = multi.ReadSingle<Customer>();
var items = multi.Read<OrderItem>().ToList();
order.Customer = customer;
order.Items = items;
The order of calls to Read, ReadSingle, or ReadFirstOrDefault must match the order of the select statements in the SQL batch. Dapper does not identify result sets by name; it advances through them like a stream. If the first SQL statement returns orders, then the first read should consume orders. If you skip a read or read the wrong type, the later mappings will also be misaligned.
Choosing the right read method
Read<T>()maps zero or more rows and is commonly used for collections such as order items, products, or comments.ReadSingle<T>()expects exactly one row and throws if there are zero or multiple rows.ReadSingleOrDefault<T>()allows zero or one row, which is useful for optional related data.ReadFirst<T>()returns the first row and throws if there are none.ReadFirstOrDefault<T>()returns the first row or the default value when no rows exist.
For a detail page, strict methods such as ReadSingle can be helpful because they reveal unexpected data problems early. For optional sections, such as a customer’s preferred address or the latest invoice, the OrDefault variants are safer. Collections should usually be materialized with ToList() before the grid reader is disposed, because the underlying reader is tied to the active connection.
You can also use QueryMulleAsync in asynchronous code. The structure is nearly identical, but each database operation is awaited:
await using var multi = await connection.QueryMulleAsync(sql, new { OrderId = orderId });
var order = await multi.ReadSingleAsync<Order>();
var customer = await multi.ReadSingleAsync<Customer>();
var items = (await multi.ReadAsync<OrderItem>()).ToList();
order.Customer = customer;
order.Items = items;
A practical pattern is to keep each result set simple and focused. Avoid returning one massive joined result when separate result sets more naturally match your object model. For example, return one row for the parent entity, one row for a related single entity, and a separate list for children. This reduces repeated parent data, avoids manual grouping for straightforward cases, and keeps the SQL easier to inspect when troubleshooting.
Mapping Related Objects with Dapper Multi-Mapping
Dapper multi-mapping is used when a single row contains columns for more than one object. A common example is loading an Order together with its Customer, where the SQL query joins both tables and returns one flattened result set. Instead of manually reading each column and assigning properties, Dapper can split each row into mulle objects and let you connect them in a mapping function.
Assume the sample model has an Order class with a Customer property. The SQL query should return the order columns first, followed by the customer columns. Dapper reads the row from left to right, creates an Order, then creates a Customer starting at the column named in splitOn.
var sql = @"
SELECT
o.Id,
o.OrderDate,
o.Total,
c.Id,
c.Name,
c.Email
FROM Orders o
INNER JOIN Customers c ON c.Id = o.CustomerId
WHERE o.Id = @OrderId;";
Free tools Windows power users keep installed
One-click scans. No signup required.
var order = connection.Query<Order, Customer, Order>(
sql,
(order, customer) =>
{
order.Customer = customer;
return order;
},
new { OrderId = 1001 },
splitOn: "Id"
).SingleOrDefault();
The type arguments in Query<Order, Customer, Order> mean: map the first part of the row to Order, map the second part to Customer, and return an Order. The lambda receives both objects and returns the final shape you want. This keeps the SQL efficient while still producing a useful object graph for application code.
Rank #3
- Easily store and access 1TB to content on the go with the Seagate Portable Drive, a USB external hard drive.Specific uses: Personal
- Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop. Reformatting may be required for Mac
- To get set up, connect the portable hard drive to a computer for automatic recognition no software required
- This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
- The available storage capacity may vary.
Using splitOn correctly
The splitOn parameter is the most common source of mapping problems. By default, Dapper splits on a column named Id. That works only when the second object starts at the first Id column after the first object’s columns. If both tables expose Id, it is often clearer to alias the split column so the boundary is explicit.
var sql = @"
SELECT
o.Id,
o.OrderDate,
o.Total,
c.Id AS CustomerId,
c.Name,
c.Email
FROM Orders o
INNER JOIN Customers c ON c.Id = o.CustomerId;";
var orders = connection.Query<Order, Customer, Order>(
sql,
(order, customer) =>
{
customer.Id = customer.CustomerId;
order.Customer = customer;
return order;
},
splitOn: "CustomerId"
).ToList();
In real projects, prefer model properties that match the selected column names, or use dedicated read models when aliases differ from domain entity property names. For example, an OrderWithCustomerRow or a DTO can be easier to maintain than forcing persistence aliases into domain classes.
Recommended Free Tools
Mapping more than two objects
Dapper can map several related objects from one row. For example, an order may include a customer and a shipping address. The same pattern applies: select columns in object order, provide a mapping function, and list the split columns in the order where each new object begins.
var sql = @"
SELECT
o.Id,
o.OrderDate,
o.Total,
c.Id AS CustomerId,
c.Name,
a.Id AS AddressId,
a.Line1,
a.City,
a.PostCode
FROM Orders o
JOIN Customers c ON c.Id = o.CustomerId
JOIN Addresses a ON a.Id = o.ShippingAddressId;";
var orders = connection.Query<Order, Customer, Address, Order>(
sql,
(order, customer, address) =>
{
order.Customer = customer;
order.ShippingAddress = address;
return order;
},
splitOn: "CustomerId,AddressId"
).ToList();
- Column order matters: place each object’s columns together in the select list.
- Aliases help: use names such as
CustomerIdandAddressIdto make split boundaries obvious. - Keep joins targeted: select only the columns needed for the mapped object graph.
- Use DTOs when useful: read models can prevent awkward aliases from leaking into core entities.
Multi-mapping is best suited for many-to-one or one-to-one joins, such as order-to-customer, invoice-to-billing-address, or product-to-category. For one-to-many joins, such as order-to-lines, the same parent row appears mulle times and needs an additional de-duplication pattern, which is covered in the next section.
Handling One-to-Many Relationships Without Duplicates
One-to-many mapping is where Dapper multi-mapping often needs extra care. A typical example is an Order with many OrderItem rows. A SQL join returns one row per item, so the order columns are repeated for every item. If you create a new Order object for each row, you end up with duplicate parent objects instead of one order containing a populated item collection.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesThe common pattern is to use a lookup dictionary keyed by the parent identifier. For each row, check whether the parent object has already been created. If it has not, add it to the dictionary and initialize its child collection. Then append the mapped child object to that collection. This keeps one parent instance in memory while still reading all joined rows from the database.
var sql = @"
SELECT
o.Id, o.OrderNumber, o.OrderDate,
i.Id, i.OrderId, i.ProductName, i.Quantity, i.UnitPrice
FROM Orders o
LEFT JOIN OrderItems i ON i.OrderId = o.Id
WHERE o.CustomerId = @CustomerId
ORDER BY o.Id, i.Id;";
var orderLookup = new Dictionary<int, Order>();
var orders = connection.Query<Order, OrderItem, Order>(
sql,
(order, item) =>
{
if (!orderLookup.TryGetValue(order.Id, out var existingOrder))
{
existingOrder = order;
existingOrder.Items = new List<OrderItem>();
orderLookup.Add(existingOrder.Id, existingOrder);
}
if (item != null && item.Id != 0)
{
existingOrder.Items.Add(item);
}
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now → return existingOrder;
},
new { CustomerId = customerId },
splitOn: "Id"
).Distinct().ToList();
The Distinct() call at the end removes repeated references returned by the mapping function, but the dictionary is what actually prevents duplicate parent construction. In many codebases, developers return orderLookup.Values.ToList() instead, which is clearer and avoids relying on reference equality.
var result = orderLookup.Values.ToList();
Using a Safer splitOn Column
When both tables have a column named Id, splitOn: "Id" can be ambiguous if the selected columns are not ordered carefully. A safer approach is to alias the child identifier and split on that alias. This makes the boundary between the parent and child mapping explicit.
var sql = @"
SELECT
o.Id, o.OrderNumber, o.OrderDate,
i.Id AS OrderItemId, i.OrderId, i.ProductName, i.Quantity, i.UnitPrice
FROM Orders o
LEFT JOIN OrderItems i ON i.OrderId = o.Id
WHERE o.CustomerId = @CustomerId;";
Rank #4
- Easily store and access 4TB of content on the go with the Seagate Portable Drive, a USB external hard drive.Specific uses: Personal
- Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop
- To get set up, connect the portable hard drive to a computer for automatic recognition no software required
- This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
- The available storage capacity may vary.
var orders = connection.Query<Order, OrderItem, Order>(
sql,
(order, item) =>
{
if (!orderLookup.TryGetValue(order.Id, out var existingOrder))
{
existingOrder = order;
existingOrder.Items = new List<OrderItem>();
orderLookup[order.Id] = existingOrder;
}
Recommended Free Tools
if (item != null && item.OrderItemId != 0)
{
existingOrder.Items.Add(item);
}
return existingOrder;
},
new { CustomerId = customerId },
splitOn: "OrderItemId"
);
- Use
LEFT JOINcarefully: if a parent has no children, Dapper may still create a child object with default values. Check the child identifier before adding it. - Initialize collections once: create
Items,Products, or similar lists only when the parent is first added to the lookup. - Order your selected columns: parent columns should appear first, followed by child columns, matching the
splitOnboundary. - Avoid duplicate children: if the query joins additional tables, the same child may appear more than once. Use a child-level lookup or check existing child IDs before adding.
For larger object graphs, such as Customer → Orders → OrderItems, apply the same dictionary pattern at each level. Keep one lookup for customers, another for orders, and add each item only after confirming its parent order exists. This keeps the model clean even when the SQL result contains repeated relational rows.
Combining QueryMultiple with Multi-Mapping
In real applications, QueryMulle and multi-mapping often work best together. QueryMultiple lets you return several result sets from one database round trip, while multi-mapping lets you shape rows that contain columns from joined tables into related objects. This is useful when loading a full aggregate, such as an order detail screen that needs the order header, customer, line items, products, and status history.
Consider a page that displays one order. The first result set can return the order joined to its customer, using multi-mapping to create an Order with a nested Customer. The second result set can return order items joined to products, again using multi-mapping. A third result set might return payment records or shipment events as a simple list. This pattern keeps each query focused and avoids building one huge join that repeats the same order and customer data across every row.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →const string sql = @"
select
o.Id, o.OrderNumber, o.OrderDate,
c.Id as CustomerId, c.Name, c.Email
from Orders o
inner join Customers c on c.Id = o.CustomerId
where o.Id = @OrderId;
select
oi.Id, oi.OrderId, oi.Quantity, oi.UnitPrice,
p.Id as ProductId, p.Name, p.Sku
from OrderItems oi
inner join Products p on p.Id = oi.ProductId
where oi.OrderId = @OrderId;
select
Id, OrderId, Status, ChangedAt
from OrderStatusHistory
where OrderId = @OrderId
order by ChangedAt;";
using var grid = connection.QueryMulle(sql, new { OrderId = orderId });
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
var order = grid.Read<Order, Customer, Order>(
(o, c) =>
{
o.Customer = c;
return o;
},
splitOn: "CustomerId"
).SingleOrDefault();
if (order is null)
{
return null;
}
var items = grid.Read<OrderItem, Product, OrderItem>(
(item, product) =>
{
item.Product = product;
return item;
},
splitOn: "ProductId"
).ToList();
var history = grid.Read<OrderStatusHistory>().ToList();
order.Items = items;
order.StatusHistory = history;
return order;
The column aliases in this example are deliberate. Dapper uses splitOn to decide where the next object begins in each row. If both joined tables expose an Id column, aliasing the second object’s identifier to CustomerId or ProductId makes the split point explicit. The model can still map correctly when property names match those aliases, or you can use custom mapping if your domain classes use different names.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11When this pattern is a good fit
- Detail screens: load a parent entity, related lists, and lookup data in one call.
- Aggregate roots: hydrate an object graph without issuing separate queries for each child collection.
- Read models: compose a response object from several targeted result sets.
- Performance-sensitive endpoints: reduce network latency while keeping SQL readable.
Be careful not to overuse this approach for unrelated data. A single QueryMulle call should usually represent one cohesive read operation. Also, read the result sets in the same order they appear in the SQL batch. Skipping or reading them out of order can produce confusing runtime behavior. If one result set is optional, still consume it with Read<T>() and handle an empty sequence rather than changing the read order conditionally.
Best Value
- [Upgraded Version] - This external hard drive features a mirrored logo stripe combined with a striped anti-slip design, and the rounded corners of the casing make it easier to grip. The stripes also have a heat dissipation function, ensuring stable and fast data transfer.
- 【Ultra-thin and quiet】 - The motherboard adopts JMicron 578 noise-free solution, giving you a quiet working environment. Lightweight and portable size designed to fit in your pocket for easy portability.
- 【Ultra-Fast Data Transfers】 - Pairing this external hard drive with JMicron 578 solution USB 3.0 and USB 2.0 interfaces enables blazing-fast data transfer. It boasts theoretical read speeds of up to 125MB/s and write speeds of up to 103MB/s.
- 【Plug and Play】 - With no software to install, just plug it in and the drive is ready to use.The hard disk chip is wrapped with an aluminum anti-interference layer to increase heat dissipation and protect data.
- 【What You Get】 - 1 x Portable Hard Drive, 1 x USB 3.0 Cable, 1 x User Manual, Gift-type shell packaging ,Three-year manufacturer's warranty and free technical support services.
For larger one-to-many graphs, combine this approach with the dictionary-based de-duplication pattern from multi-mapping. For example, if the first result set joins orders, customers, and addresses, use a lookup keyed by order id to prevent duplicate parent objects. Then use later result sets for child collections such as items, discounts, or audit records. This gives you a predictable structure: joined rows for closely related single objects, separate result sets for collections that would otherwise mully row counts.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Best Practices and Common Pitfalls
When working with Dapper’s QueryMulle and multi-mapping features, most issues come from mismatched assumptions between the SQL result shape and the C# object model. Dapper is intentionally lightweight, so it will not build a change tracker, infer relationship graphs automatically, or protect you from every column naming conflict. The best approach is to make your SQL explicit, keep your mapping code predictable, and test the exact result sets your application depends on.
Keep result sets intentional and ordered
QueryMulle reads result sets in the order returned by the SQL batch or stored procedure. If the first query returns orders, the first Read<T>() call must read orders. If the second query returns customers, the second read must map customers. Reordering SQL statements without updating the C# reads can produce empty collections, invalid casts, or objects filled with incorrect data.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitches- Read every result set in sequence: even if one result set is optional, your code should still account for it.
- Keep SQL batches small: returning five or six unrelated result sets from one call can become difficult to maintain.
- Prefer named methods: wrap each data access operation in a repository or query service method with a clear purpose.
Be precise with column aliases and split points
Multi-mapping depends heavily on column order and the splitOn parameter. By default, Dapper splits on a column named Id, which can be risky when several joined tables expose an Id column. Use explicit aliases such as CustomerId, OrderId, and ProductId, then set splitOn to match the first column of each subsequent mapped object.
For example, when mapping an Order with a related Customer, avoid selecting ambiguous columns like Id from both tables without aliases. A safer projection includes o.Id AS OrderId and c.Id AS CustomerId. Your model properties can either match those names directly, use custom type mapping, or project into intermediate DTOs before shaping the final domain object.
Avoid duplicates in one-to-many joins
A joined query that returns one order with five order items will repeat the order data five times. If you create a new parent object for every row, your final collection will contain duplicate parents. Use a lookup dictionary keyed by the parent identifier, add the parent only once, and append child records to the existing parent. This pattern is especially useful for orders and order items, blogs and posts, categories and products, or invoices and lines.
- Initialize child collections: make sure lists such as
Order.Itemsare never null before adding children. - Check for null children: left joins may produce a parent row with no child object.
- Deduplicate nested children: many-to-many joins can repeat the same child multiple times unless you check existing IDs.
Manage connections, transactions, and buffering carefully
Use using blocks or dependency-injected connection lifetimes so database connections are closed reliably. If several related reads must reflect the same state, execute them inside a transaction. This is common when loading a , detail rows, and audit entries that must be consistent with each other. Dapper supports passing the same transaction into Query, QueryMultiple, and other operations.
| Area | Common Pitfall | Better Practice |
|---|---|---|
| Result order | Reading result sets in a different order than the SQL returns them | Keep SQL and Read<T>() calls aligned |
| Multi-mapping | Relying on default Id split behavior |
Use explicit aliases and splitOn |
| One-to-many | Creating duplicate parent objects | Use a dictionary keyed by the parent ID |
| Performance | Loading large object graphs in a single query | Page results and load only required columns |
Finally, keep performance in mind. Dapper is fast, but it cannot compensate for inefficient SQL, missing indexes, excessive joins, or returning columns the application never uses. Select only the fields needed for the current use case, add indexes for join and filter columns, and consider pagination for large result sets. For complex screens, combining QueryMulle for separate collections with multi-mapping for focused relationships often produces clearer and faster data access code than trying to solve everything with one massive join.
Frequently Asked Questions
When should I use QueryMultiple instead of running separate Dapper queries?
Use QueryMulle when you need several related result sets for the same operation, such as an order, its customer, and its order lines. It reduces database round trips because all queries are sent in one command. Separate queries are still fine when the data is unrelated, conditionally loaded, or easier to cache independently.
How do I avoid duplicate parent objects when mapping one-to-many relationships?
Use a dictionary keyed by the parent ID, such as OrderId, and only create the parent object the first time you see it. For each row, add the child object to the existing parent’s collection if it is not already present. This pattern is commonly needed because SQL joins return one row per parent-child combination.
What does the splitOn parameter do in Dapper multi-mapping?
splitOn tells Dapper which column starts the next object in a joined result row. By default, Dapper looks for a column named Id, but that often fails when your query returns columns like CustomerId or ProductId. In those cases, set splitOn: "CustomerId,ProductId" or alias your columns clearly so Dapper knows where each mapped object begins.
Can I use QueryMultiple and multi-mapping together in the same query?
Yes, you can read one result set with regular mapping and another result set with multi-mapping from the same GridReader. For example, the first result set might contain an order header, while the second uses multi-mapping to map order lines with products. Make sure you read the result sets in the exact order they are returned by the SQL command.
What are the most common mistakes when using Dapper with multiple result sets?
Common mistakes include reading result sets out of order, forgetting to dispose the GridReader, using the wrong splitOn column, and not handling empty result sets. Another frequent issue is using joins for one-to-many data without de-duplicating the parent objects. Keep column names explicit in your SQL and test queries with realistic data, including parents with no children.
Bottom Line
Dapper gives you two powerful tools for efficient data access: QueryMulle for reading several result sets from one round trip, and multi-mapping for turning joined relational rows into meaningful object graphs. Used carefully, they help keep queries fast while still producing clean, usable models in your application code.
As a next step, practice both patterns with your own parent-child data, such as orders and order items or authors and books. Pay close attention to splitOn, duplicate parent objects, and result set ordering, since those are the areas where most mapping bugs appear.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




