Entity Framework not deleting rows from other tables - c#

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; }
}

Related

Adding model to database that has virtual relations

I have this scenario where i have a model named Category that has a One-To-One relation with another model named Question. The following is the code for both of the models:
Category:
public class Category
{
[Key]
[Required]
public int Id { get; set; }
[Required]
public int QuestionRef { get; set; }
[Required]
public string Name { get; set; }
[Required]
public DateTime Timestamp { get; set; }
public virtual Question Question { get; set; }
}
Question:
public class Question
{
[Required]
[Key]
public int Id { get; set; }
[Required]
public int CategoryRef { get; set; }
[Required]
public int QuestionLevel { get; set; }
[Required]
public string QuestionText { get; set; }
[Required]
public DateTime Timestamp { get; set; }
public virtual Category Category { get; set; }
}
Snippet from OnModelCreating:
modelBuilder.Entity<Category>().HasOne(x => x.Question).WithOne(x => x.Category).HasForeignKey<Question>(x => x.CategoryRef);
modelBuilder.Entity<Question>().HasOne(x => x.Category).WithOne(x => x.Question).HasForeignKey<Category>(x => x.QuestionRef);
I seem to have come across a problem that i cannot understand how to tackle. Basically, i do not have a clue of how i can insert this relation into the database (SQL Server).
The question is do i have to implicitly write in the repository that
NewCategory.Question = QuestionInput;
or does it automatically generate itself given the relations i have implied in the OnModelCreating?

Entity Framework Migration - Alter Table Statement Conflicted with Foreign Key Constraint

I am creating a blog of sorts with C#/.NET 4.5 framework using EF code-first migrations.
Things have been going well up until adding a third relationship into my main class.
I have a "story" class (kind of like a "post" for a blog) where I have the author as the user who's logged in (set in the controller), a title, some content, the date it was created, and a genre and type of story.
public class Story
{
public int Id { get; set; }
public string AuthorId { get; set; }
public virtual ApplicationUser Author { get; set; }
[Required]
[StringLength(50)]
public string Title { get; set; }
[Required]
[MinLength(100), MaxLength(5000)]
public string Content { get; set; }
[Required]
public int GenreId { get; set; }
public Genre Genre { get; set; }
[Required]
public int StoryTypeId { get; set; }
public StoryType StoryType { get; set; }
public DateTime CreatedAt { get; set; }
}
I added storytypes as a property to the story. StoryType links to the StoryType model:
public class StoryType
{
public int Id { get; set; }
public string Name { get; set; }
}
I made sure to add my dbset to my application db context:
public DbSet<Genre> Genres { get; set; }
public DbSet<Story> Stories { get; set; }
public DbSet<StoryType> StoryTypes { get; set; }
I pretty much followed the same steps I used to create the relationship between the story and genre (which worked fine). Before I start building the StoryType controllers, I went into package-console and ran:
add-migration
that returned:
public partial class CreateTypeTable : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.StoryTypes",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
})
.PrimaryKey(t => t.Id);
AddColumn("dbo.Stories", "StoryTypeId", c => c.Int(nullable: false));
CreateIndex("dbo.Stories", "StoryTypeId");
AddForeignKey("dbo.Stories", "StoryTypeId", "dbo.StoryTypes", "Id", cascadeDelete: true);
}
public override void Down()
{
DropForeignKey("dbo.Stories", "StoryTypeId", "dbo.StoryTypes");
DropIndex("dbo.Stories", new[] { "StoryTypeId" });
DropColumn("dbo.Stories", "StoryTypeId");
DropTable("dbo.StoryTypes");
}
}
Glancing over it, I didn't see an issue, then ran:
update-database
in package-console.
Which returned:
Error Number:547,State:0,Class:16
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Stories_dbo.StoryTypes_StoryTypeId". The conflict occurred in database "aspnet-HiRatik.Stories-20180724043630", table "dbo.StoryTypes", column 'Id'.
I'm not sure what went wrong here. I did the same process with the Genre relationship and it worked. I didn't see a difference in the two.
because the StoryTypeId in the class Story dosen't accept null so you need to make the StoryTypeId nullable :
public class Story
{
public int Id { get; set; }
public string AuthorId { get; set; }
public virtual ApplicationUser Author { get; set; }
[Required]
[StringLength(50)]
public string Title { get; set; }
[Required]
[MinLength(100), MaxLength(5000)]
public string Content { get; set; }
[Required]
public int GenreId { get; set; }
public Genre Genre { get; set; }
public int? StoryTypeId { get; set; }
public StoryType StoryType { get; set; }
public DateTime CreatedAt { get; set; }
}
or you create first the table StoryType and you add elements to it and then add the StoryTypeId with default value:
public class Story
{
public int Id { get; set; }
public string AuthorId { get; set; }
public virtual ApplicationUser Author { get; set; }
[Required]
[StringLength(50)]
public string Title { get; set; }
[Required]
[MinLength(100), MaxLength(5000)]
public string Content { get; set; }
[Required]
public int GenreId { get; set; }
public Genre Genre { get; set; }
[[DefaultValue(1)]]
public int StoryTypeId { get; set; }
public StoryType StoryType { get; set; }
public DateTime CreatedAt { get; set; }
}
in this case you must update the database after creating StoryType and the after adding the StoryTypeId to the class Story
You can make the foreign key nullable by using a sign ?, like this: int?.
In my case, I was getting this because there was data in the table on which I was creating the foreign key that's why I was getting this error. And as we know foreign key is nullable.

