Cascade delete in one to one relationship - entity-framework

I want to have cascade delete in 1:1 relationship, where i reference multiple entities to one. Problem is throws me an error on database update
Introducing FOREIGN KEY constraint 'FK_dbo.CategoryArticles_dbo.Articles_Article_Id' on table 'CategoryArticles' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
RoutingSeo entity is for storing seo friendly url in database for later usage. My problem is clearly M:N relationship between Article and Category. Is there something how can I deal with this problem?
Here are my entities of my model
public class Article : IEntity<int>
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Category> Categories { get; set; }
[Required]
public virtual RoutingSeo RoutingSeo { get; set; }
public int RoutingSeoId { get; set; }
}
public class Category : IEntity<int>
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Article> Articles { get; set; }
[Required]
public virtual RoutingSeo RoutingSeo { get; set; }
public int RoutingSeoId { get; set; }
}
public class SpecificProduct : IEntity<int>
{
public int Id { get; set; }
public string Name { get; set; }
[Required]
public RoutingSeo RoutingSeo { get; set; }
public int RoutingSeoId { get; set; }
}
public class RoutingSeo : IEntity<int>
{
public int Id { get; set; }
public string SeoRoute { get; set; }
public Article Article { get; set; }
public SpecificProduct SpecificProduct { get; set; }
public Category Category { get; set; }
}
Here is my fluent api code where i specify cascade delete
modelBuilder.Entity<Article>()
.HasRequired(x => x.RoutingSeo)
.WithOptional(x=>x.Article)
.WillCascadeOnDelete();
modelBuilder.Entity<Category>()
.HasRequired(x => x.RoutingSeo)
.WithOptional(x=>x.Category)
.WillCascadeOnDelete();
modelBuilder.Entity<SpecificProduct>()
.HasRequired(x => x.RoutingSeo)
.WithOptional(x=>x.SpecificProduct)
.WillCascadeOnDelete();

You are right, it is your many-to-many relation ship between Article and Category: one Article has zero or more Categories and every Category may be used by zero or more Articles.
If you delete an Article, its Categories can't be deleted automatically, because the Category might be used by other Articles, and even if it isn't used right now, entity framework doesn't know whether you want to use it tomorrow. After all, you specified that every Category might be used by zero or more Articles.
Similarly, if you remove a Category, entity framework can't automatically remove the Articles belonging to this category.
This differs from a one-to-many relationship. For example, if you have a one-to-many relationship of a Book and its Pages, then every Book has zero or more Pages and every Page belongs to exactly one Book.
If you remove the Book, then entity framework knows that it should automatically remove all Pages of the Book, which are all Pages with a foreign key BookId. If Entity Framework would only remove the Book, then we would have a bunch of Pages with foreign key value pointing to a non-existing Book. So in one-to-many relations, entity framework can cascade on delete.
Alas, in many-to-many this is not possible.
On the bright side, you have the advantage that you can delete the last Article of a Category, and keep the Category intact. Tomorrow you can add a new Article that uses this Category.
So if you want to remove an article, you manually have to remove it from the 'Categories` it uses:
many-to-many following the standard naming conventions:
class Article
{
public int Id {get; set;}
// an Article belongs to zero or more Categories:
public virtual ICollection<Category> Categories {get; set;}
...
}
class Category
{
public int Id {get; set;}
// a Category is used by zero or more Articles:
public virtual ICollection<Article> Articles{get; set;}
...
}
Don't forget to declare your ICollections virtual!
class MyDbContext : DbContext
{
public class DbSet<Article> Articles {get; set;}
public class DbSet<Category> Categories {get; set;}
}
You don't have to mention the junction-table, entity framework will make it automatically for you, but you won't have to use it for joins if you want Articles with their Categories, or Categories with their Articles, just use the ICollections
Note: As Categories is not the expected plural of Category, you'll have to tell entity framework the proper table name. Out of scope of this question.
Delete an Article, but keep all Categories it belongs to alive:
using (var dbContext = new MyDbContext(...))
{
Article articleToRemove = ...
dbContext.Articles.Remove(articleToRemove);
dbContext.SaveChanges();
}
Entity framework will automatically perform the proper joins, and remove the articleToRemove from every Category. However, the Categories won't be removed.
In fact, internally the Categories table doesn't change at all. All records with Article.Id will be removed from the junction table.

Related

EF Core 3.1.7 Data annotations for multiple 1:1 relationships in table

