Обнаружен цикл самоссылки для свойства с типом

#entity-framework

#entity-framework

Вопрос:

У меня есть 3 экземпляра: Author, EventInstance и комментарии. Автор может написать комментарий. Пользователи (авторы) могут добавлять комментарии к EventInstance. Вот модель. Я продолжаю получать следующую ошибку.

 public class Author
{
   public Guid Id { get; set; }
   //..

   public List<EventInstance> Events { get; set; } 
          = new List<EventInstance>();
   public ICollection<Comment> Comments { get; set; }
          = new List<Comment>();
}

public class EventInstance
{
   public Guid Id { get; set; }
   public Guid AuthorId { get; set; } //Author FK
   //..

   public Author Author { get; set; }
   public ICollection<Comment> Comments { get; set; }
          = new List<Comment>();
}

public class Comment
{
  public Guid Id { get; set; }
  public Guid AuthorId { get; set; }   //Author FK      
  public Guid EventId { get; set; }

  public EventInstance Event { get; set; }
  public Author Author { get; set; }
}
  

Я пробовал несколько способов исправить, но, похоже, ничего не работает. Даже простой запрос, такой как:

 var list = await db.Events
            .Include(e => e.Author)
            .ToListAsync();
  

Спасибо за помощь.

Редактировать

Вот мой класс контекста

 public class EventContext : DbContext
{
    public DbSet<EventInstance> Events { get; set; }
    public DbSet<Comment> Comments { get; set; }
    public DbSet<Author> Authors { get; set; }

    public EventContext(DbContextOptions<EventContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<EventInstance>()
            .HasOne(e => e.Author)
            .WithMany(a => a.Events)
            .HasForeignKey(e => e.AuthorId)
            .OnDelete(DeleteBehavior.NoAction);

        builder.Entity<Comment>()
            .HasOne(c => c.Event)
            .WithMany(e => e.Comments)
            .HasForeignKey(c => c.EventId)
            .OnDelete(DeleteBehavior.NoAction);

        builder.Entity<Comment>()
            .HasOne(c => c.Author)
            .WithMany(a => a.Comments)
            .HasForeignKey(c => c.AuthorId)
            .OnDelete(DeleteBehavior.NoAction);
    }
}
  

Я использую фабрику для добавления и запуска миграций

 public class EventContextFactory : IDesignTimeDbContextFactory<EventContext>
{
    public EventContext CreateDbContext(string[] args)
    {
        string connectionString = 
  

Окружающая среда.GetEnvironmentVariable(«SqlConnectionString»);

         var optionsBuilder = new DbContextOptionsBuilder<EventContext>();
        optionsBuilder.UseSqlServer(connectionString);

        return new EventContext(optionsBuilder.Options);
    }
}
  

И это класс запуска

 public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        string connectionString = Environment.GetEnvironmentVariable("SqlConnectionString");
        builder.Services.AddDbContext<EventContext>(
            options => options.UseSqlServer(connectionString));

        builder.Services.AddScoped<IEventService, EventService>();
    }
}
  

Я надеюсь, что все соответствующие фрагменты были вставлены сюда.

Комментарии:

1. Это не должно решить вашу проблему, но все же я бы сказал, что [ForeignKey("EventInstance")] это должно быть [ForeignKey("Event")]

2. Это проблема сериализации Json, а не EF (Core). Вы можете найти много связанных сообщений, выполнив поиск в Web / SO с сообщением об ошибке. Вот один из них, связанный с функциями Azure github.com/Azure/azure-functions-durable-extension/issues/685

Ответ №1:

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

 public class Author
{
   public Guid Id { get; set; }
   //..

   public virtual ICollection<EventInstance> Events { get; set; } 
          = new List<EventInstance>();
}

public class EventInstance
{
   public Guid Id { get; set; }
   [ForeignKey("Author")] 
   public Guid AuthorId { get; set; }
   //..

   public Author Author { get; set; }
   public virtual ICollection<Comment> Comments { get; set; }
}

public class Comment
{
  public Guid Id { get; set; }
  [ForeignKey("Event")]
  public Guid EventId { get; set; }

  public EventInstance Event { get; set; }
}
  

Затем, чтобы получить комментарии для конкретного автора:

 var commentsForAuthor = context.Authors
    .Where(x => x.Id == authorId)
    .SelectMany(x => x.Events.SelectMany(e => e.Comments))
    .ToList();
  

В качестве альтернативы, если комментарии можно рассматривать как объект верхнего уровня (/ w DbSet)

 var commentsForAuthor = context.Comments
    .Where(x => x.EventInstance.AuthorId == authorId)
    .ToList();
  

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

