Ant*_*lin 1 c# asp.net entity-framework-4 upshot knockout.js
我正在使用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应该抱怨这个模型的原因吗?
无论如何,我有什么选择来解决它?
谢谢你的帮助!
Ant*_*lin 15
简短的回答是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天内出现,你可能会想起一个包括以下步骤的开发过程:
这只是为了将数据导入系统.回到另一个方向,我们不得不以相反的顺序重复其中几个步骤.关键是数据库编码非常耗时(而且非常无聊).很明显,代码生成和其他工具的出现和简化.
真正的突破是NHibernate,实体框架4(代码优先方法)和其他类似的ORM工具,它们(几乎)完全从开发人员那里抽象出数据库.这些工具不仅提高了开发速度,而且提高了整体代码质量,因为它们错误地引入错误的机会较少.
现在,在许多应用程序中,由于大多数数据库细节都被隐藏起来,因此与数据库的连接和交互(几乎)是事后的想法.
微软还向Upshot.js和WebAPI提供了这两个工具,当它们相互结合使用时,将以与NHibernate和Entity Framework 4相同的方式彻底改变服务器和浏览器之间的通信.服务器和数据库之间.
这确实是一项非常有价值的成就 - 特别是当客户推动更多交互式Web应用程序时.
阻止开发人员将更多用户界面移动到(浏览器)客户端的主要障碍之一是需要大量编码.部分步骤包括:
这看起来非常像"似曾相识",因为它与将数据输入和输出数据库的遗留流程的复杂性非常相似.
根据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格式传输的对象,并公开关联的元数据,如对象属性,必填字段,字段长度,显示名称,描述等.(元数据是什么允许映射,可以访问以用于验证.)
注意:我仍然不确定如何访问upshot元数据并将其与jQuery验证或Knockout验证插件之一的验证框架联系起来.这是我的待办事项清单.
注意:我不确定支持哪种类型的元数据.这是我的待办事项清单.作为旁注,我还计划在System.ComponentModel.DataAnnotations之外试验元数据,以查看是否支持NHibernate属性以及自定义属性.
考虑到所有这一切,我开始使用Steve在他的演示中用于实际Web应用程序的相同技术集.其中包括:
期望所有这些技术能够很好地协同工作,因为a)它们是最新的Microsoft工具(除了开源Knockout),而且因为现在是微软的Steve Sanderson在主要的微软演示中一起使用它们来展示开发单页面应用程序.
不幸的是,我在实践中发现的是实体框架4.x和Upshot.js以非常不同的方式观察世界,它们的方向有点矛盾而不是互补.
如上所述,实体框架代码优先做了一个非常出色的工作,允许开发人员定义高度功能的对象模型,它几乎神奇地转换为功能数据库.
Entity Framework 4.x Code First的一个重要功能是能够从父对象导航到子对象并从子对象导航回其父对象.这些双向关联是EF的基石.它们节省了大量时间并大大简化了开发.此外,微软一再宣称这一功能是使用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<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的关联.互联网上有无数其他的例子出现在许多变体中.
因为这两种方式关联是实体框架的核心功能,所以合理的人可能会期望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]]'的对象图包含循环,如果参考跟踪则无法序列化被禁用."
我尝试了许多不同的方法来尝试解决问题.基于我的想法和我的阅读,这里有一些我尝试过的失败的解决方案.
"无法检索关联'KnockoutTest.Models.Client_Persons'的关联信息.仅支持包含外键信息的模型.有关创建包含外键信息的模型的详细信息,请参阅实体框架文档."
如果问题是系统对存在外键感到困惑的结果,我在子实体上明确指定了[ForeignKey]属性.一切都编译但.NET返回"类型的对象图...包含循环,无法序列化..."
我的一些阅读表明在WCF中添加像[DataContract(IsReference = true)]这样的属性可能会使.NET不会对周期性引用感到困惑.那是我得到这美丽的时候.
"类型'KnockoutTest.Models.Person'无法序列化为JSON,因为其IsReference设置为'True'.JSON格式不支持引用,因为没有用于表示引用的标准化格式.要启用序列化,请禁用IsReference设置类型或类型的适当父类."
此错误非常重要,因为它基本上告诉我们,我们不能在正常配置中一起使用Upshot和实体框架代码优先.为什么?实体框架旨在利用双向绑定.但是,当实现双向绑定时,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.