I am having problems figuring out the data annotations to map more than one 1:1 relationships so that EF Core 3.11.7 understands it and can build a migration.
I have a Person table and a Notes table.
There is a 0:M Notes relationship in Person. A person record can have 0 or more notes.
In the notes table is a CreatedBy field which is a Person. It also has a LastEditedBy field which is also a person. EF keeps bombing on how to construct the relationship for Note.CreatedBy. If this were non EF, both fields would be ints with the PersonID of the proper person record. How do it, preferabbly with Data Annotations, explain this to EF Core?
When I try to create a migration it fails and says:
System.InvalidOperationException: Unable to determine the relationship represented by navigation property 'Note.CreatedBy' of type 'Person'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace VetReg.Domain.Model
{
public class Family
{
public int FamilyID { get; set; } = -1;
public string FamilyName { get; set; }
public List<Pet> Pets { get; set; } = new List<Pet>();
public List<PersonFamily> People { get; set; }
public int AddressID { get; set; } = -1;
public List<Note> Notes { get; set; }
}
public class Person
{
public int PersonID { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public DateTime? Birthdate { get; set; }
public string Password { get; set; }
public List<PersonFamily> Families { get; set; }
public List<Note> Notes { get; set; }
} // class People
public class Note
{
public int NoteID { get; set; }
public int CreatedByID { get; set; }
[ForeignKey("CreatedByID")]
public Person CreatedBy { get; set; }
public DateTime DateCreated { get; set; }
public int LastEditByID { get; set; }
[ForeignKey("LastEditByID")]
public Person LastEditBy { get; set; }
public DateTime? LastEditDate { get; set; }
public string NoteText { get; set; }
}
public class PersonFamily
{
public int PersonID { get; set; }
public int FamilyID { get; set; }
public Person Person { get; set; }
public Family Family { get; set; }
}
}
The question is (and this is what makes impossible to EF to automatically determine the relationships) what is the relation between Person.Notes and Note.CreatedBy / Note.LastEditBy - none probably? You've said there is 0:M relationship between Person and Note, but note that there are potentially 3 one-to-many relationships there - notes associated with person, notes created by person and notes edited by person, which potentially leads to 3 FKs to Person in Note.
Also note that none of the navigation properties is required, but when present they must be paired.
Assuming you want 3 relationships, i.e. there is no relation between Note.CreatedBy / Note.LastEditBy and Person.Notes, you need to tell EF that Note.CreatedBy and Note.LastEditBy do not have corresponding (a.k.a. inverse) navigation property in Person. This is not possible with data annotations. The only available data annotation for that purpose [InverseProperty(...)] does not accept empty/null string name, hence cannot be used for what is needed here.
Also there is another problem here which you will encounter after resolving the current, which also cannot be resolved with data annotations. Since you have multiple required (thus cascade delete by default) relationships from Person to Note, it creates the famous "cycles or multiple cascade paths" problem with SqlServer, and requires turning off at least one of the cascade delete.
With that being said, the model in question needs the following minimal fluent configuration:
modelBuilder.Entity<Note>()
.HasOne(e => e.CreatedBy)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Note>()
.HasOne(e => e.LastEditBy)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
The essential for the original issue are the HasOne / WithMany pairs. Once you do that, EF Core will automatically map the unmapped Person.Notes collection to a third optional relationship with no inverse navigation property and shadow FP property (and column) called "PersonId", i.e. the equivalent of
modelBuilder.Entity<Person>()
.HasMany(e => e.Notes)
.WithOne()
.HasForeignKey("PersonId");
Regarding the second issue with multiple cascade paths, instead of Restrict you can use any non cascading option or the newer ClientCascade. And it could be for just one of the relationships, as soon as it breaks the "cascade path" (apparently you can't break the cycle because it is demanded by the model).

One to many relationship: why does ID field go in other entity?

In Entity Framework when I want to specify that an entity has many of another type of entity it seems to do things backwards to me.
For instance let's say I have a keyword entity that is used in several places throughout my app. All keywords are unique but on my other entities I want to have multiple keywords attached them so to me this would make sense:
class Page
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Keyword> Keywords { get; set; }
}
class Search
{
public int ID { get; set; }
public DateTime Date { get; set; }
public virtual ICollection<Keyword> Keywords { get; set; }
}
class Keyword
{
public int ID { get; set; }
public string Name { get; set; }
}
However when I do this the foreign key is added to the Keyword table whereas I want it to be on the actual entity so I can look at it in database and see small list of keywords instead of looking at keyword and seeing a ridiculous large number of page results.
So instead to get Entity Framework to put the Keyword_IDs on Page and Search entities I am doing this:
class Page
{
public int ID { get; set; }
public string Name { get; set; }
}
class Search
{
public int ID { get; set; }
public DateTime Date { get; set; }
}
class Keyword
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Page> Pages { get; set; }
public virtual ICollection<Search> Searches { get; set; }
}
This feels backwards as I am specifying the relationship on the entity that doesn't get the foreign ID field in the database table.
I feel like I am doing something wrong as I should be able to see the relationship by looking at my search & page class.
I am sorry for the basic question but for some reason I have read documentation and I am not fully understanding it.
In a one-to-many association it's always the many side that refers to the one side. How else would you implement it? If a Page would have a KeywordId as FK, it could only have one keyword, ever.
Also, even when a Keyword would belong to a myriad of pages, that doesn't mean you always have to access all of these pages through one keyword. You'd only do that if you'd do a search for pages in which specific keywords are used.
But now back to your model. You can't have one-to-many associations here. It would mean that any keyword can only belong to one Page or one Search. And if you invert the relationship, as you proposed, a Page or Search can only ever have one keyword (the one that Keyword_ID refers to).
In reality, you're dealing with many-to-many associations and the good news is, it leaves your Keyword intact.
Modelling it as many-to-many doesn't change the way your model looks (the first version), but the mapping is different:
modelBuilder.Entity<Page>().HasMany(p => p.Keywords)
.WithMany()
.Map(m =>
{
m.ToTable("PageKeyword");
m.MapLeftKey("PageID");
m.MapRightKey("KeywordID");
});
modelBuilder.Entity<Search>().HasMany(s => s.Keywords)
.WithMany()
.Map(m =>
{
m.ToTable("SearchKeyword");
m.MapLeftKey("SearchID");
m.MapRightKey("KeywordID");
});
This will generate two junction tables in your database, PageKeyword and SearchKeyword that record the many-to-many associations.

