EntityFramework Code-First: How to prevent cascade delete on one side of a many-to-many relationship? - ef-code-first

Is it possible to prevent cascade delete from a single way with Entity Framework Code-First.
Here is a sample code:
public sealed class ZeContext : DbContext
{
static ZeContext()
{
Database.SetInitializer(new CreateDatabase());
}
public IDbSet<A> As { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<A>()
.ToTable("A")
.HasMany<B>(a => a.Bs)
.WithMany(b => b.As)
;
modelBuilder.Entity<B>()
.ToTable("B")
.HasMany<A>(b => b.As)
.WithMany(a => a.Bs)
;
base.OnModelCreating(modelBuilder);
}
}
public class A
{
public Guid Id { get; set; }
public virtual ICollection<B> Bs { get; set; }
}
public class B
{
public Guid Id { get; set; }
public virtual ICollection<A> As { get; set; }
}
I would like to configure the following behaviour:
When deleting a record from table A, it should remove the corresponding association records from the internal many-to-many table created by EntityFramework.
However, when deleting a record from table B, it should fail if there is a corresponding association record in the internal many-to-many table.

Related

FK for composite key splitted into 2 when table with certain name is added

So I have the following entities defined.
internal class DeliveryArea
{
public string Postcode { get; set; }
public string State { get; set; }
public string Country { get; set; }
public ICollection<DeliveryPrice> HasDeliveryPrices { get; set; }
}
internal class DeliveryPrice
{
public uint Id { get; set; }
public DeliveryArea ForDeliveryArea { get; set; }
public string DeliveryAreaPostcode { get; set; }
public string DeliveryAreaState { get; set; }
public string DeliveryAreaCountry { get; set; }
}
and my DbContext is as follow
internal class MyDbContext : DbContext
{
// DbSets.
public DbSet<DeliveryArea> DeliveryAreas { get; set; }
public DbSet<DeliveryPrice> DeliveryPrices { get; set; }
// Overrides.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(#"Data Source=Test.EFCore.db;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region DeliveryArea.
{
var entity = modelBuilder.Entity<DeliveryArea>();
// Setup case-insensitive columns.
entity.Property(i => i.Postcode).HasColumnType("TEXT COLLATE NOCASE");
entity.Property(i => i.State).HasColumnType("TEXT COLLATE NOCASE");
entity.Property(i => i.Country).HasColumnType("TEXT COLLATE NOCASE");
// Setup composite PK.
entity.HasKey(nameof(DeliveryArea.Postcode), nameof(DeliveryArea.State), nameof(DeliveryArea.Country));
}
#endregion
#region DeliveryPrice.
{
var entity = modelBuilder.Entity<DeliveryPrice>();
// DeliveryPrice x DeliveryArea | many-to-one
entity.HasOne(left => left.ForDeliveryArea)
.WithMany(right => right.HasDeliveryPrices)
.HasForeignKey(left => new { left.DeliveryAreaPostcode, left.DeliveryAreaState, left.DeliveryAreaCountry });
}
#endregion
}
}
When the database is generated, EF Core manage to generate appropriate FK that connects both table using the composite key. Everything looks fine and the diagram looks great.
Now, I added the following entity
internal class Currency
{
public uint Id { get; set; }
public ICollection<DeliveryPrice> ForDeliveryPrices { get; set; }
}
and updated DeliveryPrice class as follow
internal class DeliveryPrice
{
public uint Id { get; set; }
// Add the following
public Currency HasCurrency { get; set; }
public uint HasCurrencyId { get; set; }
public DeliveryArea ForDeliveryArea { get; set; }
public string DeliveryAreaPostcode { get; set; }
public string DeliveryAreaState { get; set; }
public string DeliveryAreaCountry { get; set; }
}
My DbContext is updated on top of existing, as follow
internal class MyDbContext : DbContext
{
// DbSets.
public DbSet<Currency> Currencies { get; set; }
// Existing codes remain...
// Overrides.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region Currency.
{
var entity = modelBuilder.Entity<Currency>();
// Currency x DeliveryPrice | one-to-many
entity.HasMany(left => left.ForDeliveryPrices)
.WithOne(right => right.HasCurrency)
.HasForeignKey(right => right.HasCurrencyId);
}
#endregion
// Existing codes remain...
}
}
When the new database is generated, the FK that connects both DeliveryArea and DeliveryPrice table is kinda splitted into 2, as follow
The funny thing is that when the Currencies table is renamed to, say Foo, the FK that connects both DeliveryArea and DeliveryPrice table looks OK.
UPDATE 01:
Normal looking FK
Here's a screenshot of the generated FK that splitted into 2
UPDATE 02:
Upon looking further into the issue, I've found that this is specific to DBeaver only. Viewing the same database file with other database viewer (e.g. DbSchema) does not have the issue.
Any idea what's going on?

