16

I have such models

public class Question
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    public Answer Answer { get; set; }
    public List<Variant> Variants { get; set; }

    public string CorrectVariantId { get; set; }
    public Variant CorrectVariant { get; set; }
}

public class Variant
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    public string QuestionId { get; set; }
    public Question Question { get; set; }
}

// mapping

modelBuilder.Entity<Question>()
    .HasOne(q => q.CorrectVariant)
    .WithOne(v => v.Question)
    .HasForeignKey<Question>(q => q.CorrectVariantId);

modelBuilder.Entity<Variant>()
    .HasOne(v => v.Question)
    .WithMany(a => a.Variants)
    .OnDelete(DeleteBehavior.Cascade);

Which worked perfectly until I upgraded from EF RC1 to RTM. But now it throws: System.InvalidOperationException: Cannot create a relationship between 'Question.Variants' and 'Variant.Question', because there already is a relationship between 'Question.CorrectVariant' and 'Variant.Question'. Navigation properties can only participate in a single relationship.

Is there any workaround for this problem without just deleting Variants property from the Question model?

Veikedo
  • 1,453
  • 1
  • 18
  • 25

3 Answers3

21

The two examples given already got me part of the way there, but I wanted a collection and a single item of the same object type and therefore the same table on my model like in the original question. I've tried to provide a simple example of this below that works for .NET Core 2.2:

public class ParentModel
{
    public int Id { get; set; }

    // Id for single instance navigation property
    public int? ChildModelId { get; set; }

    // Single instance navigation property to ChildTable, identified by ChildModelId property as foreign key
    public virtual ChildModel ChildModel { get; set; }

    // Collection navigation property to ChildTable with identified by ParentId property
    public virtual ICollection<ChildModel> ChildModels { get; set; }
}

public class ChildModel
{
    public int Id { get; set; }

    // Id for ParentModel property back to ParentTable
    public int ParentId { get; set; }

    // Single instance navigation property to ParentTable, identified by ParentId property as foreign key
    public virtual ParentModel ParentModel { get; set; }
}

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {   
    }

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

        builder.Entity<ParentModel>()
            .ToTable("ParentTable");

        // Configure collection of ChildModels (ParentTable to ChildTable/one-to-many relationship)
        builder.Entity<ParentModel>()
            .HasMany(t => t.ChildModels)
            .WithOne(t => t.ParentModel)
            .HasForeignKey(t => t.ParentId)
            .IsRequired()
            .OnDelete(DeleteBehavior.Cascade);

        builder.Entity<ChildModel>()
            .ToTable("ChildTable");

        // Configure single ChildModel navigation property on ParentModel (one-to-one relationship)
        builder.Entity<ParentModel>()
            .HasOne(t => t.ChildModel)
            .WithOne()
            .HasForeignKey(typeof(ParentModel), nameof(ParentModel.ChildModelId))
            .IsRequired(false)
            .OnDelete(DeleteBehavior.Restrict);
    }
}

The key to avoiding the Navigation properties can only participate in a single relationship. error is to configure the navigation property back to the parent table only once. We configure this on for the ChildModels collection on the ParentTable using .WithOne(t => t.ParentModel). We then don't bother configuring the other side of the relationship for the subsequent relationships by calling .WithOne() empty, because if we did configure it again (eg .WithOne(t => t.ParentModel)) it would error.

Also the virtual modifiers on the navigation properties are to allow for lazy loading.

Luke
  • 22,826
  • 31
  • 110
  • 193
  • 2
    Bingo. Thank you. – Rich Nov 06 '19 at 18:18
  • Thanks a lot. The .WithOne() and explicit HasForeignKey was the key on my case. – Jonathan Ramos Jun 15 '20 at 20:26
  • I've found this solution causes EF not to load the navigation property on the omitted relationship. i.e.The child model will not populate the ParentModel property – jflood.net Jul 16 '20 at 13:32
  • 1
    @jflood.net Are you forgetting to include calls to .Include with your query? For example `var childWithParent = context.ChildModel.Include(x => x.ParentModel).First();` – Crag Jan 21 '21 at 02:39
