是什么让Entity Framework / Upshot相信我的对象图“包含周期”?

我正在使用Entity Framework 4.3(代码优先)测试Knockout 2.1.0和Upshot 1.0.0.2并遇到以下错误:

{“类型’System.Collections.Generic.HashSet`1 [[KnockoutTest.Models.Person,KnockoutTest,Version = 1.0.0.0,Culture = neutral,PublicKeyToken = null]]的对象图包含循环,如果引用则无法序列化跟踪已停用。“}

我使用一个相当典型的模型来测试一些基本的父子实体。

public class Client { public Client() { Projects = new HashSet(); Persons = new HashSet(); } [Key] public int ClientId { get; set; } [Required] [Display(Name = "Client Name", Description = "Client's name")] [StringLength(30)] public string Name { get; set; } public ICollection Projects { get; set; } public ICollection Persons { get; set; } } public class Project { public Project() { } [Key] public int ProjectId { get; set; } [StringLength(40)] public string Name { get; set; } public int? ClientId { get; set; } public virtual Client Client { get; set; } } public class Person { public Person() { PhoneNumbers=new HashSet(); } [Key] public int PersonId { get; set; } [Required] [Display(Name="First Name", Description = "Person's first name")] [StringLength(15)] public string FirstName { get; set; } [Required] [Display(Name = "First Name", Description = "Person's last name")] [StringLength(15)] public string LastName { get; set; } [ForeignKey("HomeAddress")] public int? HomeAddressId { get; set; } public Address HomeAddress { get; set; } [ForeignKey("OfficeAddress")] public int? OfficeAddressId { get; set; } public Address OfficeAddress { get; set; } public ICollection PhoneNumbers { get; set; } public int? ClientId { get; set; } public virtual Client Client { get; set; } } public class Address { [Key] public int AddressId { get; set; } [Required] [StringLength(60)] public string StreetAddress { get; set; } [Required] [DefaultValue("Laurel")] [StringLength(20)] public string City { get; set; } [Required] [DefaultValue("MS")] [StringLength(2)] public string State { get; set; } [Required] [StringLength(10)] public string ZipCode { get; set; } } public class PhoneNumber { public PhoneNumber() { } [Key] public int PhoneNumberId { get; set; } [Required] [Display(Name = "Phone Number", Description = "Person's phone number")] public string Number { get; set; } [Required] [Display(Name = "Phone Type", Description = "Type of phone")] [DefaultValue("Office")] public string PhoneType { get; set; } public int? PersonId { get; set; } public virtual Person Person { get; set; } } 

我的背景非常通用。

 public class KnockoutContext : DbContext { public DbSet Clients { get; set; } public DbSet Projects { get; set; } public DbSet Persons { get; set; } public DbSet
Addresses { get; set; } public DbSet PhoneNumbers { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { } }

我也有一些样本数据 – 尽管它不应该相关。

  protected override void Seed(KnockoutContext context) { base.Seed(context); context.Clients.Add(new Client { Name = "Muffed Up Manufacturing", Persons = new List { new Person {FirstName = "Jack", LastName = "Johnson", PhoneNumbers = new List { new PhoneNumber {Number="702-481-0283", PhoneType = "Office"}, new PhoneNumber {Number = "605-513-0381", PhoneType = "Home"} } }, new Person { FirstName = "Mary", LastName = "Maples", PhoneNumbers = new List { new PhoneNumber {Number="319-208-8181", PhoneType = "Office"}, new PhoneNumber {Number = "357-550-9888", PhoneType = "Home"} } }, new Person { FirstName = "Danny", LastName = "Doodley", PhoneNumbers = new List { new PhoneNumber {Number="637-090-5556", PhoneType = "Office"}, new PhoneNumber {Number = "218-876-7656", PhoneType = "Home"} } } }, Projects = new List { new Project {Name ="Muffed Up Assessment Project"}, new Project {Name ="New Product Design"}, new Project {Name ="Razor Thin Margins"}, new Project {Name ="Menial Managerial Support"} } } ); context.Clients.Add(new Client { Name = "Dings and Scrapes Carwash", Persons = new List { new Person {FirstName = "Fred", LastName = "Friday"}, new Person { FirstName = "Larry", LastName = "Lipstick" }, new Person { FirstName = "Kira", LastName = "Kwikwit" } }, Projects = new List { new Project {Name ="Wild and Crazy Wax Job"}, new Project {Name ="Pimp Ride Detailing"}, new Project {Name ="Saturday Night Special"}, new Project {Name ="Soapy Suds Extra"} } } ); IEnumerable p = context.GetValidationErrors(); if (p != null) { foreach (DbEntityValidationResult item in p) { Console.WriteLine(item.ValidationErrors); } } } } 

基本上,每当我尝试使用客户端,人员,项目等的“包含”时,我都会遇到与上面列出的类似的错误。

 namespace KnockoutTest.Controllers { public class ClientController : DbDataController { public IQueryable GetClients() { return DbContext.Clients.Include("Persons").OrderBy(o => o.Name); } } public class ProjectController : DbDataController { public IQueryable GetProjects() { return DbContext.Projects.OrderBy(o => o.Name); } } public class PersonController : DbDataController { public IQueryable GetPersons() { return DbContext.Persons.Include("Client").OrderBy(o => o.LastName); } } public class AddressController : DbDataController { public IQueryable
GetAddresses() { return DbContext.Addresses.OrderBy(o => o.ZipCode); } } public class PhoneNumberController : DbDataController { public IQueryable GetPhoneNumbers() { return DbContext.PhoneNumbers.OrderBy(o => o.Number); } } }

你能看出.NET应该抱怨这个模型的原因吗?

无论如何,我有什么选择来解决它?

谢谢你的帮助!

简短的回答是Steve Sanderson演示了Knockout,Upshot和Entity Framework 4.x Code-First来构建单页应用程序(虽然很棒!!!)可能有点误导。 乍看之下,这些工具的播放效果几乎没有那么好。 [Spoiler:我确实认为有一个合理的解决方法,但它涉及到微软领域以外的步伐。)

(对于史蒂夫精彩的单页应用程序(SPA)演示,请访问http://channel9.msdn.com/Events/TechDays/Techdays-2012-the-Netherlands/2159 。非常值得一看。)

在大多数Web应用程序中,我们在概念上需要以下列方式移动和操作数据:

数据源(通常是数据库) – > Web应用程序 – >浏览器客户端

浏览器客户端 – > Web应用程序 – >数据源(通常是数据库)

在过去,操纵数据以从中接收数据并将其传输到数据库是一个真正的噩梦。 如果你必须在.NET 1.0 / 1.1天内出现,你可能会想起一个包括以下步骤的开发过程:

  • 手动定义数据模型
  • 创建所有表,设置关系,手动定义索引和约束等。
  • 创建和测试存储过程以访问数据 – 通常手动指定要包含在每个过程中的每个字段。
  • 创建POCO(普通旧CLR对象)来保存数据
  • 用于打开与数据库的连接并迭代地递归返回的每个记录并将其映射到POCO对象的代码。

这只是为了将数据导入系统。 回到另一个方向,我们不得不以相反的顺序重复其中几个步骤。 关键是数据库编码非常耗时(而且非常无聊)。 很明显,代码生成和其他工具的出现和简化。

真正的突破是NHibernate,entity framework4(代码优先方法)和其他类似的ORM工具(几乎)完全从开发人员中抽象出数据库。 这些工具不仅提高了开发速度,而且提高了整体代码质量,因为它们错误地引入错误的机会较少。

现在,在许多应用程序中,由于大多数数据库细节都被隐藏起来,因此(几乎)是与数据库的连接和交互。

微软还向Upshot.js和WebAPI提供了这两个工具,当它们相互结合使用时,将以与NHibernate和Entity Framework 4相同的方式彻底改变服务器和浏览器之间的通信。服务器和数据库之间。

这确实是一项非常有价值的成就 – 特别是当客户推动更多交互式Web应用程序时。

阻止开发人员将更多用户界面移动到(浏览器)客户端的主要障碍之一是需要大量编码。 一些步骤包括:

  • 将数据传输到客户端(通常采用JSON格式)
  • 将.NET对象中的所有属性映射到JavaScript对象
  • 重新创建有关对象及其属性的所有元数据
  • 将该数据绑定到客户端浏览器中的元素
  • 监控变化
  • 修改后重新映射数据(用于发送回服务器)
  • 将数据传输回服务器

这看起来非常像“似曾相识”,因为它与将数据输入和输出数据库的遗留流程的复杂性非常相似。

根据Web应用程序的配置方式,一旦将数据返回到服务器,就可以将数据映射到实际的数据库对象。 (这种情况经常发生。)

这种服务器 – >客户端 – >服务器数据传输需要大量编码,并为意外挑战提供许多机会不要忘记调试JavaScript有多么有趣! (好吧,它现在比几年前更少痛苦,但它仍然不像在Visual Studio中调试C#代码那样开发人员友好。)

史蒂夫桑德森关于单页应用程序的演讲提供了一个截然不同(和更好)的解决方案。

我们的想法是,WebAPI,Upshot.js和Knockout能够无缝地向浏览器客户端提供数据并从中接收数据,同时提供高度交互的用户体验。 哇! 这不是让你想伸出手来拥抱某人吗?

虽然这个想法并不新鲜,但它是我认为在.NET中真正做到的第一批认真努力之一。

一旦数据通过WebAPI传递并到达客户端(通过Upshot),那么像Knockout这样的框架将能够使用数据并提供最先进的Web应用程序所需的高级交互性。 (虽然可能不会立即明确,但我所描述的应用程序主要不是通过加载“页面”来起作用,而是通过主要通过AJAX请求传递JSON格式的数据。)

任何削减所有这些编码的工具显然都会被开发人员社区迅速接受。

Upshot.js(RIA / JS的重命名和升级版本)应该可以处理上面列出的几个普通任务。 它应该是WebAPI和Knockout之间的粘合剂。 它旨在动态映射从.NET中以JSON或XML传输的对象,并为对象属性,必需字段,字段长度,显示名称,描述等事件公开关联的元数据。(元数据是什么允许映射,可以访问以用于validation。)

注意:我仍然不确定如何访问upshot元数据并将其绑定到validation框架,如jQueryvalidation或Knockoutvalidation插件之一。 这是我的待办事项清单。

注意:我不确定支持哪种类型的元数据。 这是我的待办事项清单。 作为旁注,我还计划在System.ComponentModel.DataAnnotations之外试验元数据,以查看是否支持NHibernate属性以及自定义属性。

因此,考虑到所有这些,我开始使用Steve在他的演示中用于实际Web应用程序的相同技术集。 其中包括:

  • entity framework4.3使用Code-First方法
  • 带有WebAPI的ASP.NET MVC4
  • Upshot.js
  • Knockout.js

期望所有这些技术能够很好地协同工作,因为a)它们是最新的Microsoft工具(除了开源Knockout),而且因为现在是Microsoft的Steve Sanderson在主要的微软演示中一起使用它们来演示开发单页面应用程序。

不幸的是,我在实践中发现的是entity framework4.x和Upshot.js以非常不同的方式观察世界,它们的方向有点矛盾而不是互补。

如上所述,entity framework代码优先做了一个非常出色的工作,允许开发人员定义高度function的对象模型,它几乎可以神奇地转换为function数据库。

Entity Framework 4.x Code First的一个重要function是能够从父对象导航到子对象并从子对象导航回其父对象。 这些双向关联是EF的基石。 它们节省了大量时间并大大简化了开发。 此外,微软一再宣称这项function是使用Entity Framework的重要理由。

在Scott Guthrie的博客文章( http://weblogs.asp.net/scottgu/archive/2010/07/16/code-first-development-with-entity-framework-4.aspx )中,他最初介绍并解释了EF 4 Code First方法,他用以下两个类演示了双向导航的概念:

 public class Dinner { public int DinnerID { get; set; } public string Title { get; set; } public DateTime EventDate { get; set; } public string Address { get; set; } public string HostedBy { get; set; } public virtual ICollection RSVPs { get; set; } } public class RSVP { public int RsvpID { get; set; } public int DinnerID { get; set; } public string AttendeeEmail { get; set; } public virtual Dinner Dinner { get; set; } } 

如您所见,Dinner包含与RSVP的关联,RSVP包含与Dinner的关联。 互联网上有无数其他的例子出现在许多变体中。

因为这两种方式关联是entity framework的核心function,所以合理的人可能会期望Microsoft在库(Upshot.js)中支持此function,它用于将数据从.NET服务器应用程序带到客户端。 如果不支持该function,那么这可能是他们想要分享的内容,因为它会显着地决定架构决策,并且最不喜欢使用任何正确设计的EF 4 Code First实现。

在我的测试代码中(在上面的原始问题中列出),我自然地假设支持正常的EF Code-Firstfunction(如双向绑定/导航),因为这是演示文稿似乎显示的内容。

但是,我立即开始收到令人讨厌的小运行时错误,如:

“类型’System.Collections.Generic.HashSet`1 [[KnockoutTest.Models.Person,KnockoutTest,Version = 1.0.0.0,Culture = neutral,PublicKeyToken = null]]’的对象图包含循环,如果参考跟踪则无法序列化被禁用。”

我尝试了许多不同的方法来尝试解决问题。 根据我的想法和我的阅读,这里有一些我尝试过的失败的解决方案。

  • 从关系的一方移除了关联。 这不是一个好的解决方案,因为能够在父母和孩子之间的每个方向导航非常方便。 (这可能就是为什么这些关联属性被称为导航属性。)从任何一方删除关系都有副作用。 当从父项中删除关系时,也会删除导航子项列表的function。 当从孩子身上移除关系时,.NET为我提供了另一个友好的错误。

“无法检索关联’KnockoutTest.Models.Client_Persons’的关联信息。仅支持包含外键信息的模型。有关创建包含外键信息的模型的详细信息,请参阅entity framework文档。”

  • 如果问题是系统对存在外键感到困惑的结果,我在子实体上明确指定了[ForeignKey]属性。 一切都编译但.NET返回“类型的对象图…包含循环,无法序列化…”

  • 我的一些阅读表明在WCF中添加类似[DataContract(IsReference = true)]的属性可能会使.NET不会对周期性引用感到困惑。 那是我得到这种美丽的时候。

“类型’KnockoutTest.Models.Person’无法序列化为JSON,因为其IsReference设置为’True’.JSON格式不支持引用,因为没有用于表示引用的标准化格式。要启用序列化,请禁用IsReference设置类型或类型的适当父类。“

此错误非常重要,因为它基本上告诉我们,我们不能在正常配置中一起使用Upshot和Entity Framework Code-First。 为什么? entity framework旨在利用双向绑定。 但是,当实现双向绑定时,Upshot说它无法处理循环引用。 管理循环引用时,Upshot基本上表示它无法处理父对象和子对象之间的引用,因为JSON不支持它。

当我看到史蒂夫的演示时,我回忆起他与客户和交付之间存在关系。 我决定回去看看他的对象模型。

 public class Customer { public int CustomerId { get; set; } public string Name { get; set; } public string Address { get; set; } } public class Delivery { // Primary key, and one-to-many relation with Customer public int DeliveryId { get; set; } public virtual int CustomerId { get; set; } public virtual Customer Customer { get; set; } // Properties for this delivery public string Description { get; set; } public bool IsDelivered { get; set; } // <-- This is what we're mainly interested in 

我们发现,史蒂夫的演示中他的关系只有一种方式,它将孩子与父母联系起来,而不是孩子的父母。

在这个演示中,它有点工作。 但是,在许多实际应用中,这种方法使得数据访问变得不切实际。 以我在原始问题中包含的演示场景为例。 我们有:

 Clients Projects Persons Addresses PhoneNumbers 

大多数开发人员,我认为不想从地址或电话号码开始查询。 他们希望能够选择客户端或项目或人员列表,然后导航到其后代列表。

我不是100%肯定不可能使用启用了双向绑定的实体,但我不知道任何可能仅使用Microsoft工具获得成功的配置。

幸运的是,我确实认为有一个解决方案(负责循环依赖问题),我打算在接下来的几天内测试。 那个解决方案是...... JSON.Net

JSON.Net支持循环依赖关系并维护对子对象的引用。 如果它按预期工作,它将处理我在测试中得到的两个错误。

一旦我测试过,我将在此处报告结果。

我认为史蒂夫的演示非常出色,我很喜欢他的演示。 我相信Knockout很棒。 我非常感谢微软的目标。 如果该工具存在值得注意的限制,我认为微软可能应该更加适应它们。

我并不是要过分批评微软(而且绝对不会批评史蒂夫)因为我认为他们做得非常好。 我喜欢结果的承诺,并期待看到它的发展方向。

我真的很想看到有人拿出结果并重新考虑它(以及WebAPI),以便它可以在不使用第三方工具的情况下与Entity Framework完全集成。

我不知道NHiberbnate是否存在类似的工具,但我希望甚至可以看到有人扩展upshot以与NHibernate集成(或开发类似的库)。 通过扩展,我主要谈论从NHibernate消费元数据。

当我测试JSON.Net时,我也计划测试NHibernate。

如果在关系的两边公开导航属性,您将获得一个循环。 例如, Client实例包含相关Project实例的集合,并且这些Project实例包含返回其主要Client实例=周期的导航属性。

解决方法是仅在关系的一侧使用导航属性,使用序列化可以解决开箱即用的循环或排除序列化一侧的导航属性(尝试使用IgnoreDataMemberAttribute标记它)。