实体框架检测到自引用循环。

116

我遇到了一个奇怪的错误。我正在尝试使用.NET 4.5 Web API、Entity Framework和MS SQL Server进行实验。我已经创建了数据库并设置了正确的主键、外键和关系。

我创建了一个 .edmx 模型,并导入了两个表:Employee 和 Department。一个部门可以有很多员工,这种关系是存在的。我使用脚手架选项创建了一个名为 EmployeeController 的新控制器,用 Entity Framework 创建了一个具有读/写操作的 API 控制器。在向导中,我选择了 Employee 作为模型和数据上下文的正确实体。

生成的方法看起来像这样:

public IEnumerable<Employee> GetEmployees()
{
    var employees = db.Employees.Include(e => e.Department);
    return employees.AsEnumerable();
}

当我通过 /api/Employee 调用我的API时,出现以下错误:

“ObjectContent`1”类型无法序列化响应正文,其内容类型为 “application/json; ...System.InvalidOperationException”,“StackTrace”:null,“InnerException”:{"Message":"An error has occurred.","ExceptionMessage":"检测到自引用循环,类型为 'System.Data.Entity.DynamicProxies.Employee_5D80AD978BC68A1D8BD675852F94E8B550F4CB150ADB8649E8998B7F95422552'。路径 '[0].Department.Employees'。","ExceptionType":"Newtonsoft.Json.JsonSerializationException","StackTrace":" ...

为什么会出现 [0].Department.Employees 的自引用循环?这看起来并不合理。如果我的数据库中存在循环引用,我会期望发生这种情况,但这只是一个非常简单的例子。可能出了什么问题?

13个回答

191

对于基于Json.net的默认Json格式化程序,正确的答案是将ReferenceLoopHandling设置为Ignore

只需在Global.asax中的Application_Start中添加此代码:

HttpConfiguration config = GlobalConfiguration.Configuration;

config.Formatters.JsonFormatter
            .SerializerSettings
            .ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;

这是正确的方法。它将忽略指向对象的引用。

其他回答集中在通过排除数据或制作外观对象来改变返回的列表,有时这并不是一个选项。

使用JsonIgnore属性限制引用可能会耗费时间,并且如果您想从另一个点开始序列化树,那将是一个问题。


变量config = GlobalConfiguration.Configuration.Formatters.JsonFormatter? - Motlicek Petr
1
HttpConfiguration config = GlobalConfiguration.Configuration; //System.Web.HttpHttpConfiguration config = GlobalConfiguration.Configuration; //System.Web.Http - Pedro Figueiredo
16
无效,异常仍然发生。 - Doug Beard
Doug Beard,你能给我关于你课程的任何信息吗? - Pedro Figueiredo
2
尽管我在Application_Start中有上述代码并且在相关属性上有[JsonIgnore],但我仍然遇到了自引用循环错误。我还在这些属性上使用了[IgnoreDataMember],并且为实体框架设置了ProxyCreationEnabledfalse - Brian Gradin
显示剩余2条评论

56

出现这种情况是因为您试图直接序列化EF对象集合。由于部门与员工有关联,而员工又与部门有关联,JSON序列化程序将无限循环读取d.Employee.Departments.Employee.Departments等等...

要在序列化之前解决此问题,请创建一个带有所需属性的匿名类型

示例(伪代码):

departments.select(dep => new { 
    dep.Id, 
    Employee = new { 
        dep.Employee.Id, dep.Employee.Name 
    }
});

在这种情况下,我会使用一个模型类,例如 departments.select(dep => new { dep.Id, Employee = new ModelClass { Id = dep.Employee.Id, Name = dep.Employee.Name } }); - sam starobin
@AtulChaudhary 这取决于您想要什么。如果您只是想通过 AJAX 从 SPA 消耗服务,那么这就足够了,如果您正在执行 MVVM,则我会说更好,因为它允许您在服务器上指定 ViewModel。如果您想将此服务用作 N-Tier API 的一部分,并由其他 .net 服务使用,则另一个答案更合适,因为它保持数据库合同完整(即,您仍将发送有资格的 EF 实体)。 - Nicolas Straub

36

我曾经遇到过相同的问题,后来发现只需将[JsonIgnore]属性应用于您不想进行序列化的导航属性即可。这仍然会序列化父实体和子实体,但可以避免自引用循环问题。


21

我知道这个问题已经很久了,但它仍然很受欢迎,而且我看不到任何ASP.net Core的解决方案。

ASP.net Core 中,你需要在Startup.cs文件中添加新的JsonOutputFormatter

    public void ConfigureServices(IServiceCollection services)
    {

        services.AddMvc(options =>
        {
            options.OutputFormatters.Clear();
            options.OutputFormatters.Add(new JsonOutputFormatter(new JsonSerializerSettings()
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            }, ArrayPool<char>.Shared));
        });

        //...
    }

实施后,JSON序列化程序将简单地忽略循环引用。这意味着它将返回null,而不是无限加载相互引用的对象。

如果没有上述解决方案,则会出现:

var employees = db.Employees.ToList();

将会加载员工和他们相关的部门

设置ReferenceLoopHandlingIgnore后,除非在查询中包含它,否则部门将被设置为null:

var employees = db.Employees.Include(e => e.Department);

还要注意的是,如果不想清除所有OutputFormatters,可以尝试删除此行:

options.OutputFormatters.Clear();

但是在我的情况下,移除它会出现self referencing loop异常。


9
主要问题是序列化一个与其他实体模型(外键关系)有关联的实体模型。这种关系会导致自我引用,从而在将其序列化为JSON或XML时抛出异常。 有很多选项。通过使用自定义模型而不是序列化实体模型来映射对象数据或值到自定义模型中(AutomapperValueinjector),然后返回请求,就可以避免其他任何问题进行序列化。 或者你可以先禁用实体模型中的代理,然后再对实体模型进行序列化。
public class LabEntities : DbContext
{
   public LabEntities()
   {
      Configuration.ProxyCreationEnabled = false;
   }

为了在XML中保留对象引用,你有两个选项。更简单的选择是向你的模型类添加[DataContract(IsReference=true)]。IsReference参数启用对象引用。请记住,DataContract使序列化成为一种选择,所以你还需要在属性上添加DataMember属性。
[DataContract(IsReference=true)]
public partial class Employee
{
   [DataMember]
   string dfsd{get;set;}
   [DataMember]
   string dfsd{get;set;}
   //exclude  the relation without giving datamember tag
   List<Department> Departments{get;set;}
}

在 global.asax 中,以 Json 格式
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = 
    Newtonsoft.Json.PreserveReferencesHandling.All;

以XML格式
var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
var dcs = new DataContractSerializer(typeof(Employee), null, int.MaxValue, 
    false, /* preserveObjectReferences: */ true, null);
xml.SetSerializer<Employee>(dcs);

选项1使用DataContract和DataMember属性可以正常工作,但是在结果的末尾我得到了“Employees”:[{"$ref":"1"}]}]。它已经消除了症状,但问题仍然存在。肯定其他人也会遇到完全相同的问题。我是否错误地接近了这个解决方案? - Lydon
我在global.asax文件中将ProxyCreationEnabled设置为false,然后那个jason东西就能正常工作了。太棒了,谢谢! - nahaelem

8
在你的上下文模型部分类定义的构造函数中添加一行 Configuration.ProxyCreationEnabled = false;
    public partial class YourDbContextModelName : DbContext
{
    public YourDbContextModelName()
        : base("name=YourDbContextConn_StringName")
    {
        Configuration.ProxyCreationEnabled = false;//this is line to be added
    }

    public virtual DbSet<Employee> Employees{ get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
    }
}

这个在2017年对我有效,其他的解决方案都不起作用。 - Richard Aguirre

8
消息错误意味着您有一个自引用循环。
您生成的JSON示例类似于以下示例(其中包含一个员工列表):
[
employee1 : {
    name: "name",
    department : {
        name: "departmentName",
        employees : [
            employee1 : {
                name: "name",
                department : {
                    name: "departmentName",
                    employees : [
                        employee1 : {
                            name: "name",
                            department : {
                                and again and again....
                            }
                    ]
                }
            }
        ]
    }
}

当您请求某些内容时,您必须告诉数据库上下文,您不想获取所有关联实体。DbContext的选项是Configuration.LazyLoadingEnabled

我发现最好的方法是为序列化创建一个上下文:

public class SerializerContext : LabEntities 
{
    public SerializerContext()
    {
        this.Configuration.LazyLoadingEnabled = false;
    }
}

6
我只想使用一个模型,所以最终我得到了以下代码:
var JsonImageModel = Newtonsoft.Json.JsonConvert.SerializeObject(Images, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore });

6
如果你想要在 Blazor(ASP.NET Core Hosted)模板中更改此设置,则需要在 Server 项目的 Startup.cs 中向 AddNewtonsoftJson 方法传入以下内容:
services.AddMvc().AddNewtonsoftJson(options => 
    options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore
);

6

我在我的.Net Core网站上遇到了同样的问题。

被接受的答案对我没用,但我发现将ReferenceLoopHandling.Ignore和PreserveReferencesHandling.Objects结合起来使用可以解决这个问题。

//serialize item
var serializedItem = JsonConvert.SerializeObject(data, Formatting.Indented, 
new JsonSerializerSettings
{
     PreserveReferencesHandling = PreserveReferencesHandling.Objects,
     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});

当我拥有一对一关系时,从模型中可以看出从属实体没有外键,只有父对象。使用 ReferenceLoopHandling.Ignore 会在序列化时忽略子级内的父对象。 - Bercovici Adrian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接