Dapper联表查询用ViewModel接收返回null、传object正常问题
Dapper 一对多JOIN查询映射嵌套ViewModel返回null问题
问题复现
定义的实体类
// Customer Model public class CustomerDto { public int ID { get; set; } public string? Name { get; set; } }
// Customer Contact Details public class ContactdetailsDto { public int ID { get; set; } public string? Name { get; set; } }
定义的ViewModel
public class CustomersViewModel { public CustomerDto? CustomerDto { get; set; } public List<CustomerContactDetailsDto>? CustomerContactDetailsDto { get; set; } }
问题查询代码
List<CustomersViewModel> customerData = await Task.FromResult(dapper.GetAll<CustomersViewModel >($"SELECT * FROM [dbo].[Customer] LEFT JOIN [Customer].[ContactDetails] ON [dbo].[Customer].[CompanyID] = [Customer].[ContactDetails].[CompanyID] WHERE [dbo].[Customer].[CompanyID] = '{companyID}'", null, commandType: CommandType.Text));
异常表现
- 上述代码返回的
CustomersViewModel所有属性值均为null - 将泛型参数替换为
object类型时,可以正常读取到Name等字段值 - 尝试将
SELECT *替换为显式指定两张表的Name字段,问题未解决 - 同SQL语句在SQL Studio中执行可正常返回结果
问题根因
- Dapper默认的单泛型参数映射逻辑,不会自动将JOIN查询返回的平铺结果集,拆分为嵌套子对象+子集合的层级结构。当传入
CustomersViewModel作为映射目标类型时,Dapper只会查找结果集中与CustomersViewModel一级属性名完全匹配的列,也就是需要存在名为CustomerDto、CustomerContactDetailsDto的列才会给这两个属性赋值。但JOIN返回的列是ID、Name、CompanyID这类业务字段,和一级属性名完全不匹配,自然所有属性都会映射为null。换成object时Dapper会返回动态ExpandoObject,直接把所有返回列作为动态对象的属性,所以可以正常读取字段值。 - 代码存在类型命名不一致问题:ViewModel中声明的集合元素类型为
CustomerContactDetailsDto,但实际定义的联系详情类名为ContactdetailsDto,即使映射逻辑正确,这里也会触发类型不匹配错误。 - 当前写法用字符串插值拼接SQL参数,存在明确的SQL注入风险。
修复方案
- 先统一实体命名,补全实体中缺失的关联字段,保证类型引用一致:
// 统一类名和ViewModel引用匹配 public class CustomerContactDetailsDto { public int ID { get; set; } public string? Name { get; set; } // 补全关联字段 public int CompanyID { get; set; } } public class CustomerDto { public int ID { get; set; } public string? Name { get; set; } // 补全关联字段 public int CompanyID { get; set; } }
- 使用Dapper原生的多映射(MultiMapping)功能手动实现一对多关系映射,替换原有的单泛型
GetAll调用,同时改用参数化查询避免注入风险:
// 编写参数化SQL,显式指定返回字段避免列名冲突 string sql = @"SELECT c.ID, c.Name, c.CompanyID, d.ID, d.Name, d.CompanyID FROM [dbo].[Customer] c LEFT JOIN [Customer].[ContactDetails] d ON c.CompanyID = d.CompanyID WHERE c.CompanyID = @CompanyID"; // 用字典缓存主客户对象,合并重复的客户行 var customerCache = new Dictionary<int, CustomersViewModel>(); // 调用多映射重载QueryAsync var mappedResult = await dbConnection.QueryAsync<CustomerDto, CustomerContactDetailsDto, CustomersViewModel>( sql, (customer, contact) => { // 缓存中不存在当前客户时,初始化ViewModel实例 if (!customerCache.TryGetValue(customer.ID, out var customerEntry)) { customerEntry = new CustomersViewModel { CustomerDto = customer, CustomerContactDetailsDto = new List<CustomerContactDetailsDto>() }; customerCache.Add(customer.ID, customerEntry); } // 关联的联系方式不为空时加入集合 if (contact != null) { customerEntry.CustomerContactDetailsDto.Add(contact); } return customerEntry; }, new { CompanyID = companyID }, // 传入参数,禁止字符串拼接SQL splitOn: "ID" // 指定拆分列:遇到第二个ID列时开始映射第二个实体ContactDetails ); // 去重后得到最终的一对多结果 List<CustomersViewModel> customerData = mappedResult.Distinct().ToList();
- 如果你使用的
GetAll是第三方封装的Dapper扩展方法,需要确认该方法是否支持嵌套对象/一对多自动映射,如果不支持直接使用Dapper原生的多映射API即可。
内容的提问来源于stack exchange,提问作者Midz Elwekil
相关产品推荐
相关产品推荐

