I need your help understanding relationships between tables. I´m having a hard time trying to understand the usage/need of using navigation properties with foreign key properties to define relantionships.
Given the 2 classes below,
public class Person
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required]
public string Name{ get; set; }
}
public class Package
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required]
public Guid AccountId { get; set; }
public virtual Person Account { get; set; }
public Guid ShipperId { get; set; }
public virtual Client Shipper { get; set; }
[Required]
public Guid ReceiverId { get; set; }
public virtual Client Receiver { get; set; }
}
If I try to update the database, I get the error
Introducing FOREIGN KEY constraint 'FK_' on table 'Packages' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
On the other hand if I remove the property public Guid ....Id { get; set; }, the database is created with the foreign keys but the [Required] annotation as no effect.
What´s the difference between including a Guid property and not?
try to use this classes:
public class Person
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required]
public string Name{ get; set; }
[InverseProperty(nameof(Package.Account))]
public virtual ICollection<Package> Packages { get; set; }
}
public class Package
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required]
public Guid AccountId { get; set; }
[ForeignKey(nameof(AccountId))]
[InverseProperty("Packages")]
public virtual Person Account { get; set; }
}
and use this code in dbcontext:
modelBuilder.Entity<Package>(entity =>
{
entity.HasOne(d => d.Person)
.WithMany(p => p.Packages)
.HasForeignKey(d => d.AccountId)
.OnDelete(DeleteBehavior.ClientSetNull);
});
Related
I am trying to relate my Tables with ForeignKey and PrimaryKey on the other end. But now i will be using a ForeignKey which is not the primary for the said table. I was using [InverseProperty] but i think there's a bug with it since i've been looking around for hours already and all of them says the same thing about it.
Documents Table:
public class Document
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int DocumentId { get; set; }
public int ProjectId { get; set; }
public int DepartmentId { get; set; }
public int AuthorId { get; set; }
[NotMapped]
public virtual User Author { get; set; }
}
Users
public class User
{
[Key]
public int UserId { get; set; }
public int AuthUserId { get; set; }
public string DisplayName { get; set; }
[NotMapped]
[ForeignKey("AuthorId")]
public virtual Document Document { get; set; }
}
Context:
modelBuilder.Entity<User>(entity =>
{
entity.HasOne(u => u.Document)
.WithMany("AuthorId");
});
I am trying to use the solution they here, but no luck.
Any help would really be appreciated. Thanks!
But now i will be using a ForeignKey which is not the primary for the said table.
To do this you can use EF Core Alternate Keys feature. But first correct your model class set up as follows: (As you said a User will have multiple Document)
public class Document
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int DocumentId { get; set; }
public int ProjectId { get; set; }
public int DepartmentId { get; set; }
public int AuthorId { get; set; }
public User Author { get; set; }
}
public class User
{
[Key]
public int UserId { get; set; }
public int AuthUserId { get; set; }
public string DisplayName { get; set; }
public ICollection<Document> Documents { get; set; }
}
Then in the Fluent API configuration as follows:
modelBuilder.Entity<Document>()
.HasOne(p => p.Author)
.WithMany(b => b.Documents)
.HasForeignKey(p => p.AuthorId)
.HasPrincipalKey(b => b.AuthUserId); // <-- here you are specifying `AuthUserId` as `PrincipalKey` in the relation which is not primary key
I have the below domain classes:
public class Product
{
public Guid? Id { get; set; }
public string? Name { get; set; }
}
public class Order
{
public Guid? Id { get; set; }
public string? Address{ get; set; }
public Guid? ProductId{ get; set; }
}
I am not using navigation properties for setting the foreign key but am doing it as below:
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasOne<Product>()
.WithMany()
.HasForeignKey(c => c.ProductId);
}
My problem when I do the update-database, it says that the field ProductId does not exist. This is because it named as Id in table product but in table order it is named as ProductId. Is there a way to map the foreign key with a different name?
Modify the Product class
public class Product
{
public Guid? Id { get; set; }
public string? Name { get; set; }
public ICollection<Order> Orders {get; set;}
}
The relationship will work properly.
Reference: Relationships
At present when I try to delete a Subject, it gets deleted but the rows from the other tables that correspond to this subject are left behind orphaned.
Here is my model
public class SubjectsDbContext : DbContext
{
public virtual DbSet<Subject> Subjects { get; set; }
public virtual DbSet<FaceImage> EnrolledFaces { get; set; }
public virtual DbSet<KVPair> KeyValuePairs { get; set; }
public SubjectsDbContext(string connectionString) : base(connectionString)
{
}
public SubjectsDbContext()
{
}
}
public class Subject
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid SubjectId { get; set; }
[Required]
public virtual FaceImage EnrolledFace {get;set;}
[Required]
public DateTimeOffset EnrolledTime { get; set; }
[Required]
[Column(TypeName = "varchar")]
[StringLength(64)]
public string BiometricId { get; set; }
public virtual ICollection<KVPair> KeyValues { get; set; }
public Subject()
{
KeyValues = new List<KVPair>();
}
}
[Table("SubjectFaces")]
public class FaceImage
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid FaceId { get; set; }
[Required]
public byte[] Image { get; set; }
}
[Table("SubjectData")]
public class KVPair
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid KvPairId { get; set; }
[Required]
[Column(TypeName = "varchar")]
[StringLength(128)]
public string Key { get; set; }
[Column(TypeName = "varchar")]
[StringLength(128)]
public string Value { get; set; }
}
Now when I try to delete the Subject, the rows from the table SubjectFaces and SubjectData are not deleted.
var subject = dbContext.Subjects.Where(a => a.SubjectId == subjectId).FirstOrDefault();
if(subject != null)
{
dbContext.Subjects.Remove(subject);
}
else
{
throw new Exception($"Subject not found");
}
dbContext.SaveChanges();
I think my model is not correct, how can I annotate it correctly?
UPDATE:
After Chris's response I have changed my model to this
public class Subject
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid SubjectId { get; set; }
[Required]
public virtual FaceImage EnrolledFace {get;set;}
[Required]
public DateTimeOffset EnrolledTime { get; set; }
[Required]
[Column(TypeName = "varchar")]
[StringLength(64)]
public string BiometricId { get; set; }
public virtual ICollection<KVPair> KeyValues { get; set; }
public Subject()
{
KeyValues = new List<KVPair>();
}
}
[Table("SubjectFaces")]
public class FaceImage
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid FaceId { get; set; }
[Required]
public byte[] Image { get; set; }
[ForeignKey(nameof(SubjectId))]
public virtual Subject Subject { get; set; }
[Required]
public Guid SubjectId { get; set; }
}
[Table("SubjectData")]
public class KVPair
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid KVPairId { get; set; }
[Required]
[Column(TypeName = "varchar")]
[StringLength(128)]
public string Key { get; set; }
[Column(TypeName = "varchar")]
[StringLength(128)]
public string Value { get; set; }
[ForeignKey(nameof(SubjectId))]
public virtual Subject Subject { get; set; }
[Required]
public Guid SubjectId { get; set; }
}
However when I try to create a new Subject, I get this exception now.
A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column: 'SubjectId'
Been banging my head on this for hours trying different things. :(
I think it's worth reading some basics of EntityFramework at https://www.entityframeworktutorial.net/code-first/cascade-delete-in-code-first.aspx.
Your Model needs some modifications as to perform a cascade delete you will need to cross reference one model in another. This is missing in SubjectFaces table.
There are different ways to structure your entities that make this easier, however to start off with lets add a method to your SubjectsDbContext class to use fluent notation to enforce the cascade delete behaviour:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Normally these conventions should easily target your entire model to support cascade delete
//modelBuilder.Conventions.Add(new OneToManyCascadeDeleteConvention());
//modelBuilder.Conventions.Add(new ManyToManyCascadeDeleteConvention());
// However you many not want the entire model to cascade,
// or due to the relationshipships not being explicitly defined so the above conventions
// may not resolve correctly.
// These statements explicitly define the relationships and the cascade operation
modelBuilder.Entity<Subject>().HasMany(x => x.KeyValues).WithRequired().WillCascadeOnDelete(true);
modelBuilder.Entity<Subject>().HasRequired(x => x.EnrolledFace).WithRequiredPrincipal().WillCascadeOnDelete(true);
}
Your Model seems to have the minimal fields necessary to support it, however you have not included any foreign keys in the Dependent tables so the model does not know that the related records MUST depend on their linking to a Subject.
In fact by NOT specifying the relationship, the model assumes that the related keys that it generates on your behalf are in fact optional, meaning that FaceImage and KVPair records are supposed to exist on their own, not linked to a Subject.
Adding the Foreign Key links in the FaceImage and KVPair reduces the abiguity:
NOTE: only the navigation properties have been added here
public class FaceImage
{
...
[Required]
public virtual Subject Subject { get; set; }
}
public class KVPair
{
...
[Required]
public virtual Subject Subject { get; set; }
}
With this change, to target these relationships in the OnModelCreating method we need to change the fluent notation slightly:
modelBuilder.Entity<Subject>().HasMany(x => x.KeyValues).WithRequired(x => x.Subject).WillCascadeOnDelete(true);
modelBuilder.Entity<Subject>().HasRequired(x => x.EnrolledFace).WithRequiredPrincipal(x => x.Subject).WillCascadeOnDelete(true);
(Or you could just un-comment the lines that add the delete cascade conventions.)
There is a final derivation of this, if you need access to the actual Foreign Key fields that are used to enforce the navigation links, or if this is a Code First implementation and you want to influence the names of these fields, then you simply add those fields into the classes as well:
public class FaceImage
{
...
[Required]
public Guid SubjectId { get; set; }
[ForeignKey(nameof(SubjectId))]
public virtual Subject Subject { get; set; }
}
public class KVPair
{
...
[Required]
public Guid SubjectId { get; set; }
[ForeignKey(nameof(SubjectId))]
public virtual Subject Subject { get; set; }
}
Looking at your code, you still need more work. Entity classes are not complete. Read more about code first entity classes here.
Entity framework initial application
When you refer an entity in Subject class, you also need to refer the Subject in the child entity.
public class Subject
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid SubjectId { get; set; }
[Required]
public virtual long EnrolledFaceId {get;set;} //
[ForeignKey("EnrolledFaceId")]
public virtual FaceImage EnrolledFace {get;set;}
[Required]
public DateTimeOffset EnrolledTime { get; set; }
[Required]
[Column(TypeName = "varchar")]
[StringLength(64)]
public string BiometricId { get; set; }
public virtual ICollection<KVPair> KeyValues { get; set; }
public Subject()
{
KeyValues = new List<KVPair>();
}
}
[Table("SubjectFaces")]
public class FaceImage
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid FaceId { get; set; }
//Add a reference to the Subject
[Required]
public Subject Subject { get; set; }
public long SubjectId { get; set; }
[Required]
public byte[] Image { get; set; }
}
[Table("SubjectData")]
public class KVPair
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid KvPairId { get; set; }
//Add a reference to the Subject
[Required]
public long SubjectId { get; set; }
[ForeignKey("SubjectId")]
public Subject Subject { get; set; }
[Required]
[Column(TypeName = "varchar")]
[StringLength(128)]
public string Key { get; set; }
[Column(TypeName = "varchar")]
[StringLength(128)]
public string Value { get; set; }
}
I have the following models in my .NET Core 2.1 application:
public class Product
{
[Key]
public Guid ProductId { get; set; }
public virtual ICollection<Favourite> Favourites { get; set; }
}
public class Retailer
{
[Key]
public Guid BusinessId {get; set;}
public virtual ICollection<Favourite> Favourites { get; set; }
}
public class Favourite
{
[Key]
public Guid FavouriteId { get; set; }
[ForeignKey("Retailer_BusinessId")]
public virtual Retailer Business { get; set; }
[ForeignKey("Product_ProductId")]
public virtual Product Product { get; set; }
}
When trying to run an EF migration, I get the following error:
The relationship from 'Favourite.Business' to 'Retailer.Favourites'
with foreign key properties {'Retailer_BusinessId' : Nullable}
cannot target the primary key {'BusinessId' : Guid} because it is not
compatible. Configure a principal key or a set of compatible foreign
key properties for this relationship.
I suspect it's because I'm using Guid's as my keys in the foreign tables. How do I tell EF that?
It looks like EF expects the ForeignKey attribute to refer to the navigation property. I was able to reproduce the error you were getting and after making the following change, it migrated successfully.
public class Favourite
{
[Key]
public Guid FavouriteId { get; set; }
[ForeignKey("Business")]
public Guid BusinessId { get; set; }
[ForeignKey("Product")]
public Guid ProductId { get; set; }
public virtual Retailer Business { get; set; }
public virtual Product Product { get; set; }
}
EDIT: The way you were doing it was close, but you need to have the property and then specify just the property's name in the ForeignKey attribute.
public class Favourite
{
[Key]
public Guid FavouriteId { get; set; }
public Guid BusinessId { get; set; }
public Guid ProductId { get; set; }
[ForeignKey("BusinessId")]
public virtual Retailer Business { get; set; }
[ForeignKey("ProductId")]
public virtual Product Product { get; set; }
}
i'm creating a relation between 4 tables: Provider, CostumerSite, DrmType and Drm.
Relation:
in the Drm table there is a composite key formed by the primary keys of the other 3 tables.. this is the Code:
public partial class Provider
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProviderId { get; set; }
[StringLength(128)]
[Required]
public string Title { get; set; }
// one-to-many
public virtual ICollection<Content> Contents { get; set; }
public virtual Drm Drm { get; set; }
}
public partial class CustomerSite
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CustomerSiteId { get; set; }
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
[StringLength(128)]
public string Username { get; set; }
[Required]
[StringLength(128)]
public string Password { get; set; }
// many-to-many
public virtual ICollection<Content> Contents { get; set; }
// one-to-one
public virtual Drm Drm { get; set; }
}
public partial class DrmType
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int DrmTypeId { get; set; }
[Required]
[StringLength(128)]
public string Name { get; set; }
// one-to-one
public virtual Drm Drm { get; set; }
}
public partial class Drm
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int DrmId { get; set; }
// one-to-one relation
[Key, Column(Order = 1), ForeignKey("CustomerSite")]
public int CustomerSiteId { get; set; }
// one-to-one relation
[Key, Column(Order = 2), ForeignKey("Provider")]
public int ProviderId { get; set; }
// one-to-one relation
[Key, Column(Order = 3), ForeignKey("DrmType")]
public int DrmTypeId { get; set; }
public virtual Provider Provider { get; set; }
public virtual CustomerSite CustomerSite { get; set; }
public virtual DrmType DrmType { get; set; }
}
Is this correct? After this, i'm using the onModel function:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Drm>()
.HasKey(d => new { d.ProviderId, d.CustomerSiteId, d.DrmType });
}
But in this way, when i launch the "Enable-Migrations -EnableAutomaticMigrations -Force" command it shows an error:
One or more validation errors were detected during model generation:
Drm_CustomerSite_Source: : Multiplicity is not valid in Role
'Drm_CustomerSite_Source' in relationship 'Drm_CustomerSite'. Because
the Dependent Role properties are not the key properties, the upper
bound of the multiplicity of the Dependent Role must be ''.
Drm_Provider_Source: : Multiplicity is not valid in Role
'Drm_Provider_Source' in relationship 'Drm_Provider'. Because the
Dependent Role properties are not the key properties, the upper bound
of the multiplicity of the Dependent Role must be ''.
Can you explain how to solve this? Thanks
Based on your model, Drm should not have its own "id". So, it should look like this:
public partial class Drm
{
//remove this property
//[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
//public int DrmId { get; set; }
// one-to-one relation
[Key, Column(Order = 1), ForeignKey("CustomerSite")]
public int CustomerSiteId { get; set; }
// one-to-one relation
[Key, Column(Order = 2), ForeignKey("Provider")]
public int ProviderId { get; set; }
// one-to-one relation
[Key, Column(Order = 3), ForeignKey("DrmType")]
public int DrmTypeId { get; set; }
public virtual Provider Provider { get; set; }
public virtual CustomerSite CustomerSite { get; set; }
public virtual DrmType DrmType { get; set; }
}
Since you provided the [Key] attribute at the class declaration, you do not need to declare HasKey in the model binding. So, remove this line:
modelBuilder.Entity<Drm>()
.HasKey(d => new { d.ProviderId, d.CustomerSiteId, d.DrmType });
EDIT
Provider, CustomerSite and DrmType must have a collection of Drm.
public partial class Provider
{
//...
//public virtual Drm Drm { get; set; }
public virtual ICollection<Drm> Drms { get; set; }
}
public partial class CustomerSite
{
//...
//public virtual Drm Drm { get; set; }
public virtual ICollection<Drm> Drms { get; set; }
}
public partial class DrmType
{
//public virtual Drm Drm { get; set; }
public virtual ICollection<Drm> Drms { get; set; }
}
Hope this helps!
The easiest way to solve this is by giving the DRM table its own ID and making the 3 fields Foreign keys.
I do not know to what extend you are normalising but it is something often done to prevent unnecessary complex code.
keep it clean, keep it simple