James Hogle
James Hogle

Reputation: 3040

Entity Framework Foreign Key For Circular Relation

I am converting an application to Entity Framework Core and am running into trouble getting a Foreign Key relationship between two of my model classes. The classes are setup like so (Note that a Guid Id field is declared on BaseEntity):

public class Crt : BaseEntity
{
    [Required]
    public Guid FacId { get; set; }

    [Required]
    public string Code { get; set; }

    [ForeignKey("ActiveCrtChk") 
    public Guid? ActiveCrtChkId { get; set; }

    public string Description { get; set; }

    public string Device { get; set; }

    #region navigation properties
    public CrtChk ActiveCrtChk;
    public List<CrtChk> CartChecks;
    #endregion
}

public class CrtChk : BaseEntity
{
    [Required]
    public Guid CrtId { get; set; }

    [Required]
    public string Device { get; set; }

    [Required]
    public Guid OutSysUsrId { get; set; }

    [Required]
    public DateTime OutSysDateTime { get; set; }

    public Guid? InSysUsrId { get; set; }

    public DateTime? InSysDateTime { get; set; }

    [Required]
    public string Type { get; set; }

    #region navigation properties
    public Crt Cart { get; set; }
    public Usr OutSysUsr { get; set; }
    public Usr InSysUsr { get; set; }
    public List<CrtEvt> CartEvents { get; set; }
    #endregion
}

The idea behind the relationship is that one Crt can have many CrtChk records, but Crt also stores the Id of the active CrtChk record.

When I run the migration, it generates all of the foreign key relationships I would expect between Crt and CrtChk except there is no foreign key generated for the ActiveCrtChkId field.

It is my understanding from reading this post that having the ForeignKey attribute on the ActiveCrtChkId property with the name of the ActiveCrtChk navigation property, that I should get a Foreign Key constraint in my migration.

What am I missing here?

Edit

After fixing my mistake of declaring the Crt navigation properties as fields, I have stumbled on a new error when I try to create the migration.

Unable to determine the relationship represented by navigation property 'Crt.ActiveCrtChk' of type 'CrtChk'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

I thought the ForeignKey attribute was manually configuring the relationship? Do I need to use the Fluent API do create the relationship? If so, how can I use the Fluent API to make a relationship that can be both one-to-one (Crt to ActiveCrtChk) and one to many (all CrtChks associated with Crt)?

Upvotes: 3

Views: 3784

Answers (1)

Ivan Stoev
Ivan Stoev

Reputation: 205679

It's possible, but since this design creates circular dependency between the two entities, it would cause you a lot of problems. For instance, not only one of the relationships (let say from CrtChk to Crt) cannot use cascade delete, but also you cannot simply delete the Crt without first updating the ActiveCrtChkId to null (and calling SaveChanges).

Anyway, here is how you configure the desired relationships. Usually it would be enough to use InverseProperty attribute to resolve navigation property mapping ambiguity, but one-to-one unidirectional (i.e. with navigation property only at one of the ends) requires fluent configuration (otherwise it will be mapped by convention to one-to-many). Also specially for relationships, I find explicit fluent configuration much clear than considering all EF conventional assumptions and data annotations like where to put ForeignKey attribute (on FK property or navigation property), what string to put there is the first or later case etc.

Shortly, here is the full explicit configuration of the relationships in question:

// Crt 1 - 0..N CrtChk
modelBuilder.Entity<Crt>()
    .HasMany(e => e.CartChecks)
    .WithOne(e => e.Cart)
    .HasForeignKey(e => e.CrtId)
    .OnDelete(DeleteBehavior.Cascade);

// CrtChk 1 - 0..1 Crt
modelBuilder.Entity<Crt>()
    .HasOne(e => e.ActiveCrtChk)
    .WithOne()
    .HasForeignKey<Crt>(e => e.ActiveCrtChkId)
    .OnDelete(DeleteBehavior.Restrict);

Note that Cart property cannot be used in both relationships. First, because each navigation property can be mapped only to one relationship. Second, because the relational model cannot enforce that CrtChk record referenced by ActiveCrtChkId FK has the same CrtId as the Id of the Crt referencing it - it could be any other (although logically the intent is different).

Upvotes: 4

Related Questions