
In a relational database, two tables can be related through primary and foreign key constraints. A very common relationship is One-to-Many, where each record of one table is related to zero or more records of another table. In Dapper, we execute a SQL JOIN query with the Dapper Query Method to retrieve records from two tables that are in a One-to-Many relationship.
Let’s now understand records retrieval process in Dapper based on tables that are in One-to-Many relationship.
We have 2 tables which are in One-to-Many relationship, these are:
Each employee in the Employee table can have zero or many addresses in the Address table. Similarly, many addresses can belong to a particular employee.
CREATE TABLE [dbo].[Employee] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] VARCHAR (50) NOT NULL,
[Email] VARCHAR (50) NOT NULL,
[Salary] MONEY NOT NULL,
PRIMARY KEY CLUSTERED ([Id] ASC)
);CREATE TABLE [dbo].[Address] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[EmployeeId] INT NOT NULL,
[HouseNo] INT NOT NULL,
[Street] VARCHAR (50) NOT NULL,
[City] VARCHAR (50) NOT NULL,
CONSTRAINT [PK_Address] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Address_Employee] FOREIGN KEY ([EmployeeId]) REFERENCES [dbo].[Employee] ([Id])
);The One-to-Many relationship between the 2 tables is shown below:

Through Dapper Query method we can read records from One-to-Many relationship based tables. We simply apply Inner Join sql query and then map the result to strongly typed objects.
Create 2 classes Employee.cs and Address.cs. To these classes the result set of the Inner Join query will be mapped.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Salary { get; set; }
public List<Address> Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public int HouseNo { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
Note that the Employee class has a List<Address> type property which will be filled with the multiple addresses an employee is having. Next, see the code of the Query method which executes an INNER JOIN query to fetch Employee and his/her related Address records.
//connection string
string myCS = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=DapperStart;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";
string sql = "SELECT * FROM Employee AS A INNER JOIN Address AS B ON A.Id = B.EmployeeID;";
using (var connection = new SqlConnection(myCS))
{
var dictionary = new Dictionary<int, Employee>();
var list = connection.Query<Employee, Address, Employee>(
sql,
(employee, address) =>
{
Employee e;
if (!dictionary.TryGetValue(employee.Id, out e))
{
e = employee;
e.Address = new List<Address>();
dictionary.Add(e.Id, e);
}
e.Address.Add(address);
return e;
},
splitOn: "Id")
.Distinct()
.ToList();
}Explanation: In this example, the input parameters of query method – Query are Employee and Address. The return type is Employee. So Query method will take an Employee and a Address, process them, and return an Employee.
Next, see the Func delegate where the processing of the resultset is done:
(employee, address) =>
{
Employee e;
if (!dictionary.TryGetValue(employee.Id, out e))
{
e = employee;
e.Address = new List<Address>();
dictionary.Add(e.Id, e);
}
e.Address.Add(address);
return e;
}We used a dictionary for going through each record of the result set. Then we are adding the list of Address to the “Address” property of the Employee. In the end we are returning the Employee object.
The splitOn argument tells to split the record set on the Id column which is the first column of the Address table.
Let’s now check the working of this code. We place a breakpoint and checked the value of the list variable. The first Employee record’s Address Property is filled with it’s two related Address records. See the below image.

This means the code is working properly.
Download source codes:
In this tutorial, we learned how to retrieve records from two tables that are in a One-to-Many relationship using the Dapper Query method. We executed a SQL JOIN query and mapped the result set to the Employee and Address classes. A dictionary made sure each employee was created only once, while all of the employee’s related addresses were added to its Addresses list. We also used the splitOn argument to tell Dapper where the Address columns begin in each row.
Remember that an INNER JOIN returns only the employees who have at least one address. To include employees with no addresses, use a LEFT JOIN and check the address for null inside the Func delegate.
Keep the following points in mind when working with One-to-Many relationships in Dapper:
It is a relationship in which one record of a table is linked to zero or more records of another table. For example, one employee can have many addresses. It is implemented with a foreign key in the child table that references the primary key of the parent table.
Dapper uses the Query method with a multi-mapping Func delegate. You run a JOIN query, and Dapper maps each row to the parent and child types. In the delegate, you add the child to the parent’s collection.
The JOIN query returns the same parent on several rows, one for each child. Without a dictionary, you would end up with duplicate parent objects, each holding a single child. The dictionary makes sure only one parent object exists per Id and collects all its children.
It tells Dapper where one object’s columns end and the next object’s columns begin in each row. By default, Dapper splits on a column named Id. If your child table’s first column has a different name, specify it with splitOn.
You are probably using an INNER JOIN, which returns only the employees who have at least one address. Switch to a LEFT JOIN to include employees without addresses, and handle the null Address in your code.
Yes. Dapper supports multi-mapping with up to seven types in a single Query call. Add each type as a generic argument and list the split columns in splitOn, separated by commas.
QueryMultiple executes several SELECT statements in one round trip and returns each result set separately. It is useful when the parent table has many columns or the child table has many rows, because the parent data is not repeated for every child row.
Dapper is generally faster because it is a lightweight micro-ORM with very little overhead. The trade-off is that you write the SQL and the mapping logic yourself, while EF Core generates them for you.