Newtonsoft Json 序列化和反序列化问题(检测到 属性 的自引用循环)

Newtonsoft Json Serialization and Deserialization Issue (Self referencing loop detected for property)

我在课程和学生实体之间建立了多对多关系 当我尝试获取选项的课程时发生了一些奇怪的行为

我试过寻找原因,但没有找到任何有用的东西。

关系是:- 1-课程可以有很多学生 2- 学生可以有很多课程

table是 1-课程 2- StudentCourse(以 CourseId 作为外键,StudentId 作为外键) 3- 学生

请注意这个人 table 有完全相同的情况但是,我没有遇到同样的问题

    public class Repository<TEntity>:IRepository<TEntity> where TEntity : class
    {
        protected readonly DbContext Context;

        public Repository(DbContext context)
        {
            Context = context;
        }
        public async Task<List<T>> GetAllAsync<T>(Expression<Func<TEntity, bool>> predicate)
        {
            var q = Context.Set<TEntity>().Where(predicate);
            var ret = await q.ToListAsync();
            List<T> result = JsonConvert.DeserializeObject<List<T>>(JsonConvert.SerializeObject(ret));
            return result;
        }
   }

导致异常的行

List<T> result = JsonConvert.DeserializeObject<List<T>>(JsonConvert.SerializeObject(ret));

这是堆栈跟踪。

at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CheckForCircularReference(JsonWriter writer, Object value, JsonProperty property, JsonContract contract, JsonContainerContract containerContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CalculatePropertyValues(JsonWriter writer, Object value, JsonContainerContract contract, JsonProperty member, JsonProperty property, JsonContract& memberContract, Object& memberValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonConvert.SerializeObjectInternal(Object value, Type type, JsonSerializer jsonSerializer)
   at Newtonsoft.Json.JsonConvert.SerializeObject(Object value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.SerializeObject(Object value)
   at ApheliateProgram.Data.DataAccessLayer.Repositories.Repository`1.<GetAllAsync>d__6`1.MoveNext()

我的建议是,要避免现在和将来出现这样的痛苦,请取消通用存储库。

此方法似乎是尝试使用延迟加载以通用方式使用序列化程序来获取整个对象图以执行深层复制,或者您正在偶然发现延迟加载序列化程序的副作用行为或碰巧有一些相关实体已经被 DbContext 预取,这会阻碍您的序列化。

您可以通过利用投影来填充视图模型以供使用来避免各种此类问题。然而,这不是通用兼容操作,但它使代码简单快速。

在我使用存储库的地方,我让它们传回 IQueryable<TEntity> 而不是 IEnumerable<TEntity>Task<IEnumerable<TEntity>>,因为这使调用者可以完全控制数据的使用方式,包括投影、分页、排序、附加过滤,以及调用应该是异步的还是同步的。这同样适用于我确实想使用实体(例如执行更新)的调用,调用者控制是否急切加载相关数据以及加载哪些相关数据。

为了帮助解释您在通用实施中遇到的问题:

通过使用可以获取学生记录的 _context.Set<Student>() 来加载学生数据。但是,学生记录包含对课程的引用,而课程包含对学生的引用。 (包括该学生的记录,相当于循环引用)

启用延迟加载后,序列化程序将“接触”课程并继续获取所有相关课程。然后,当它通过每门课程时,它将“接触”该课程的学生,然后对于每个学生,接触课程......您可以限制深度,但也必须考虑任何循环引用。即使没有 运行 错误,这也会变得非常昂贵,因为每次“触摸”都会导致另一个查询对数据库进行 运行。

即使禁用延迟加载,当您获取学生时,DbContext 也会查看它可能碰巧正在跟踪的所有课程,并会在返回时自动在学生中添加对这些课程的引用。如果您引用了指向同一个学生的课程,则序列化程序可以在查找循环引用时触发异常。这也会导致不完整且不可预测的相关数据被发送到您的视图/消费者,因为 DbContext 将填充它所知道的任何内容,这些数据可能是所有相关数据、一些相关数据或没有相关数据。

相反,如果我有一个 StudentRepository returns IQueryable<Student> 你会得到这样的东西:

public class StudentRepository
{
    public IQueryable<Student> GetStudents()
    {
        var query = Context.Students.AsQueryable();
        // or could use:
        //var query = Context.Set<Student>().AsQueryable();
        return query;
    }
}

您不需要谓词,因为无论如何调用者都在编写谓词。 过滤存储库可以做的是您要确保一致执行的低级别规则,例如如果您有软删除模型(即 IsActive):

    public IQueryable<Student> GetStudents(bool includeInactive = false)
    {
        var query = Context.Students.AsQueryable();
        if(!includeInactive)
            query = query.Where(x => x.IsActive);

        return query;
    }

这确保默认情况下只返回活跃的学生。同样可用于对当前用户应用所有权检查,以确保返回的数据仅是允许他们查看的数据。 (例如在多租户 SaaS 系统的情况下)

需要学生数据的来电者:

var students = StudentRepository.GetStudents()
    .Where(x => {insert where conditions})
    .OrderBy(...)
    ....

从这里我们可以 SkipTakeToListAnyCount 等以及使用 Include如果我们想与实体本身一起工作,则急切加载数据。所有这些都增加了 IEnumerable 支持的相当大的复杂性。我可以调用 ToList() 之类的同步方法或等待异步调用,而无需在存储库中加倍努力或强制所有内容都使用一个或另一个。

通过使用 IQueryable,与使用限制性更强的通用存储库模式相比,我们不会“泄露”领域知识,因为像谓词这样组合细节的行为需要领域知识和 EF 主义作为传递的谓词必须符合 EF 可以使用的内容。 (没有调用方法、访问非映射属性等)

从那里您可以利用 AutoMapper 的 ProjectTo 将 IQueryable 结果投影到 ViewModel 或 DTO,其中仅包含消费者需要的数据,可以安全地序列化这些数据,而不必担心循环引用或触发延迟加载。或者,可以使用 Select 手动完成投影。它避免了很多问题,并提供了显着的性能和资源利用率改进。

添加 ReferenceLoopHandling.Ignore 选项似乎可以解决问题。

JsonConvert.SerializeObject(ert, new JsonSerializerSettings{ ReferenceLoopHandling = ReferenceLoopHandling.Ignore})

感谢@Guru Stron