Search code examples
c#entity-frameworkentity-framework-coreef-fluent-api

Entity Framework fluent one-to-one - property/navigation already exists


I have two entities, Movie and UserRating. I want to be able to specify table names and column names myself. UserRating shall have a foreign key to Movie in the database, but I'd like both entities in my code to have a reference to the other.

When I try to configure the relations using the fluent API in Entity Framework 7 i run into issues where creating a migrations fails due to the property/navigation already existing.

My two entity classes look like this

public class Movie
{
    public long Id { get; set; }
    public string Title { get; set; }
    public UserRating UserRating { get; set; }
}

public class UserRating
{
    public long Id { get; set; }
    public Movie Movie { get; set; }
    public int Rating { get; set; }
}

I have two classes where I configure the mappings:

public class MovieConfig : IEntityTypeConfig<Movie>
{
    public void Configure(EntityTypeBuilder<Movie> builder)
    {
        builder.ToTable("movie");
        builder.HasKey(m => m.Id);
        builder.Property(m => m.Id).HasColumnName("id").ValueGeneratedOnAdd();
        builder.Property(m => m.Title).HasColumnName("title").IsRequired();
    }
}

public class UserRatingConfig : IEntityTypeConfig<UserRating>
{
    public void Configure(EntityTypeBuilder<UserRating> builder)
    {
        builder.ToTable("user_rating");
        builder.HasKey(ur => ur.Id);
        builder.Property(ur => ur.Id).HasColumnName("id").ValueGeneratedOnAdd();
        builder.Property(ur => ur.Rating).HasColumnName("rating").IsRequired();
        builder.Property(ur => ur.Movie).HasColumnName("movie_id").IsRequired();
        builder.HasOne(ur => ur.Movie).WithOne(m => m.UserRating).HasForeignKey("movie_id");
    }
}

And lastly the DbContext where the configs are applied:

public class MovieContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseNpgsql("<connection string>");

    public DbSet<Movie> Movies { get; set; }
    public DbSet<UserRating> UserRatings { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new MovieConfig());
        modelBuilder.ApplyConfiguration(new UserRatingConfig());
    }
}

With this setup, when I try to run dotnet ef migrations add InitialCreate I get the following error:

The property or navigation 'Movie' cannot be added to the entity type 'UserRating' because a property or navigation with the same name already exists on entity type 'UserRating'. The stacktrace originates from the last line in UserRatingConfig.

So it sounds like the relationship's being configured two times somehow. So I tried to prevent it being configured from Movie, first by setting [NotMapped] on the UserRating property. And then by specifying builder.Ignore(m => m.UserRating); in MovieConfig. Both resulted in the same error.

Okay, so what if I try to just skip configuring the relation from UserRating? I tried to just comment out the last line, where the stacktrace originates:

//builder.HasOne(ur => ur.Movie).WithOne(m => m.UserRating).HasForeignKey("movie_id");

That results in a different error message, that is even more confusing. Now I get the error: The 'Movie' property 'UserRating.Movie' could not be mapped because the database provider does not support this type. Consider converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

This makes me even more confused. So somehow I'm trying to configure the relationship two times, and if I remove the only place I know about, then I'm apparently not configuring it at all.

What do I need to change in order for this to work?


Solution

  • You only need to configure the relationship once, and you only need the fluent configuration at all if you don't have a declared foreign key property. eg

        modelBuilder.Entity<Movie>()
                    .HasOne(m => m.UserRating)
                    .WithOne(r => r.Movie)
                    .HasForeignKey<UserRating>(r => r.Id);
    

    creates

          CREATE TABLE [Movie] (
              [Id] bigint NOT NULL IDENTITY,
              [Title] nvarchar(max) NOT NULL,
              CONSTRAINT [PK_Movie] PRIMARY KEY ([Id])
          );
    
          CREATE TABLE [UserRating] (
              [Id] bigint NOT NULL,
              [Rating] int NOT NULL,
              CONSTRAINT [PK_UserRating] PRIMARY KEY ([Id]),
              CONSTRAINT [FK_UserRating_Movie_Id] FOREIGN KEY ([Id]) REFERENCES [Movie] ([Id]) ON DELETE CASCADE
          );
    

    Also 1-1 relationships have bad code smell. Just add the properties to Movie, or declare UserRating as an Owned Entity Type if you want to dot into the rating properties.