Entity Framework 0:1 relationship isn't being mapped

I have a model of Job that I'm trying to add an optional job survey to:
public class Job
{
[Key]
public int JobID { get; set; }
// Leaving out all the other fields...
public virtual JobSurvey JobSurvey { get; set; }
}
The job survey model looks like this:
public class JobSurvey
{
[Key]
public int JobSurveyId { get; set; }
public string CustomerEmail { get; set; }
[Index]
[Column(TypeName = "Date")]
public DateTime? SentDate { get; set; }
[Column(TypeName = "Date")]
public DateTime? ReturnedDate { get; set; }
public int? RatingValue { get; set; }
public virtual Job Job { get; set; }
}
In my context I've added the following:
modelBuilder.Entity<Job>()
.HasOptional(s => s.JobSurvey)
.WithRequired(j => j.Job);
When I ran add-migration, the script created the following:
CreateTable(
"dbo.JobSurveys",
c => new
{
JobSurveyId = c.Int(nullable: false),
CustomerEmail = c.String(nullable: false, maxLength: 100),
SentDate = c.DateTime(storeType: "date"),
RatingValue = c.Int(),
})
.PrimaryKey(t => t.JobSurveyId)
.ForeignKey("dbo.Jobs", t => t.JobSurveyId)
My problem is that the table that was created, has no foreign key property to be able to go to the related entity.
So in my SQL, there is no JobSurveyId property that lets me get the survey for a job, and my JobSurvey table doesn't have a navigation property going back to the Job. So I can create JobSurveys, but they're not linked.
What have I done wrong?
Edit
I've tried modifying JobSurvey as follows:
[Key]
[ForeignKey("Job")]
public int JobSurveyId { get; set; }
public int JobId { get; set; }
no success
Edit 2
Have also tried adding [Required] to the navigation property, but add-migration isn't picking up this as a change that needs updating:
[Required]
public virtual Job Job { get; set; }
Since I would have to write a lot in the comment, try this, its according to the link I replied but I don't think you followed it right:
public class JobSurvey
{
[Key]
public int JobSurveyId { get; set; }
public string CustomerEmail { get; set; }
[Index]
[Column(TypeName = "Date")]
public DateTime? SentDate { get; set; }
[Column(TypeName = "Date")]
public DateTime? ReturnedDate { get; set; }
public int? RatingValue { get; set; }
[ForeignKey("Job")]
public int JobId { get; set; }
public virtual Job Job { get; set; }
}
Edit
I'm not entirely sure whether you want 0:1 or 1:1 so here are all the possibilities (in links):
1:1
Entity Framework 1 to 1 relationship using code first. how?
EF Code-First One-to-one relationship: Multiplicity is not valid in Role * in relationship
0:1
Is it possible to capture a 0..1 to 0..1 relationship in Entity Framework?
Entity Framework 0..1 to 0 relation
Basically this could be your solution:
public class JobSurvey
{
[Key, ForeignKey("First")]
public int JobSurveyId { get; set; }
public string CustomerEmail { get; set; }
[Index]
[Column(TypeName = "Date")]
public DateTime? SentDate { get; set; }
[Column(TypeName = "Date")]
public DateTime? ReturnedDate { get; set; }
public int? RatingValue { get; set; }
public virtual Job Job { get; set; }
}
In one-to-one or one-to-zero or one relationships priamry key in the dependent model is also foreign key to parent model. There is no need for additional foreign key because there can only be one data linked to parent, it is reasonable to link them with thier IDs. So in your case JobSurveyId inside JobSurvey model is also foreign key to Job.
public class Job
{
[Key]
public int JobID { get; set; }
public virtual JobSurvey JobSurvey { get; set; }
}
public class JobSurvey
{
[Key]
public int JobSurveyId { get; set; } // This is also foreign key to Job
public virtual Job Job { get; set; }
}
Be sure to check this site out.
you job class is okay, you only have to add ForeignKey attribute to your JobSurvey to the correct id property:
public class Job
{
[Key]
public int JobID { get; set; }
public virtual JobSurvey JobSurvey { get; set; }
}
public class JobSurvey
{
[Key, ForeignKey("Job")]
public int JobId { get; set; }
public Job Job { get; set; }
/* other properties */
}