Error having two columns in primarykey when using computed colum (EF Migrations)

I'm trying to add migrations for a model which has multiple versions of my model in time (ModificationDate).
Creating a PK based on Id alone creates the migrations without incident (also tried ModificationDate as PK also no problems). But when I use Id + ModificationDate to create a PK I'm getting the following error:
The property 'Id' cannot be configured as 'ValueGeneratedOnUpdate' or
'ValueGeneratedOnAddOrUpdate' because the key value cannot be changed
after the entity has been added to the store.
public class Model
{
public int Id { get; set; }
public System.DateTimeOffset ModificationDate { get; set; }
public string Json { get; set; }
}
protected override CreateModel(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Model>()
.ToTable("model")
.HasKey(e => new { e.Id, e.ModificationDate });
modelBuilder.Entity<Model>()
.Property(t => t.Id)
.HasComputedColumnSql("CAST(JSON_Value(Json, '$.Id') as int) PERSISTED");
}
I'm using EF Core Tools version 3.1.13.
Update
No fix but I fill the Id manually instead taking it from the json value.
public class Model
{
public int Id { get; set; }
public System.DateTimeOffset ModificationDate { get; set; }
public string Json { get; set; }
}
protected override CreateModel(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Model>()
.ToTable("model")
.HasKey(e => new { e.Id, e.ModificationDate });
}

How to delete an item in 'A' entity and delete all other items in 'B' entity which are linked by FK to 'A' entity

I'm using ASP.NET MVC to build an application for Forums. I have an entity named 'Posts' and an entity named 'PostReplies'.
On a particular Post, there will be a list of replies which are linked by a FK:'Post_Id' within my 'PostReplies' entity.
I'm wanting to know how I would go about deleting a Post which would then delete the replies linked to that post.
I've used Fluent API to try and solve this but I am getting this error message:
One or more validation errors were detected during model generation:
"BookClub.Data.IdentityUserLogin: : EntityType 'IdentityUserLogin' has no >key defined. Define the key for this EntityType.
BookClub.Data.IdentityUserRole: : EntityType 'IdentityUserRole' has no ?>key defined. Define the key for this EntityType.
IdentityUserLogins: EntityType: EntitySet 'IdentityUserLogins' is based >on type 'IdentityUserLogin' that has no keys defined.
IdentityUserRoles: EntityType: EntitySet 'IdentityUserRoles' is based on >type 'IdentityUserRole' that has no keys defined."
I'm using MVC's default ApplicationDbContext and therefore have ApplicationUser's tables in my database.
Does anyone know how to rectify this issue?
POST ENTITY MODEL
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime Created { get; set; }
public virtual Discussion Discussion { get; set; }
public virtual ICollection<PostReply> Replies { get; set; }
public virtual ApplicationUser ApplicationUser { get; set; }
}
POSTREPLY ENTITY MODEL
public class PostReply
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime Created { get; set; }
public virtual Post Post { get; set; }
public virtual ApplicationUser ApplicationUser { get; set; }
}
DELETEPOST METHOD/LOGIC
public void DeletePost(Post post)
{
var deletePost = _context.Post.FirstOrDefault(p => p.Id == id);
if (deletePost != null)
{
_context.Post.Remove(deletePost);
_context.SaveChanges();
}
}
POSTCONTROLLER
[HttpGet]
public ActionResult DeletePost(int id)
{
return View(_postService.GetPost(id));
}
[HttpPost]
public ActionResult DeletePost(Post post)
{
var posts = new Post();
_postService.DeletePost(id, post);
return RedirectToAction("GetPostsByDiscussion","Discussion",
new { id = post.Id })
}
I have used Fluent API and written the following code:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<PostReply>()
.HasRequired(p => p.Post)
.WithMany(p => p.Replies)
.HasForeignKey(r => r.PostId)
.WillCascadeOnDelete(false);
}
You need to add relation between entities in your DBContext
This may be One-to-Many, One-to-One, Many-to-Many
You can find detailed documentation here
Edit:
I'm using MVC's default ApplicationDbContext and therefore have ApplicationUser's tables in my database.
if you are inheriting the ApplicationDbContext you should call the base.OnModelCreating() method in your model creating method.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PostReply>()
.HasRequired(p => p.Post)
.WithMany(p => p.Replies)
.HasForeignKey(r => r.PostId)
.WillCascadeOnDelete(true);
}
And to enable cascade delete, you should send true parameter like .WillCascadeOnDelete(true)

