You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C#中避免Mock DbSet<TData>的引用问题?

解决Mock DbContext/DbSet时修改查询实体导致原数据自动更新的问题

当前你的Mock实现直接返回原始List中实体的引用,所以通过FirstOrDefault查询到的对象就是原集合里的实例,修改它的属性自然会同步到Mock DbSet对应的原数据集合里,这和真实EF Core的行为不符(真实场景下,DbContext会跟踪实体,但只有调用SaveChanges才会持久化到数据库,而内存中的实体虽然会被修改,但如果你需要模拟"未调用Update/SaveChanges时原数据不变"的场景,就需要调整Mock逻辑)。

方案一:返回实体副本(简单场景)

如果只需要避免修改查询到的实体影响原集合,最直接的方式是让每次查询都返回实体的深拷贝,而不是原引用。

修改你的GetMock方法中GetEnumerator的Setup逻辑,添加克隆步骤:

public Mock<TContext> GetMock<TData, TContext>(List<TData> data,
        Expression<Func<TContext, DbSet<TData>>> dbSetSelectionExpression) where TData : class where TContext : DbContext
{
    var queryableData = data.AsQueryable();
    var stubSet = new Mock<DbSet<TData>>();
    var stubContext = new Mock<TContext>();
    
    stubSet.As<IQueryable<TData>>().Setup(s => s.Provider).Returns(queryableData.Provider);
    stubSet.As<IQueryable<TData>>().Setup(s => s.Expression).Returns(queryableData.Expression);
    stubSet.As<IQueryable<TData>>().Setup(s => s.ElementType).Returns(queryableData.ElementType);
    
    // 修改这里:返回每个实体的副本
    stubSet.As<IQueryable<TData>>().Setup(s => s.GetEnumerator())
           .Returns(() => queryableData.Select(item => Clone(item)).GetEnumerator());        
    
    stubContext.Setup(dbSetSelectionExpression).Returns(stubSet.Object);
    return stubContext;
}

// 实现深拷贝方法(示例用Json序列化,适合大多数简单实体)
private TData Clone<TData>(TData item) where TData : class
{
    var json = System.Text.Json.JsonSerializer.Serialize(item);
    return System.Text.Json.JsonSerializer.Deserialize<TData>(json);
}

这样每次查询都会生成新的实体实例,修改查询到的对象不会影响原集合中的数据。

方案二:模拟EF Core跟踪机制(贴近真实场景)

如果需要模拟真实EF Core的Update/SaveChanges行为(即只有调用Update后才同步到原数据),可以维护一个跟踪集合,记录被查询的实体,仅在调用Update时更新原数据。

修改后的GetMock方法如下:

public Mock<TContext> GetMock<TData, TContext>(List<TData> data,
        Expression<Func<TContext, DbSet<TData>>> dbSetSelectionExpression) 
        where TData : class 
        where TContext : DbContext
{
    // 保存原始数据作为"数据库"存储
    var originalData = new List<TData>(data);
    // 跟踪被查询的实体(假设实体有Id属性作为主键)
    var trackedEntities = new Dictionary<object, TData>();

    var stubSet = new Mock<DbSet<TData>>();
    var stubContext = new Mock<TContext>();

    // 配置查询逻辑:返回实体副本并跟踪
    stubSet.As<IQueryable<TData>>().Setup(s => s.Provider).Returns(originalData.AsQueryable().Provider);
    stubSet.As<IQueryable<TData>>().Setup(s => s.Expression).Returns(originalData.AsQueryable().Expression);
    stubSet.As<IQueryable<TData>>().Setup(s => s.ElementType).Returns(originalData.AsQueryable().ElementType);
    stubSet.As<IQueryable<TData>>().Setup(s => s.GetEnumerator())
           .Returns(() => originalData.Select(item => 
           {
               var clone = Clone(item);
               // 获取实体主键(这里假设主键名为Id,可根据实际调整)
               var primaryKey = typeof(TData).GetProperty("Id")?.GetValue(clone);
               if (primaryKey != null)
               {
                   trackedEntities[primaryKey] = clone;
               }
               return clone;
           }).GetEnumerator());

    // 模拟DbSet.Update方法:仅当调用Update时,才同步修改到原始数据
    stubSet.Setup(s => s.Update(It.IsAny<TData>()))
           .Callback<TData>(updatedEntity => 
           {
               var primaryKey = typeof(TData).GetProperty("Id")?.GetValue(updatedEntity);
               if (primaryKey == null || !trackedEntities.ContainsKey(primaryKey))
                   return;

               // 找到原始数据中对应的实体
               var originalEntity = originalData.FirstOrDefault(e => 
                   typeof(TData).GetProperty("Id")?.GetValue(e).Equals(primaryKey) == true);
               if (originalEntity == null)
                   return;

               // 将修改后的属性值同步到原始实体
               foreach (var prop in typeof(TData).GetProperties()
                   .Where(p => p.CanRead && p.CanWrite && p.Name != "Id"))
               {
                   prop.SetValue(originalEntity, prop.GetValue(updatedEntity));
               }
           });

    stubContext.Setup(dbSetSelectionExpression).Returns(stubSet.Object);
    return stubContext;
}

// 复用之前的Clone方法
private TData Clone<TData>(TData item) where TData : class
{
    var json = System.Text.Json.JsonSerializer.Serialize(item);
    return System.Text.Json.JsonSerializer.Deserialize<TData>(json);
}

使用这个方案时:

  • 查询到的实体是副本,修改它不会影响原集合
  • 只有调用dbContextMock.Object.MyEntity.Update(data)后,原集合中的数据才会被更新
  • 更贴近真实EF Core的工作流程

注意事项

  • 如果实体包含复杂嵌套对象,Json序列化的克隆方式可能无法处理循环引用,此时可以改用AutoMapper配置深拷贝映射,或者手动实现实体的克隆方法。
  • 如果实体的主键不是Id,需要修改代码中获取主键的逻辑,比如通过反射自定义主键属性名。

内容的提问来源于stack exchange,提问作者Ashok Ananthan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 23:05:04