3

I'm trying to convert the following model (see image below) to Code First. I've tried various combinations involving ForeignKey and InverseProperty attributes with no luck. I've found this answer but it seems that combinations of ForeignKey and InverseProperty cause a different behaviour.

The attached source code gives the following error:

Unable to determine the principal end of an association between the types 'InversePropertyTest.Author' and 'InversePropertyTest.Book'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.

This is my model with EDMX

Model example

Sample code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InversePropertyTest
{
    public class Author
    {
        public int AuthorId { get; set; }
        public Nullable<int> CurrentlyWorkingBookId { get; set; }

        [InverseProperty("Author")] public ICollection<Book> Books { get; set; }
        [ForeignKey("CurrentlyWorkingBookId"), InverseProperty("EditoredBy")] public Book CurrentlyWorkingBook { get; set; }
    }

    public class Book
    {
        public int BookId { get; set; }
        public int AuthorId { get; set; }

        [ForeignKey("AuthorId"), InverseProperty("Books")] public Author Author { get; set; }
        [InverseProperty("CurrentlyWorkingBook")] public Author EditoredBy { get; set; }
    }

    public class SimpleContext : DbContext
    {
        public DbSet<Author> Authors { get; set; }
        public DbSet<Book> Books { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new SimpleContext())
            {
                IList<Author> authors = (from a in context.Authors select a).ToList();
                IList<Book> books = (from b in context.Books select b).ToList();
            }
        }
    }
}

Any help is greatly appreciated

Community
  • 1
  • 1
Dan
  • 1,060
  • 13
  • 39

1 Answers1

2

When you use [InverseProperty] at both ends of a 1:1 association it is not clear who the principal should be. The principle is the entity the other end (the dependent) refers to by a foreign key. Even though you tell EF that EditoredBy and CurrentlyWorkingBookId both are part of one association it still would be possible to have a foreign key field for EditoredBy in Book (that wouldn't show in the class model).

Admittedly, one could contend that you've told EF enough to create the database model properly. EF could have logic that says: if I've been told about one foreign key in a 1:1 association, then I know who the principle should be. However, unfortunately it doesn't.

So I would use the fluent API to model this:

public class Author
{
    public int AuthorId { get; set; }
    public ICollection<Book> Books { get; set; }
    public Book CurrentlyWorkingBook { get; set; }
}

public class Book
{
    public int BookId { get; set; }
    public int AuthorId { get; set; }
    public Author Author { get; set; }
    public Author EditoredBy { get; set; }
}

In OnModelCreating:

modelBuilder.Entity<Author>()
            .HasMany(a => a.Books)
            .WithRequired(b => b.Author)
            .HasForeignKey(b => b.AuthorId);
modelBuilder.Entity<Author>()
            .HasOptional(a => a.CurrentlyWorkingBook)
            .WithOptionalDependent(b => b.EditoredBy)
            .Map(m => m.MapKey("CurrentlyWorkingBookId"));

Personally, I like the fluent API because the lambda expressions allow compile-time checks and it is much more conspicuous which ends comprise one association.

As you see, CurrentlyWorkingBookId can not be part of the class model in this scenario. That is because an OptionalNavigationPropertyConfiguration (the return type of WithOptionalDependent) doesn't have HasForeignKey method. I'm not sure why not. I think it should be possible to set a primitive FK value (CurrentlyWorkingBookId) as well as a reference property (CurrentlyWorkingBook).

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291