Вопрос по entity-framework-4, asp.net, c# – Что заставило бы Entity Framework / Upshot верить, что мой граф объектов «содержит циклы»?

1

Я тестирую Knockout 2.1.0 и Upshot 1.0.0.2 с Entity Framework 4.3 (Code-First) и сталкиваюсь со следующей ошибкой:

{"Object graph for type 'System.Collections.Generic.HashSet`1[[KnockoutTest.Models.Person, KnockoutTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' contains cycles and cannot be serialized if reference tracking is disabled."}

Я использую довольно типичную модель для тестирования с некоторыми базовыми родительско-дочерними сущностями.

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);
                }
            }
        }

    }

По сути, всякий раз, когда я пытаюсь использовать & quot; Включить & quot; от Клиента, Человека, Проекта и т. д. Я получаю ошибку, аналогичную приведенной выше.

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 должен жаловаться на эту модель?

Независимо от того, какие варианты я должен обойти это?

Спасибо за любую помощь!

Person содержитPhoneNumber который содержитPerson. Dour High Arch
@DourHighArch, спасибо! Предполагается, что целью свойства Person в PhoneNumber является «scaffolding». Моя реализация неверна? Anthony Gatlin
Что вы подразумеваете под словом "строительные леса"? Такой дизайн позволяет, например, Джону Смиту иметь телефонный номер 123-4567, а телефонный номер 123-4567 - для человека Мэри Джонс. Это то, что вы хотите? Если это так, я предлагаю использовать промежуточную таблицу. Если это не то, что вы хотите, вы можете узнать, с каких людей данный номер телефонаPerson. Dour High Arch

Ваш Ответ

2   ответа
2

вы получите цикл. НапримерClient экземпляр содержит коллекцию связанныхProject случаи и теProject экземпляры содержат свойство навигации обратно к своему основномуClient экземпляр = цикл.

Обходной путь - либо использовать свойство навигации только на одной стороне отношения, используя сериализацию, которая может разрешить циклы из коробки, либо исключить свойство навигации на одной стороне из сериализации (попробуйте пометить его какIgnoreDataMemberAttribute).

Большое спасибо! Я проверю это, как только доберусь до офиса, но я ожидаю, что вы совершенно правы. Это кажется очень странным, потому что кажется, что рекомендуемый подход к Entity Framework Code First заключается в создании ассоциаций на обеих сторонах отношений. В то же время Upshot, который Microsoft написала для работы с EF Code-First, не любит многонаправленную ассоциацию. Еще раз большое спасибо, что нашли время ответить на этот вопрос для меня! Anthony Gatlin
14

что демонстрация Стивом Сандерсоном Knockout, Upshot и Entity Framework 4.x Code-First для создания одностраничного приложения была (хотя и хороша !!!), возможно, немного вводящей в заблуждение. Эти инструменты не так хорошо сочетаются друг с другом, как кажется на первый взгляд. [Спойлер: Я действительно считаю, что есть разумный обходной путь, но он предполагает выход за пределы арены Microsoft очень-чуть.]