Error: Unable to determine composite primary key ordering for type

I try to scaffold a controller for my ClientModel and it gives me an error called:
There was an error running the selected code generator: 'Unable to retrieve metadata for '/////.ClientModel'. Unable to determine composite primary key for ordering for type '/////.ClientModel'. Use the ColumnAttribute (link) or the HasKey method (link) to specify an order for composite primary keys.
I have three classes with all of them an One-To-One relationship with my Client class.
This is my ClientModel:
[Table("Client")]
public class ClientModel : PersonModel
{
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ClientId { get; set; }
[Required]
public string FirstName { get; set; }
public string Initials { get; set; }
[Required]
public string LastName { get; set; }
//... snip ...
[Required]
public long ClientIdentificationNumber { get; set; }
public virtual PassportDetailsModel Passport { get; set; }
public virtual MembershipClientValidationModel Membership { get; set; }
public virtual AccountClientModel Account { get; set; }
}
Passport class:
[Table("Passport")]
public class PassportDetailsModel
{
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid PassportDetailsId { get; set; }
[Required]
public string IssuingCountry { get; set; }
[Required]
public DateTime Issued { get; set; }
[Required]
public DateTime ExpirationDate { get; set; }
public virtual ClientModel Client { get; set; }
}
Account class:
[Table("AccountClient")]
public class AccountClientModel
{
[Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid AccountId { get; set; }
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
public virtual ClientModel Client { get; set; }
}
Membership class:
[Table("MembershipClientValidation")]
public class MembershipClientValidationModel
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid MembershipClientValidationId { get; set; }
public DateTime MembershipStartDate { get; set; }
public DateTime MembershipExpiryDate { get; set; }
public bool IsMembershipValid { get; set; }
[Required]
public virtual ClientModel Client { get; set; }
}
I don't understand how to fix the 'unable to retrieve metadata for <class> error. How do I link classes with composite keys?
21-09-2015
After reading some comments I deleted the
DatabaseGenerated.DatabaseOption.Identity
In my primary key's in the other classes (Passport, Membership and Account). So I added some properties to my ClientModel:
[ForeignKey("Passport")]
public Guid PassportDetailsId { get; set; }
public virtual PassportDetailsModel Passport { get; set; }
[ForeignKey("Membership")]
public Guid MembershipClientValidationId { get; set; }
public virtual MembershipClientValidationModel Membership { get; set; }
[ForeignKey("Account")]
public Guid AccountClientId { get; set; }
public virtual AccountClientModel Account { get; set; }
It still gives me the same error as it gave me above.
I fixed this problem by doing some simple changes.
I removed my three [ForeignKey("//")] in my ClientModel. I added the Foreign keys to my Primary keys in my other 3 models with reference to my ClientModel.
What I did next was the major fix. I added [Required] above the public ClientModel Client { get; set; } in my other three models.
Now it works and scaffolds my ClientController without error(s).
So to show an example of one of the other three models:
public class MembershipClientValidationModel
{
[Key]
[ForeignKey("Client")]
public Guid ClientId { get; set; }
public DateTime MembershipStartDate { get; set; }
public DateTime MembershipExpiryDate { get; set; }
public bool IsMembershipValid { get; set; }
[Required]
public virtual ClientModel Client { get; set; }
}
It just works.

