October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk18 min

Tutorial on Handling Multiple ResultSets and Multiple Mapping Using Dapper

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

dotnet add package Dapper

You also need a database provider package. For SQL Server, install:

#1 Best Overall
Seagate 2TB Portable Hard Drive | USB 3.0 (STGX2000400) | Everyday HDD storage. Plug-and-play backup. Windows & macOS. Adobe CC trial included. 1-Year Warranty
  • 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
);

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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();
}

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

public 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
Seagate Portable 5TB External Hard Drive HDD – USB 3.0 for PC, Mac, PS4, & Xbox - 1-Year Rescue Service (STGX5000400), Black
  • 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
);

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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();

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
Seagate Portable 1TB External Hard Drive HDD – USB 3.0 for PC, Mac, PlayStation, & Xbox, 1-Year Rescue Service (STGX1000400) , Black
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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 CustomerId and AddressId to 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The 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);
}

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
Seagate Portable 4TB External Hard Drive HDD – USB 3.0, 1-Year Rescue | Everyday HDD storage. Plug-and-play backup. Windows & macOS. Adobe CC trial included. 1-Year Warranty
  • 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;
}

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

if (item != null && item.OrderItemId != 0)
{
existingOrder.Items.Add(item);
}

return existingOrder;
},
new { CustomerId = customerId },
splitOn: "OrderItemId"
);

  • Use LEFT JOIN carefully: 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 splitOn boundary.
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When 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
Sale
UnionSine 500GB Ultra Slim Portable External Hard Drive HDD-USB 3.0 | for PC, Mac, Laptop, PS4, Xbox one, Xbox 360-(Black)
  • [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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.Items are 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quick Recap

Bestseller No. 1
Seagate 2TB Portable Hard Drive | USB 3.0 (STGX2000400) | Everyday HDD storage. Plug-and-play backup. Windows & macOS. Adobe CC trial included. 1-Year Warranty
Seagate 2TB Portable Hard Drive | USB 3.0 (STGX2000400) | Everyday HDD storage. Plug-and-play backup. Windows & macOS. Adobe CC trial included. 1-Year Warranty
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$129.99
Bestseller No. 2
Seagate Portable 5TB External Hard Drive HDD – USB 3.0 for PC, Mac, PS4, & Xbox - 1-Year Rescue Service (STGX5000400), Black
Seagate Portable 5TB External Hard Drive HDD – USB 3.0 for PC, Mac, PS4, & Xbox - 1-Year Rescue Service (STGX5000400), Black
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$219.99
Bestseller No. 3
Seagate Portable 1TB External Hard Drive HDD – USB 3.0 for PC, Mac, PlayStation, & Xbox, 1-Year Rescue Service (STGX1000400) , Black
Seagate Portable 1TB External Hard Drive HDD – USB 3.0 for PC, Mac, PlayStation, & Xbox, 1-Year Rescue Service (STGX1000400) , Black
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$119.80
Bestseller No. 4
Seagate Portable 4TB External Hard Drive HDD – USB 3.0, 1-Year Rescue | Everyday HDD storage. Plug-and-play backup. Windows & macOS. Adobe CC trial included. 1-Year Warranty
Seagate Portable 4TB External Hard Drive HDD – USB 3.0, 1-Year Rescue | Everyday HDD storage. Plug-and-play backup. Windows & macOS. Adobe CC trial included. 1-Year Warranty
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$189.99

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.