code first one-to-one enable cascade delete

I have one to one relationship with foreign keys but the Cascade Delete is not enabled for some reason. The sample code is below.
public class AppRegistration
{
public int AppRegistrationId { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Username")]
public string UserName { get; set; }
[Required]
[StringLength(100)]
public string Password { get; set; }
[StringLength(20)]
public string StudentOrAgent { get; set; }
// navigation properties
public virtual AppStatus AppStatus { get; set; }
public virtual Agreement Agreement { get; set; }
public virtual AnotherTable AnotherTable { get; set; }
}
The dependent table with a foreign key is below.
public class Agreement
{
[Key]
[ForeignKey("AppRegistration")]
public int AppRegistrationId { get; set; }
public DateTime DateAgreed { get; set; }
public virtual AppRegistration AppRegistration { get; set; }
}
When I try to delete an entry from the generated AppRegistrations table I get a Reference constraint conflict.
I tried putting [Required] on the navigation property in the dependent table but it doesn't do anything - the Update-Database command shows the No pending code-based migrations. message. Any ideas? Thanks.
Update:
I'm getting the following error message:
The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.AppStatus_dbo.AppRegistrations_AppRegistrationId". The conflict occurred in database "MVCapp", table "dbo.AppStatus", column 'AppRegistrationId'.
I decided to work out the cascade delete problem in a separate sample project. I found the following blog & MSDN pages very useful.
http://blog.bennymichielsen.be/2011/06/02/entity-framework-4-1-one-to-one-mapping/
http://msdn.microsoft.com/en-us/library/gg671256%28v=VS.103%29.aspx
http://msdn.microsoft.com/en-us/library/gg671273%28v=VS.103%29.aspx
Using the Code First approach create the following Model.
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public virtual Book Book { get; set; }
}
public class Book
{
public int CategoryId { get; set; }
public string BookTitle { get; set; }
public string BookAuthor { get; set; }
public string BookISBN { get; set; }
public virtual Category Category { get; set; }
}
(I realize the entity names suggest one-to-many relationship, but I am trying to model 1-to-1 relationship, as in my original question at the top.)
So, in the above model each Category can only have one Book.
In your DbContext-derived class add the following.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Book>()
.HasKey(t => t.CategoryId);
modelBuilder.Entity<Category>()
.HasRequired(t => t.Book)
.WithRequiredPrincipal(t => t.Category)
.WillCascadeOnDelete(true);
}
(The following namespaces are required for the above code: System.Data.Entity, System.Data.Entity.ModelConfiguration.Conventions.)
This properly creates the 1-to-1 relationship. You'll have a primary key in each table and also a foreign key in Book table with ON DELETE CASCADE enabled.
In the above code, on the Category entity I used WithRequiredPrincipal() with t => t.Category argument, where the argument is the foreign key column in the dependent table.
If you use WithRequiredPrincipal() without an argument you'll get an extra column in the Book table and you'll have two foreign keys in the Book table pointing to CategoryId in Category table.
I hope this info helps.
UPDATE
Later on I found answer directly here:
http://msdn.microsoft.com/en-us/data/jj591620#RequiredToRequired
A reason why you're not getting cascading delete is because your relationship is optional.
If you want the relationship required i.e. an AppRegistration has to have one Agreement you can use (cascading delete configured automatically):
public class Agreement
{
...
[Required]
public AppRegistration AppRegistration{ get; set; }
}
If you want the relationship to be optional with cascading delete you can configure this using Fluent API:
modelBuilder.Entity<AppRegistration>()
.HasOptional(a => a.Agreement)
.WithOptionalDependent()
.WillCascadeOnDelete(true);