15

In case someone will run into this question. Here is more elegant solution

public class Question
{
    public Guid Id { get; private set; }
    public IReadOnlyList<Variant> Variants { get; private set; }
    public Guid CorrectVariantId { get; private set; }
    public Guid? AnsweredVariantId { get; private set; }    
    public bool IsAnswerCorrect => CorrectVariantId == AnsweredVariantId;
    public bool IsAnswered => AnsweredVariantId != null;
}

public class Variant
{
    public Guid Id { get; private set; }
    public Guid QuestionId { get; private set; }
    public string HiddenUserLogin { get; private set; }
    public User HiddenUser { get; private set; }
}

// mapping
mb.Entity<Question>()
    .HasMany(q => q.Variants)
    .WithOne()
    .HasForeignKey(nameof(Variant.QuestionId))
    .IsRequired()
    .OnDelete(DeleteBehavior.Cascade);

mb.Entity<Question>()
    .HasOne(typeof(Variant))
    .WithOne()
    .HasForeignKey<Question>(nameof(Question.AnsweredVariantId))
    .IsRequired(false) 
    .OnDelete(DeleteBehavior.Restrict);

// EF creates Unique Index for nullable fields
mb.Entity<Question>()
    .HasIndex(q => q.AnsweredVariantId)
    .IsUnique(false);

// create index instead of FK hence the cyclic dependency between Question and Variant
mb.Entity<Question>()
    .HasIndex(q => q.CorrectVariantId)
    .IsUnique();
Veikedo
  • 1,453
  • 1
  • 18
  • 25
5

That was a bug/undesired behaviour in RC1 and it has been fixed.

You should create another property, say SecondQuestion for the other relationship.

public class Question
{
  public string Id { get; set; } = Guid.NewGuid().ToString();
  public List<Variant> Variants { get; set; }

  public string CorrectVariantId { get; set; }
  public Variant CorrectVariant { get; set; }
}

public class Variant
{
  public string Id { get; set; } = Guid.NewGuid().ToString();

  public string QuestionId { get; set; }
  public Question Question { get; set; }

  public Question SecondQuestion { get; set; }
}

Your DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Question>()
    .HasOne(q => q.CorrectVariant)
    .WithOne(v => v.SecondQuestion)
    .HasForeignKey<Question>(q => q.CorrectVariantId);

  modelBuilder.Entity<Variant>()
      .HasOne(v => v.Question)
      .WithMany(a => a.Variants).HasForeignKey(x => x.QuestionId).OnDelete(DeleteBehavior.SetNull);

  base.OnModelCreating(modelBuilder);
}

How to use it:

using (var myDb = new MyDbContext())
{
  var variantFirst = new Variant();
  var variantSecond = new Variant();

  var question = new Question();
  variantFirst.Question = question;

  variantSecond.SecondQuestion = question;

  myDb.Variants.Add(variantFirst);
  myDb.Variants.Add(variantSecond);

  myDb.SaveChanges();
}
Bassam Alugili
  • 16,345
  • 7
  • 52
  • 70
  • 1
    Don't like this solution, but also don't see other. Thank you! – Veikedo Jul 25 '16 at 04:22
  • 2
    Some people's "undesired behavior" are evidently other people's "desired behaviour". :| – Sailing Judo Mar 08 '17 at 19:42
  • 4
    is this still the case? I found this horrible, to have to define two property that point always to the same object, even because this could lead to a broken scenario if you set explicitly SecondQuestion to a different value than Question – Not Important Aug 05 '17 at 11:58
  • 1
    @FabioAngela you can pass string property name (using `nameof` operator). Something like this https://pastebin.com/RsE4dzeR – Veikedo Oct 17 '17 at 07:44
  • Any updates? as @FabioAngela said, I do not like that definition of the "same" property twice. – Mohammed Noureldin Dec 09 '17 at 12:19