- 环境
FreeSql 版本: 3.5.310 (latest)
数据库: SQLite(推测所有支持批量 INSERT 的数据库均受影响)
.NET 版本: net10.0
- 描述
InsertDict 批量插入 List<Dictionary<string, object>> 时,如果第一条记录的 Guid? 属性为 null,后续同属性有值,则第一条的 null 值被错误地赋值为了 Guid.Empty。
- 复现步骤
// FreeSql InsertDict Bug: Guid? null 在批量插入中被污染为 Guid.Empty
//
// 环境: FreeSql 3.5.310 + SQLite in-memory
// 表现: 一批字典中,前几条 ParentId=null,后几条 ParentId=实际值,
// 查询结果中 null 被变成 Guid.Empty。
// 期望: null 保持为 null(数据库级别为 NULL)。
using FreeSql;
using FreeSql.DataAnnotations;
// 1. 创建 FreeSql 实例(SQLite 内存库)
var fsql = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, "Data Source=:memory:")
.UseAutoSyncStructure(true)
.Build();
fsql.CodeFirst.SyncStructure<TestMenu>();
// 2. 准备测试数据:混合 null 和非 null 的 ParentId
var rootId = Guid.NewGuid();
var child1Id = Guid.NewGuid();
var child2Id = Guid.NewGuid();
var dicts = new List<Dictionary<string, object>>
{
new() { ["Id"] = rootId, ["Label"] = "根目录", ["ParentId"] = null }, // 第一条: null
new() { ["Id"] = child1Id, ["Label"] = "子菜单1", ["ParentId"] = rootId }, // 第二条: 有值
new() { ["Id"] = child2Id, ["Label"] = "子菜单2", ["ParentId"] = rootId }, // 第三条: 有值
};
// 3. 用 InsertDict 批量写入
var rows = fsql.InsertDict(dicts).AsTable("test_menu").ExecuteAffrows();
Console.WriteLine($"批量插入影响行数: {rows}");
// 4. 查询验证
var all = fsql.Select<TestMenu>().ToList();
Console.WriteLine("\n查询结果:");
Console.WriteLine($"| {"Id",-36} | {"Label",-10} | {"ParentId",-36} |");
Console.WriteLine(new string('-', 90));
foreach (var m in all)
{
var parentStr = m.ParentId?.ToString() ?? "null";
Console.WriteLine($"| {m.Id} | {m.Label,-10} | {parentStr,-36} |");
}
// 5. 检查问题:第一条记录的 ParentId 应该是 null,实际可能是 Guid.Empty
var root = all.First(m => m.Label == "根目录");
bool hasBug = root.ParentId.HasValue && root.ParentId.Value == Guid.Empty;
Console.WriteLine($"\n--- 结果分析 ---");
Console.WriteLine($"根目录 ParentId: {(root.ParentId.HasValue ? root.ParentId.Value.ToString() : "null")}");
Console.WriteLine($"期望值: null");
if (hasBug)
{
Console.WriteLine("\n❌ BUG 确认: 第一条字典的 ParentId=null 被污染为 Guid.Empty!");
Console.WriteLine(" 根因: FreeSql InsertDict 在批量处理时,第一条的 Guid? null");
Console.WriteLine(" 被后续有值的 Guid 覆盖为默认值 Guid.Empty。");
}
else if (root.ParentId == null)
{
Console.WriteLine("\n✅ 表现正确: ParentId 保持为 null。");
}
else
{
Console.WriteLine($"\n⚠️ 其他异常: ParentId = {root.ParentId}");
}
// 6. 验证拆分后的行为
Console.WriteLine("\n--- 拆分验证 ---");
var topDicts = new List<Dictionary<string, object>>
{
new() { ["Id"] = Guid.NewGuid(), ["Label"] = "单独插入", ["ParentId"] = null },
};
var subDicts = new List<Dictionary<string, object>>
{
new() { ["Id"] = Guid.NewGuid(), ["Label"] = "子菜单X", ["ParentId"] = rootId },
};
fsql.InsertDict(topDicts).AsTable("test_menu").ExecuteAffrows();
fsql.InsertDict(subDicts).AsTable("test_menu").ExecuteAffrows();
var splittedRoot = fsql.Select<TestMenu>().Where(m => m.Label == "单独插入").ToOne();
Console.WriteLine($"拆分后插入 ParentId: {(splittedRoot.ParentId.HasValue ? splittedRoot.ParentId.Value.ToString() : "null")}");
Console.WriteLine($"期望值: null → {(splittedRoot.ParentId == null ? "✅ 正确" : "❌ 错误")}");
// 定义测试用的表结构(用实体类自动建表)
[Table(Name = "test_menu")]
public class TestMenu
{
[Column(IsPrimary = true)]
public Guid Id { get; set; }
public string Label { get; set; } = "";
public Guid? ParentId { get; set; }
}
- 复现项目
FreeSqlInsertDictNullBug.zip
FreeSql 版本: 3.5.310 (latest)
数据库: SQLite(推测所有支持批量 INSERT 的数据库均受影响)
.NET 版本: net10.0
InsertDict 批量插入 List<Dictionary<string, object>> 时,如果第一条记录的 Guid? 属性为 null,后续同属性有值,则第一条的 null 值被错误地赋值为了 Guid.Empty。
FreeSqlInsertDictNullBug.zip