Вероятно, вы можете применить выражения сопоставления для устранения ошибки с помощью явного сопоставления, но я бы рекомендовал удалить денормализацию. Проблема с денормализацией ссылок, чтобы автор мог иметь коллекцию комментариев, заключается в том, что нет способа обеспечить соответствие ссылки на автора комментария автору EventInstance комментария. Т.е. Я могу иметь идентификатор автора 1 с идентификатором EventInstance 1 и создать комментарий сИДЕНТИФИКАТОР 101. Ничто не мешает мне изменить идентификатор автора этого комментария на 2. Теперь у меня есть два источника истины относительно того, кто является автором. Comment.Author (ID: 2) и Comment.EventInstance.Author . (ID: 1) Везде, где это возможно, удалите эти повторяющиеся ссылочные пути.

РЕДАКТИРОВАТЬ: обратная связь по сопоставлению

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

 protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<EventInstance>()
        .HasOne(x => x.Author)
        .WithMany(x => x.Events)
        .HasForeignKey(x => x.AuthorId)
        .OnDelete(DeleteBehavior.NoAction);
    modelBuilder.Entity<EventInstance>()
        .HasMany(x => x.Comments)
        .WithOne(x => x.Event)
        .HasForeignKey(x => x.EventId)
        .OnDelete(DeleteBehavior.NoAction);

    modelBuilder.Entity<Author>()
        .Ignore(a => a.AuthorName)
        .HasMany(x => x.Comments)
        .WithOne(x => x.Author)
        .HasForeignKey(x => x.AuthorId)
        .OnDelete(DeleteBehavior.NoAction);

}
  

Редактировать # 2: если авторы могут создавать комментарии для событий другого автора, я бы предложил удалить связь комментариев со стороны автора. Комментарии могут быть получены как объект верхнего уровня или через События как объект верхнего уровня.

Например:

 public class Author
{
   public Guid Id { get; set; }
   //..

   public virtual ICollection<EventInstance> Events { get; set; } 
          = new List<EventInstance>();
}

public class EventInstance
{
   public Guid Id { get; set; }
   public Guid AuthorId { get; set; }
   //..

   public Author Author { get; set; }
   public virtual ICollection<Comment> Comments { get; set; }
}

public class Comment
{
  public Guid Id { get; set; }
  public Guid EventId { get; set; }

  public virtual EventInstance Event { get; set; }
  public Guid AuthorId { get; set; }
  public virtual Author Author { get; set; }
  
}
  

Затем в сопоставлении:

     modelBuilder.Entity<Comment>()
        .HasOne(x => x.Author)
        .WithMany()
        .HasForeignKey(x => x.AuthorId)
        .OnDelete(DeleteBehavior.NoAction);
    modelBuilder.Entity<Comment>()
        .HasOne(x => x.Event)
        .WithMany(x => x.Comments)
        .HasForeignKey(x => x.EventId)
        .OnDelete(DeleteBehavior.NoAction);

    modelBuilder.Entity<Author>()
        .Ignore(a => a.AuthorName);
  

Это переключает сопоставление с события на комментарий, но комментарий сохраняет двунаправленную ссылку на это событие, но однонаправленную ссылку на его автора. ( HasMany() ) Это должно удовлетворять отображению EF. На заметку с явным сопоставлением отношений с использованием OnModelCreating or IEntityTypeConfiguration нам не нужны [ForeignKey] атрибуты, поэтому я их удалил.

Комментарии:

1. Автор может добавить комментарий к событию, которое он / она не создавал. В этом случае я должен создать authors и comments many to many связь? Я боюсь, что это может не решить проблему.

2. Если это так, то авторы могут создать комментарий для события, которое они не создавали, тогда я бы удалил «многие» отношения для автора из комментариев. Я обновлю свой ответ, чтобы описать этот вариант.

3. Все, что вы предлагаете, не сработало Azure Function . Мне было так любопытно, что я реализовал исходную модель в ASP.NET Ядро MVC, то есть автор может добавить событие. Автор события или любой другой автор может прокомментировать событие. Тот же код, который вызывал проблему в функции Azure, отлично работал в ASP.NET Ядро MVC.

4. Работают ли другие отношения сущностей, как ожидалось, в функции Azure?

5. нет. Я удалил все свойства навигации из всех объектов и всей конфигурации. Я запускаю миграцию, а затем обновляю базу данных. Итак, все 3 таблицы не имеют FK. Я запускаю простейший запрос var list = await db.Events.ToListAsync(); . У меня все еще одна и та же проблема, как включенная Azure , так и включенная MSSQLLocalDB .