1

我正在使用 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<Project>();
        Persons = new HashSet<Person>();
    }

    [Key]
    public int ClientId { get; set; }

    [Required]
    [Display(Name = "Client Name", Description = "Client's name")]
    [StringLength(30)]
    public string Name { get; set; }

    public ICollection<Project> Projects { get; set; }
    public ICollection<Person> 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<PhoneNumber>();    
    }

    [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<PhoneNumber> 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<Client> Clients { get; set; }
    public DbSet<Project> Projects { get; set; }
    public DbSet<Person> Persons { get; set; }
    public DbSet<Address> Addresses { get; set; }
    public DbSet<PhoneNumber> 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<Person> { 
                                            new Person {FirstName = "Jack", LastName = "Johnson",
                                                PhoneNumbers = new List<PhoneNumber>
                                                    {
                                                        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<PhoneNumber>
                                                    {
                                                        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<PhoneNumber>
                                                    {
                                                        new PhoneNumber {Number="637-090-5556", PhoneType = "Office"}, 
                                                        new PhoneNumber {Number = "218-876-7656", PhoneType = "Home"}
                                                    }
                                            }
                                        },
                                        Projects = new List<Project>
                                                       {
                                                           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<Person> { new Person {FirstName = "Fred", LastName = "Friday"},
                                            new Person { FirstName = "Larry", LastName = "Lipstick" },
                                            new Person { FirstName = "Kira", LastName = "Kwikwit" }
                                        },
                                        Projects = new List<Project>
                                                       {
                                                           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<DbEntityValidationResult> p = context.GetValidationErrors();

            if (p != null)
            {
                foreach (DbEntityValidationResult item in p)
                {
                    Console.WriteLine(item.ValidationErrors);
                }
            }
        }

    }

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

namespace KnockoutTest.Controllers
{

    public class ClientController : DbDataController<KnockoutTest.Models.KnockoutContext>
    {
        public IQueryable<Client> GetClients()
        {
            return DbContext.Clients.Include("Persons").OrderBy(o => o.Name);
        }
    }


    public class ProjectController : DbDataController<KnockoutTest.Models.KnockoutContext>
    {
        public IQueryable<Project> GetProjects()
        {
            return DbContext.Projects.OrderBy(o => o.Name);
        }
    }


    public class PersonController : DbDataController<KnockoutTest.Models.KnockoutContext>
    {
        public IQueryable<Person> GetPersons()
        {
            return DbContext.Persons.Include("Client").OrderBy(o => o.LastName);
        }
    }

    public class AddressController : DbDataController<KnockoutTest.Models.KnockoutContext>
    {
        public IQueryable<Address> GetAddresses()
        {
            return DbContext.Addresses.OrderBy(o => o.ZipCode);
        }
    }

    public class PhoneNumberController : DbDataController<KnockoutTest.Models.KnockoutContext>
    {
        public IQueryable<PhoneNumber> GetPhoneNumbers()
        {
            return DbContext.PhoneNumbers.OrderBy(o => o.Number);
        }
    }
}

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

无论如何,我必须解决哪些选项?

感谢您的任何帮助!

4

2 回答 2

15

简短的回答是,Steve Sanderson 对 Knockout、Upshot 和 Entity Framework 4.x Code-First 构建单页应用程序的演示(虽然很棒!!!)可能有点误导。这些工具并没有像乍一看那样完美地结合在一起。[剧透:我确实相信有一个合理的解决方法,但它涉及稍微走出微软领域。]

(关于 Steve 精彩的单页应用程序 (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 Framework 4(代码优先方法)和其他类似的 ORM 工具,它们(几乎)完全从开发人员那里抽象出数据库。这些工具不仅提高了开发速度,而且还提高了整体代码质量,因为它们错误地引入错误的机会更少。

现在,在许多应用程序中,与数据库的连接和交互(几乎)是事后才想到的,因为大多数数据库细节都被隐藏起来了。

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

这确实是一项非常有价值的成就——尤其是当客户正在推动更具交互性的 Web 应用程序时。

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

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

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

根据 Web 应用程序的配置方式,在数据返回服务器后将数据映射到实际的数据库对象可能会带来更多乐趣。(这种情况通常会发生。)

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

Steve Sanderson 关于单页应用程序的演讲提供了一个完全不同(并且更好)的解决方案。

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

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

一旦数据通过 WebAPI 传递并到达客户端(通过 Upshot),那么像 Knockout 这样的框架将能够使用数据并提供尖端 Web 应用程序所需的非常高水平的交互性。(虽然可能不是很清楚,但我所描述的应用程序主要不是通过加载“页面”来运行,而是主要通过 AJAX 请求传递 JSON 格式的数据。)

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

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

注意:我仍然不确定如何访问结果元数据并将其绑定到一个验证框架,如 jQuery 验证或 Knockout 验证插件之一。这是我要测试的待办事项列表。

注意:我不确定这些类型的元数据中的哪一种受支持。这是我要测试的待办事项列表。作为旁注,我还计划在 System.ComponentModel.DataAnnotations 之外尝试使用元数据,以查看是否支持 NHibernate 属性以及自定义属性。

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

  • Entity Framework 4.3 使用 Code-First 方法
  • 带有 WebAPI 的 ASP.NET MVC4
  • Upshot.js
  • 淘汰赛.js

期望所有这些技术能够很好地协同工作,因为 a) 它们是最新的 Microsoft 工具(开源 Knockout 除外),并且因为现任 Microsoft 的史蒂夫·桑德森 (Steve Sanderson) 在主要的 Microsoft 演示文稿中一起使用了它们来展示开发单页应用程序。

不幸的是,我在实践中发现 Entity Framework 4.x 和 Upshot.js 以非常不同的方式看待世界,并且它们的方向有些矛盾而不是互补。

如前所述,Entity Framework Code First 做得非常出色,它允许开发人员定义功能强大的对象模型,它几乎神奇地转换为功能数据库。

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

在 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<RSVP> 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 的核心功能,所以有理智的人可能会期望 Microsoft 会在库 (Upshot.js) 中支持此功能,它用于将数据从 .NET 服务器应用程序带到客户端。如果该功能不受支持,那么他们可能希望共享该功能,因为这将非常关键的架构决策,并且最不希望与任何设计合理的 EF 4 Code First 实现一起使用。

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

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

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

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

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

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

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

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

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

这个错误非常重要,因为它基本上告诉我们,我们不能在正常配置中同时使用 Upshot 和 Entity Framework Code-First。为什么?实体框架旨在利用双向绑定。然而,当实现双向绑定时,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 支持循环依赖并维护对子对象的引用。如果它按预期工作,它将解决我在测试中遇到的两个错误。

一旦我测试过,我会在这里报告结果。

我认为史蒂夫的演示很棒,我喜欢他的演示。我相信淘汰赛是惊人的。我非常感谢微软的发展方向。如果该工具有值得注意的限制,我认为微软可能应该更愿意接受它们。

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

我真的很想看到有人接受并重构它(和 WebAPI),以便它可以在不使用第三方工具的情况下与 Entity Framework 完全集成。

我不知道 NHibernate 是否存在类似的工具,但我什至希望看到有人扩展结果以与 NHibernate 集成(或为 NHibernate 开发类似的库)。通过扩展,我主要是在谈论使用 NHibernate 的元数据。

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

于 2012-05-25T01:59:06.287 回答
2

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

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

于 2012-05-23T07:40:03.397 回答