How to define a table that its primary key is constructed from 2 foreign keys with EF code-first

I am using models to define my tables using EF code-first.
I have and Item model and an Order model.
Item:
public class Item
{
public int ItemID { get; set; }
[Required]
public int Price { get; set; }
[Required]
public int AmountLeft { get; set; }
[Required]
public string Image { get; set; }
[Required]
public string Description { get; set; }
[Required]
public string FullDescription { get; set; }
[Required]
public DateTime PublishDate { get; set; }
public int CompanyID { get; set; }
public int CategoryID { get; set; }
// Navigation properties
public virtual Company Company { get; set; }
// Navigation properties
public virtual Category Category { get; set; }
}
Order model:
public class Order
{
public int OrderID { get; set; }
[Required]
public DateTime DeliveryDate { get; set; }
[Required]
public string Currency { get; set; }
[Required]
public int TotalAmount { get; set; }
public List<int> Items { get; set; }
public DateTime OrderDate { get; set; }
public int UserID { get; set; }
// Navigation properties
public virtual User user { get; set; }
}
I want to create another table which will be called ItemInOrder which will only have 2 fields: ItemID and OrderID.
the primary key would be these 2 foreign keys.
i tried to define this model:
public class ItemInOrder
{
public int OrderID { get; set; }
public int ItemID { get; set; }
// Navigation properties
public virtual Order order { get; set; }
public virtual Item item { get; set; }
}
But i got errors. i tried to put [Key] notation on both fields but still i got errors.
how will i be able to create the table i want?
When you need to create a table with composite PKs, you need to specify the order of you keys. There are two variants:
You could override the OnModelCreating method on your Context, and try with these Fluent Api configurations:
// Configure the primary keys for the ItemInOrder in the order you want
modelBuilder.Entity<ItemInOrder>()
.HasKey(t => new{t.OrderID,ItemID);
modelBuilder.Entity<ItemInOrder>()
.HasRequired(io=>io.Order)
.WithMany()
.HasForeigKey(io=>io.OrderID);
modelBuilder.Entity<ItemInOrder>()
.HasRequired(io=>io.Item)
.WithMany()
.HasForeigKey(io=>io.ItemID);
The second variant using Data Annotations should be this way:
[Key]
[Column(Order=1)]
[ForeignKey("Order")]
public int OrderID { get; set; }
[Key]
[Column(Order=2)]
[ForeignKey("Item")]
public int ItemID { get; set; }
EF will notice you want to create two relationships and it will do the job for you.

Categories