Search code examples
asp.net-coreentity-framework-coreforeign-keys

Entity Framework Core - one-to-many but parent also has navigation property to a single child?


I currently have a working one-to-many relationship between the entities 'Conversation' and 'Message', where a conversation can have multiple messages.

This works fine:

public class Conversation
{
    public long ID { get; set; }
}

public class Message : IEntity
{
    public virtual Conversation Conversation { get; set; }
    public long ConversationID { get; set; }
    public long ID { get; set; }
}

However, I am trying to add a navigation property to the 'Conversation' class called 'LastMessage' which will keep track of the last message record that was created:

public class Conversation
{
    public long ID { get; set; }
    public virtual Message LastMessage { get; set; }
    public long LastMessageID { get; set; }
}

When I try to apply the above, I get the error

System.InvalidOperationException: The child/dependent side could not be determined for the one-to-one relationship between 'Conversation.LastMessage' and 'Message.Conversation'.

How do I maintain a one-to-many relationship between 'Conversation' and 'Message', but ALSO add a navigation property in the 'Conversation' class that navigates to a single 'Message' record?


Solution

  • After trying all sorts of Data Annotations and Fluent API nonsense, the cleanest solution I could come up with turned out to be very simple which requires neither. It only requires adding a 'private' constructor to the Conversation class (or a 'protected' one if you're using Lazy Loading) into which your 'DbContext' object is injected. Just set up your 'Conversation' and 'Message' classes as a normal one-to-many relationship, and with your database context now available from within the 'Conversation' entity, you can make 'LastMessage' simply return a query from the database using the Find() method. The Find() method also makes use of caching, so if you call the getter more than once, it will only make one trip to the database.

    Here is the documentation on this ability: https://learn.microsoft.com/en-us/ef/core/modeling/constructors#injecting-services

    Note: the 'LastMessage' property is read-only. To modify it, set the 'LastMessageID' property.

    class Conversation
    {
        public Conversation() { }
        private MyDbContext Context { get; set; }
        // make the following constructor 'protected' if you're using Lazy Loading
        // if not, make it 'private'
        protected Conversation(MyDbContext Context) { this.Context = Context; }
    
        public int ID { get; set; }
        public int LastMessageID { get; set; }
        public Message LastMessage { get { return Context.Messages.Find(LastMessageID); } }
    }
    
    class Message
    {
        public int ID { get; set; }
        public int ConversationID { get; set; }
        public virtual Conversation Conversation { get; set; }
    }