(Для фантастической презентации Стивом одностраничного приложения (SPA), пожалуйста, посетитеhttp://channel9.msdn.com/Events/TechDays/Techdays-2012-the-Netherlands/2159, Это стоит посмотреть.)

В большинстве любых веб-приложений нам концептуально необходимо перемещать и манипулировать данными следующим образом:

Data Source (often a Database) -> Web Application -> Browser Client

А ТАКЖЕ

Browser Client -> Web Application -> Data Source (often a Database)

В прошлом манипулирование данными для их получения и передачи в базу данных было настоящим кошмаром. Если вам нужно быть в курсе .NET 1.0 / 1.1 дней, вы можете вспомнить процесс разработки, который включал в себя следующие шаги:

Manually defining a data model Creating all of the tables, setting up relationships, manually defining indexes and constraints, etc. Creating and testing stored procedures to access the data - generally manually specifying each field to be included in each procedure. Create POCO (Plain Old CLR Objects) to hold the data Code to open a connection to the database and iteratively recurse each record returned and map it into the POCO objects.

Это было просто, чтобы получить данные в систему. Возвращаясь в другую сторону, нам пришлось повторить несколько из этих шагов в обратном порядке. Дело в том, что кодирование базы данных было очень трудоемким (и действительно довольно скучным). Очевидно, что генерация числового кода и другие инструменты пришли и упростили вещи.

Настоящим прорывом стали NHibernate, Entity Framework 4 (подход Code-First) и другие подобные инструменты ORM, которые (почти) полностью абстрагировали базу данных от разработчика. Эти инструменты не только увеличили скорость разработки, но и улучшили общее качество кода, поскольку у них было меньше возможностей ошибочно вводить ошибки.

Теперь во многих приложениях подключение к базе данных и взаимодействие с ней (почти) являются запоздалой мыслью, так как большинство деталей базы данных скрыты.

Microsoft также предоставила Upshot.js и WebAPI идею о том, что эти два инструмента, при использовании в сочетании друг с другом, будут революционизировать связь между сервером и браузером так же, как это сделали NHibernate и Entity Framework 4. между сервером и базой данных.

Это действительно было бы очень достойным достижением, особенно когда клиенты стремятся к созданию более интерактивных веб-приложений.

Одним из главных камней преткновения, который мешает разработчикам перенести больше пользовательского интерфейса на (браузерный) клиент, является значительный объем необходимого кодирования. Некоторые из шагов включают в себя:

Transmit the data to the client (usually in JSON format) Map all of the properties from the .NET objects into JavaScript objects Re-create all of the meta-data about the object and its properties Bind that data to the elements in the client browser Monitor changes Re-map the data (for sending back to the server) once it has been modified Transmit the data back to the server

Это действительно выглядит как «дежа вю» потому что он очень похож по сложности на унаследованные процессы для получения данных в базу данных и из нее.

В зависимости от того, как настроено веб-приложение, может потребоваться дополнительное сопоставление данных, когда они возвращаются на сервер к фактическим объектам базы данных. (Это будет чаще, чем нет.)

Передача данных с сервера на клиент-сервер требует большого количества кодирования и предлагает множество возможностей для неожиданных задач. Не забывайте, как весело отлаживать JavaScript! (Хорошо, сейчас это менее болезненно, чем пару лет назад, но все же не так удобно для разработчиков, как отладка кода C # в Visual Studio.)

Презентация Стива Сандерсона на одностраничных приложениях предлагает совершенно иное (и лучшее) решение.

Идея состоит в том, что WebAPI, Upshot.js и Knockout смогут беспрепятственно доставлять данные и получать данные от клиента браузера, обеспечивая при этом высоко интерактивный пользовательский интерфейс. Вот Это Да! Разве это не заставляет вас просто хотеть протянуть руку и обнять кого-то?

Хотя эта идея не нова, я думаю, что это одна из первых серьезных попыток действительно сделать это в .NET.

Как только данные доставляются через WebAPI и достигают клиента (через Upshot), фреймворки, такие как Knockout, смогут потреблять данные и обеспечивать очень высокий уровень интерактивности, требуемый современными веб-приложениями. (Хотя может быть не совсем понятно, что я описываю, это приложения, которые в основном не работают, загружая «страницы», а, скорее, передавая данные в формате JSON через запросы AJAX.)

Любой инструмент, который сокращает все это кодирование, очевидно, будет быстро принят сообществом разработчиков.

Upshot.js (переименованная и обновленная версия RIA / JS) должна была решить несколько мирских задач, перечисленных выше. Он должен быть связующим звеном между WebAPI и Knockout. Он предназначен для динамического сопоставления объектов, которые передаются в JSON или XML из .NET, а также предоставляет связанные метаданные для таких вещей, как свойства объекта, обязательные поля, длины полей, отображаемые имена, описания и т. Д. (Метаданные что позволяет сопоставить и МОЖЕТ быть доступным для использования при проверке.)

Note: I am still uncertain how to access the upshot meta-data and tie it to a validation framework like jQuery validation or one of the Knockout validation plugins. This is on my todo list to test.

Note: I am uncertain which of these types of meta-data are supported. This is on my todo list to test. As a side note, I also plan to experiment with meta-data outside of System.ComponentModel.DataAnnotations to also see if NHibernate attributes are supported as well as custom attributes.

Поэтому, имея в виду все это, я решил использовать тот же набор технологий, который Стив использовал в своей демонстрации, в реальном веб-приложении. К ним относятся:

Entity Framework 4.3 using Code-First approach ASP.NET MVC4 with WebAPI Upshot.js Knockout.js

Ожидается, что все эти технологии будут хорошо работать вместе, потому что а) они являются новейшими инструментами Microsoft (за исключением Knockout с открытым исходным кодом) и потому что Стив Сандерсон, в настоящее время Microsoft, использовал их вместе в основной презентации Microsoft, демонстрирующей развитие одностраничного приложения.

К сожалению, на практике я обнаружил, что Entity Framework 4.x и Upshot.js смотрят на мир по-разному, и их ориентация несколько более противоречива, чем дополняет.

Как уже упоминалось, Entity Framework Code First выполняет действительно фантастическую работу, позволяя разработчикам определять высокофункциональные объектные модели, которые он почти волшебным образом переводит в функциональную базу данных.

Одна из замечательных особенностей Entity Framework 4.x Code First - это возможность переходить от родительского объекта к дочернему и переходить от дочернего объекта обратно к его родительскому. Эти двусторонние ассоциации являются краеугольным камнем EF. Они экономят огромное количество времени и значительно упрощают разработку. Более того, Microsoft неоднократно рекламировала эту функциональность как отличную причину для использования Entity Framework.

В блоге Скотта Гатри (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 (например, двусторонняя привязка / навигация), потому что именно это и показала презентация.

Тем не менее, я сразу начал получать неприятные маленькие ошибки во время выполнения, такие как:

"Object graph for type 'System.Collections.Generic.HashSet`1[[KnockoutTest.Models.Person, KnockoutTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' contains cycles and cannot be serialized if reference tracking is disabled."

Я попробовал множество различных подходов, чтобы попытаться решить проблему. Основываясь на моих идеях и моем чтении, вот некоторые из неудачных решений, которые я пытался.

Removed the association from one side of the relationship. This is NOT a good solution because it is very handy to be able navigate in each direction between parent and child. (This is probably why these associative properties are referred to as navigation properties.) Remove the relationship from either side had side effects. When the relationship was removed from the parent, the ability to navigate a list of children was also removed. When the relationship was removed from the child, .NET provided me with another friendly error.

"Unable to retrieve association information for association 'KnockoutTest.Models.Client_Persons'. Only models that include foreign key information are supported. See Entity Framework documentation for details on creating models that include foreign key information."

Just in case the issue was the result of the system becoming confused about there being a foreign key, I explicitly specified a [ForeignKey] attribute on the child entity. Everything compiles but .NET returns the "Object graph for type... contains cycles and cannot be serialized..."

Some of my reading indicated that adding an attribute like [DataContract(IsReference = true)] in WCF might keep .NET from getting confused about cyclical references. That's when I get this beauty.

"The type 'KnockoutTest.Models.Person' cannot be serialized to JSON because its IsReference setting is 'True'. The JSON format does not support references because there is no standardized format for representing references. To enable serialization, disable the IsReference setting on the type or an appropriate parent class of the type."

Эта ошибка очень важна, потому что она в основном говорит нам, что мы НЕ сможем использовать Upshot AND Entity Framework Code-First вместе в их обычной конфигурации. Зачем? Entity Framework предназначен для использования двусторонней привязки. Однако, когда реализована двусторонняя привязка, Апшот говорит, что она не может обрабатывать циклические ссылки. Когда циклические ссылки управляются, 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 поддерживает циклические зависимости и поддерживает ссылки на дочерние объекты. Если все работает так, как ожидалось, он позаботится о двух ошибках, которые я получил при тестировании.

Once I have tested, I will report results here.

Я думаю, что демонстрация Стива была блестящей, и мне понравилась его демонстрация. Я верю, что нокаут потрясающий. Я очень благодарен за то, куда Microsoft, кажется, идет. Если в инструменте есть примечательные ограничения, я думаю, что Microsoft, возможно, следовало бы быть более откровенным с ними.

Я не имею в виду чрезмерную критику Microsoft (и определенно не критикую Стива вообще), потому что я думаю, что они проделали замечательную работу. Мне нравится то, что обещает, и я с нетерпением жду, когда это произойдет.

Мне бы очень хотелось, чтобы кто-то взял его в исходное состояние и изменил его фактор (и WebAPI), чтобы он мог полностью интегрироваться с Entity Framework без использования стороннего инструмента.

Я не знаю, существует ли подобный инструмент для NHiberbnate, но я хотел бы, возможно, даже увидеть кого-то расширенного для интеграции (или разработки аналогичной библиотеки для) NHibernate. В общем, я в первую очередь говорю о потреблении метаданных из NHibernate.

When I test JSON.Net, I also plan to test NHibernate as well.

+1 за такой хорошо продуманный и сконструированный пост
Боже мой, что за пост! +1 для деталей!
это был даже не тот ответ, который я искал, но он такой красивый, что мне пришлось +1 к нему.
Фантастический ответ. У меня был опыт работы в WCF, когда он не генерировал вышеупомянутую ошибку, когда где-то на графе объектов было установлено двухстороннее отображение. Что бы он сделал, это перебрать весь график. Мои справочные страницы WCF были бы больше, чем объединенные работы Толстого. Поэтому, когда я нажимал на одну из этих красот, сервер зависал, пока выполнял свои «удивительные» Черепашки на всем пути вниз ». трюк. +1

Похожие вопросы