Many to many relation between Identity and custom table. EF7 - Code first

How can I make many to many relation between AspNetRoles from Identity 3.0 and my custom table? I want simple 3 table, with both PermissionId and RoleId, something like AspNetUsersRole. I have something like this:
public class Permission
{
public int PermissionId { get; set; }
public string Name { get; set; }
public virtual ICollection<ApplicationRole> Roles { get; set; }
}
public class ApplicationRole : IdentityRole
{
public virtual ICollection<Permission> Permissions { get; set; }
}
But when I want to add migration, I got error:
Unable to determine the relationship represented by navigation property 'ApplicationRole.Permissions' of type 'ICollection<Permission>'. Either manually configure the relationship, or ignore this property from the model.
EF Core (EF7) does not currently support many to many relationship without a join entity. (Reference)
So, what you should do is to create an entity class for the join table and mapping two separate one-to-many relationships. Like;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostTag>()
.HasKey(t => new { t.PostId, t.TagId });
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}
public class PostTag
{
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
Regarding to this question answer, it can be done more easily like this-
class Photo
{
public int Id { get; set; }
public ICollection<PersonPhoto> PersonPhotos{ get; set; }
}
class PersonPhoto
{
public int PhotoId { get; set; }
public Photo Photo { get; set; }
public int PersonId { get; set; }
public Person Person { get; set; }
}
class Person
{
public int Id { get; set; }
public ICollection<PersonPhoto> PersonPhotos{ get; set; }
}
Be sure to configure PersonPhoto with a composite key:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PersonPhoto>().HasKey(x => new { x.PhotoId, x.PersonId });
}
To navigate, use a Select:
// person.Photos
var photos = person.PersonPhotos.Select(c => c.Photo);
Add This namespace-
using Microsoft.AspNetCore.Identity;
public class Permission
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int PermissionId { get; set; }
public string Name { get; set; }
public string UserIdFK { get; set; } //Foreign Key of Identity tbl
[ForeignKey("UserIdFK")]
public IdentityUser UserDetail { get; set; }
}
That's it, Happy coding :)

model builder add one column more?

I use EF code first.
It's my modelbuilder
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Market>()
.HasRequired(s => s.State)
.WithMany()
.HasForeignKey(s => s.StateId)
.WillCascadeOnDelete(false);
}
and State Class :
public class State
{
public State()
{
Markets = new HashSet<Market>();
}
[Key]
public int StateId { get; set; }
public string StateName { get; set; }
// navigation property
public virtual ICollection<Market> Markets { get; set; }
}
and Market class :
public class Market
{
[Key]
public int MarketId { get; set; }
public string MarketName { get; set; }
public int StateId { get; set; }
// navigation property
public virtual State State { get; set; }
}
Of course I remove extra code.
Problem is when I use this code , an State_StateId column add to my Market table in database, and when I do not use modelbuilder an error occurred with message loop code and ... (I say that I remove extra code), so how can I use code first without this "State_StateId" extra column.
excuse me for bad english writing.
If you want to remove State_StateId column set the configuration completely like the code below and don't let WithMany empty:
modelBuilder.Entity<Market>()
.HasRequired(s => s.State)
.WithMany(p => p.Markets)
.HasForeignKey(s => s.StateId)
.WillCascadeOnDelete(false);
Or you can just remove the Fluent API configuration and let EF use the default configuration convention and will set all tables, primary keys, foreign keys and column name for you.

Resources