Reputation: 3040
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
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