5

I have the scenario where a user can upload multiple photos (One-to-Many). The user also can have a default photo (One-to-One). However, I entity framework core 2.0 tells that he cannot recognize the relationship when I use the following code:

public class User
{
    public Guid Id { get; set; }

    public ICollection<Photo> Photos{ get; set; }

    public Photo DefaultPhoto { get; set; }
    [ForeignKey("DefaultPhoto")]
    public Guid DefaultPhotoId { get; set; }
}

public class Photo
{
    public Guid Id { get; set; }

    public User Owner { get; set; }
}

How may I achieve these multiple relationships?

There error shown by EF-Core:

System.InvalidOperationException: 'Unable to determine the relationship represented by navigation property 'Photo.Owner' of type 'User'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.'

UPDATE:

Adding [InverseProperty("Photos")] to the navigation property Owner in File Model seems to be working. I am not sure if that is the correct way?

enter image description here In this image File=Photo; Uploader=Owner (to be comparable with the above model).

UPDATE 2:

I confirm what @Ivan said in the comments, with DataAnnotation approach, I get One-to-Many in two directions instead of One-to-Many and One-to-One. This figure shows the generated database by using InverseProperty (the connection between the to entities show the bi-directional One-to-Many relationship):

enter image description here

In this image File=Photo; Uploader=Owner (to be comparable with the above model).

26
  • 2
    This attribute tells which property from another class is "the other end" of navigation property. So by marking Photos with [InverseProperty("Owner")]` you tell that another end of navigation property Photos in class User is property Owner in class Photo. Same can be done in reverse. If you mark Owner with [InverseProperty("Photos")] - you are doing the same. Why it works one way and not the other for you - I'm not sure. Commented Dec 9, 2017 at 14:02
  • 2
    I guess so. When I generated database from model in your question - one of them (OwnerId) was declared as nullable. I wonder what would happen indeed when you try to insert into your current database. Commented Dec 9, 2017 at 14:35
  • 2
    InverseProperty solves just half of the problem - correctly relating one-to-many relationship defining navigation properties. However the second relationship by convention is one-to-many, not one-to-one as desired. Forget about data annotations, fluent configuration is a must. Commented Dec 9, 2017 at 17:06
  • 2
    Take it this way - by default the inverse of Photo DefaultPhoto in User is something like ICollection<User> DefaultPhotoUsers in Photo, or in other words - one to many. There is no way to configure it as one to one using data annotations, hence you should use fluent API (as in @gldraphael answer). Commented Dec 9, 2017 at 17:27
  • 2
    That's correct. Commented Dec 9, 2017 at 17:50

1 Answer 1

3

Use the Fluent API to establish the one-to-one relationship:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Establish a one to one relationship with the
    // User as the dependent End
    modelBuilder.Entity<User>()
        .HasOne(u => u.DefaultPhoto)
        .WithOne(p => p.Owner)
        .HasForeignKey<User>(u => u.DefaultPhotoId);
}

For a relationship to be a one-to-one (1:0..1) relationship, the relationship must be established between two primary keys or two candidate keys. (Check this blog post for more info on this.)

EF has no way of setting a candidate key (also called unique or alternate key) via Annotations right now, so this is your only option. Check the EF docs here at Microsoft Docs: One to One relationships


Update: The earlier code would automatically generate a UserId column and sets up relationships correctly. I've added the OwnerId field to the Photo entity to manually set the relationship, like you wanted:

public class User
{
    public Guid Id { get; set; }

    [InverseProperty("Owner")]
    public ICollection<Photo> Photos{ get; set; }

    public Photo DefaultPhoto { get; set; }
    public Guid DefaultPhotoId { get; set; }
}

public class Photo
{
    public Guid Id { get; set; }

    public Guid OwnerId { get; set; }
    public User Owner { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Photo> Photos { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // You may use this instead of the inverse property
        // // The One to Many relationship between
        // // User.Id (Principal End) and Photo.OwnerId
        // modelBuilder.Entity<Photo>()
        //     .HasOne(p => p.Owner)
        //     .WithMany(u => u.Photos)
        //     .HasForeignKey(p => p.OwnerId);

        // Establishes 1:0..1 relationship between
        // Photo.Id (Principal End) and User.DefaultPhoto (Dependent end)
        modelBuilder.Entity<User>()
            .HasOne(u => u.DefaultPhoto)
            .WithOne() // we leave this empty because it doesn't correspond to a navigation property in the Photos table
            .HasForeignKey<User>(u => u.DefaultPhotoId);
    }
}

The trick is in figuring the relationships (especially the principal end and the dependent ends) and in figuring which ones require navigation properties.

The Inverse property is required because EF doesn't know which property to map to Photo.Owner. It's just a way of making a relationship explicit in cases like these.

Sign up to request clarification or add additional context in comments.

11 Comments

I am trying this, but what if I added public Guid OwnerId { get; set; } to file entity? (I have tried adding it but no success, even the key is defined).
@MohammedNoureldin managed to get it working or do you want me to update the answer?
with a help from Evk I got it working, but I still do not understand why it is working. You can update the answer, so you share the knowledge. Thanks.
@MohammedNoureldin I've made an update. Let me know if you have questions. This was a major source of frustration when I got started with EF. But once you get this, it's a piece of cake.
@MohammedNoureldin the one to many relationship is established by EF conventions in the first instance. That code is just for the 1:0..1 relationship. Whenever EF can't figure things out it'll throw an error, which is why you saw the error in the question in the first place.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.