Entity Framework TPH Inheritance Data Modeling Issues

I'm new to Entity Framework and C#/.Net and trying to create a TPH inheritance model, I'm not sure if I should be or not, so if not, please advise,
Here's the model:
public abstract class Vote
{
public int VoteID { get; set; }
public int UserID { get; set; }
public bool Value { get; set; }
public DateTime DateCreated { get; set; }
public User User { get; set; }
}
public class ProjectVote_ : Vote
{
public int ProjectID { get; set; }
public virtual Project Project { get; set; }
}
public class CommentVote_ : Vote //There are three more like this, votes for different hings
{
public int CommentID { get; set; }
public virtual Comment Comment { get; set; }
}
Now the Project model (comment and model is similar)
public class Project
{
public int ProjectID { get; set; }
public string Title { get; set; }
public virtual ICollection<Vote> Vote { get; set; }
}
What happens is that ICollection creates a database column Project_ProjectID as the foreign key in the Vote table (I think) instead of using the ProjectID I defined. How do I fix it or should I model it differently. If the fluent API is the way to fix it, I don't know how to do that.
In the end I want to be able to use one table to store 5 different types of votes.
When you have related entities you don't need to have a property to store the FK in your model. Entity framework knows that it needs to make a FK to the Project table in ProjectVote when it detects Project in your ProjectVote_ model. Same thing with User and UserId and Comment and CommentId. You don't need to have a property that stores the FK in your model.
You are getting the FK column with the name you don't like "Project_ProjectID" because Entity framework is detecting that it needs to create a FK for your navigation property "Project". It's using it's own naming convention to create the column hence "Project_ProjectID".
If you want to provide your own name for the column override OnModelCreating in your DBContext class and add this fluent mapping.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Project>()
.HasMany(p => p.Vote)
.HasRequired(v => v.Project) //or .WithOptional(v => v.Project)
.Map(m => m.MapKey("ProjectId")); //or any other name you want.
}
And for the future this is a helpful reference for how to use the Fluent API. For example here is some documentation on how to custimize TPH with fluent.
Hope that helps!

Entity Framework asp.net MVC foreign key

I am trying to code the following in code first... since I am just begining I am not able to.. please help.. thanks in advance
1. Student: Student will have student ID, First Name, Last Name
Student should belong to one class and one section(basically one to one relationship with each entity)
2. Classes: Class will have ClassId, Name
Class should have collection of students and collection of sections(basically many to many relationship with each entity)
3. Sections: Section will have SectionID, Name
Section should belong to one class and should have collection of students(basically one to one relation with class and one to many relation with Students)
Below is the code for the same
Students.cs
public class Students
{
public int StudentsId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public decimal Grade { get; set; }
public int ClassesId { get; set; }
public Classes Classes { get; set; }
public int SectionsId { get; set; }
public Sections Sections { get; set; }
}
Classes.cs
public class Classes
{
public int ClassesId { get; set; }
public string Name { get; set; }
public ICollection<Sections> Sections { get; set; }
}
Sections.cs
public class Sections
{
public int SectionsId { get; set; }
public string Name { get; set; }
public int ClassesId { get; set; }
public Classes Classes { get; set; }
public ICollection<Students> Students { get; set; }
}
If I do this I get error saying:
Introducing FOREIGN KEY constraint
'FK_dbo.Sections_dbo.Classes_ClassesId' on table 'Sections' may cause
cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON
UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
I know I can get rid of this error using fluent APIs and telling not to cascade on delete, but I don't want to do that. Is there any other solution to this?? Please help
With your current model, no, there is no other way than disabling casdading delete for some of the relationships.
All your relationships are required, that means that if a class is deleted you delete the sections and the students of that class (Classes has a not exposed collection of students due to the required navigation property Classes in Students). But if the sections are deleted the students of that sections are deleted as well - and that's the second delete path to Students.
I don't know the exact meaning of your model but to me it sounds strange to delete all students of a class if the class gets deleted. Does a student always must have a class or couldn't he temporarily be without class assignment (and section assignment as well)? Maybe the student has a holiday semester for half a year and doesn't participate in any class?
In that case you could make the relationships of Students optional. Just declare the foreign key properties as nullable:
public class Students
{
//...
public int? ClassesId { get; set; }
public Classes Classes { get; set; }
public int? SectionsId { get; set; }
public Sections Sections { get; set; }
}
This would fix your problem of multiple cascading delete paths in the Students class because by default optional relationships don't have cascading delete enabled. The relationship between Classes and Sections is still required, so deleting a class will delete all sections belonging to the class as well, but it won't delete the students anymore.