Incomplete EF code-first cascade delete on many to many relationship - c#

I have a PhoneNumber entity which I'd like to reference across multiple entities. For example, a Contact entity that has many PhoneNumbers, and a Business entity with one PhoneNumber.
public class PhoneNumber
{
[Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Number { get; set; }
}
public class Contact
{
[Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
}
public class Business
{
[Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[ForeignKey("PhoneNumber")]
public int PhoneNumberId { get; set; }
public virtual PhoneNumber PhoneNumber { get; set; }
}
I've setup Contact and Business so that they have one way navigation properties. Also, the phone numbers are optional. I've also setup the many relationship for Contact to prevent EF from adding a Contact_Id column when I add a migration. The mapping is as follows (note WithMany() is used since PhoneNumber doesn't have a nav property back to Contact):
modelBuilder.Entity<Contact>().HasMany(r => r.PhoneNumbers).WithMany()
.Map(x => x.MapLeftKey("ContactId").MapRightKey("PhoneId"));
When I add a Contact with multiple phone numbers it gets added fine. There are records for the Contact, the PhoneNumbers table, and the ContactPhoneNumbers link table.
However, the issue I'm struggling with is when I delete a contact. EF correctly deletes the entry in the ContactPhoneNumbers link table, and the Contact entry, but it doesn't delete the entries from the PhoneNumbers table. I've seen examples of mapping with modelBuilder where WillCascadeOnDelete(true) is used, but that option isn't available when using WithMany().
What do I need to do to get that type of cascade delete working correctly? Is it possible with this setup? Or will I need to have a separate PhoneNumbers table for each entity (Contact and Business) to setup a relationship where the respective PhoneNumber table uses a FK (eg., Contact_Id)?
I'm fairly new to EF so any suggestions are welcome. I might be going about this entirely wrong.
EDIT: here's the related migration code...
CreateTable(
"dbo.PhoneNumbers",
c => new
{
Id = c.Int(nullable: false, identity: true),
Number = c.String()
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.Contacts",
c => new
{
Id = c.Int(nullable: false, identity: true)
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.ContactPhoneNumbers",
c => new
{
ContactId = c.Int(nullable: false),
PhoneId = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.ContactId, t.PhoneId })
.ForeignKey("dbo.Contacts", t => t.ContactId, cascadeDelete: true)
.ForeignKey("dbo.PhoneNumbers", t => t.PhoneId, cascadeDelete: true)
.Index(t => t.ContactId)
.Index(t => t.PhoneId);
CreateTable(
"dbo.Business",
c => new
{
Id = c.Int(nullable: false),
PhoneNumberId = c.Int(nullable: false)
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.PhoneNumbers", t => t.PhoneNumberId, cascadeDelete: true)
.Index(t => t.Id)
.Index(t => t.PhoneNumberId);

In order for the cascading delete to work records that are going to be cascaded must have a foreign key back to the record being deleted. So in your example, You delete a Contact record. Because their is a foreign key from ContactPhoneNumber to Contact, the cascade works. Since there is no foreign key from PhoneNumber to ContactPhoneNumber, (the foreign key goes the other way) the cascade does not continue.
This is because you defined the relationship as a many to many. If you think about trying to perform a cascading delete on your model as you would like, If a ContactPhoneNumber is deleted and then its associated PhoneNumbers are deleted, there could now be other ContactPhoneNumbers that that don't have a valid PhoneNumber (Because there can be many ContactPhoneNumbers to one PhoneNumber). Now these would need to be deleted and this process would continue. Databases don't like this cyclical cascading.
It is not entirely clear to me why you need the many to many relationship, If you truly need it you will not be able to perform a cascade on delete. If you can make your relationship:
1 Contact - * ContactPhoneNumber 1- * PhoneNumber, then you could configure the cascade to work like you want it too.

Related

Extra Foreign Key in Code First Migrations

I'm kinda new to Entity Framework and Code First Migration so I hope this is an easy question to answer. I am trying to create a one to one relationship between ApplicationUser (from ASP.NET identity) and Member. I have a Member class:
public class Member
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual Address Address { get; set; }
public UserStatus Status { get; set; }
public DateTime CreateDate { get; set; }
public virtual string ApplicationUserID { get; set; }
public virtual ApplicationUser ApplicationUser { get; set; }
}
and an ApplicationUserClass:
public class ApplicationUser : IdentityUser
{
public ApplicationUser()
{
}
public virtual Member Member { get; set; }
}
In my DBContext (inherits IdentityDbContext)I have the following configuration:
modelBuilder.Entity<ApplicationUser>()
.HasOptional(t => t.Member).WithOptionalPrincipal();
base.OnModelCreating(modelBuilder);
When I run the code first migration, I'm getting this:
CreateTable(
"dbo.Members",
c => new
{
ID = c.Int(nullable: false, identity: true),
FirstName = c.String(),
LastName = c.String(),
Status = c.Int(nullable: false),
CreateDate = c.DateTime(nullable: false),
ApplicationUserID = c.String(maxLength: 128),
Address_ID = c.Int(),
ApplicationUser_Id = c.String(maxLength: 128),
})
.PrimaryKey(t => t.ID)
.ForeignKey("dbo.Addresses", t => t.Address_ID)
.ForeignKey("dbo.AspNetUsers", t => t.ApplicationUser_Id)
.ForeignKey("dbo.AspNetUsers", t => t.ApplicationUserID)
.Index(t => t.ApplicationUserID)
.Index(t => t.Address_ID)
.Index(t => t.ApplicationUser_Id);
Notice that I have 2 foreign keys, ApplicationUserID and ApplicationUser_Id. I want to try to do everything using FluentAPI (i.e. not Data Annotations). How would I configure it so that EF uses ApplicationUserID, the string ID I have in my class? I thought that Class+ID was the convention, so why is it creating another foreign key?
I believe you should update your configuration this way:
modelBuilder.Entity<Member>()
.HasOptional(x => x.ApplicationUser)
.WithMany()
.HasForeignKey(x => x.ApplicationUserID);
This is the way EntityFramework treats one-to-one relationships, you have to map it that way and introduce UNIQUE constraints over your DB table.
More information about this case is here: http://weblogs.asp.net/manavi/associations-in-ef-code-first-ctp5-part-3-one-to-one-foreign-key-associations
Here's the quote from the link:
The reason is simple: Code First (and EF in general) does not natively
support one-to-one foreign key associations. In fact, EF does not
support any association scenario that involves unique constraints at
all. Fortunately, in this case we don’t care what’s on the target side
of the association, so we can treat it like a to-one association
without the many part. All we want is to express “This entity (User)
has a property that is a reference to an instance of another entity
(Address)” and use a foreign key field to represent that relationship.
Basically EF still thinks that the relationship is many-to-one. This
is a workaround for the current EF limitation which comes with two
consequences: First, EF won't create any additional constraint for us
to enforces this relationship as a one to one, we need to manually
create it ourselves. The second limitation that this lack of support
impose to us is more important: one to one foreign key associations
cannot be bidirectional (i.e. we cannot define a User property on the
Address class).

Unable to determine a valid ordering for dependent operations circular reference

I've been wasting two days now to try solve this problem but have yet to find a solution.
In my code that saves an entity with a relationship, I get this error when reaching ctx.SaveChanges():
Unable to determine a valid ordering for dependent operations.
Dependencies may exist due to foreign key constraints, model
requirements, or store-generated values.
Shipment.cs
[ForeignKey("ShipmentNumber")]
public int? DefaultShipmentNumber { get; set; }
public virtual ShipmentNumber ShipmentNumber { get; set; }
ShipmentNumber.cs
[Column("shipment_id")]
[ForeignKey("Shipment")]
public byte ShipmentId { get; set; }
public virtual Shipment Shipment { get; set; }
To avoid circular references, ShipmentNumber belonging to Shipment is nullable (optional), whereas ShipmentNumber's dependency on Shipment is required.
I first create a Shipment, add it and then attach a ShipmentNumber to it and add it to table as well.
Here's the fluent API code:
modelBuilder.Entity<Shipment>()
.HasOptional<ShipmentNumber>((shipment) => shipment.ShipmentNumber)
.WithMany();
Shipment has one "true" ShipmentNumber, but many ShipmentNumbers can link to the same Shipment, hence the WithMany() call (relation without a navigator property). In theory, both relations should always return one entity, but I know EF won't allow me a 1:1 relation here, so I'm using optional.
Here's the actual code:
shipment = tracker.Shipment;
ctx.Shipments.Add(shipment);
shipment.ShipmentNumber = new ShipmentNumber { Number = tracker.ShipmentNumber };
ctx.ShipmentNumbers.Add(shipment.ShipmentNumber);
ctx.SaveChanges();
If someone knows how to make it properly save the entity along with the relation, please do tell. I'm totally stuck at the moment.
Well, I don't know why you want a 1:n relationship in database and a 1:0.1 relationship in the model.
Case 1
If you want to make a 1:1 relationship, you should declare your model as follows:
public class Shipment
{
public int ShipmentId { get; set; }
//NO FK here
public virtual ShipmentNumber ShipmentNumber { get; set; }
}
public class ShipmentNumber
{
public int ShipmentId { get; set; } //ShipmentNumber PK is Also Shipment FK
public virtual Shipment Shipment { get; set; }
}
Mapping:
modelBuilder.Entity<Shipment>()
.HasKey(i => i.ShipmentId);
modelBuilder.Entity<ShipmentNumber>()
.HasKey(i => i.ShipmentId);
modelBuilder.Entity<Shipment>()
.HasRequired(i => i.ShipmentNumber)
.WithRequiredPrincipal(i => i.Shipment)
.WillCascadeOnDelete(false);
Generated Migration:
CreateTable(
"dbo.Shipments",
c => new
{
ShipmentId = c.Int(nullable: false, identity: true),
})
.PrimaryKey(t => t.ShipmentId);
CreateTable(
"dbo.ShipmentNumbers",
c => new
{
ShipmentId = c.Int(nullable: false),
})
.PrimaryKey(t => t.ShipmentId)
.ForeignKey("dbo.Shipments", t => t.ShipmentId)
.Index(t => t.ShipmentId);
Case 2
If you want to make a 1:n relationship:
public class Shipment
{
public int ShipmentId { get; set; }
public virtual ICollection<ShipmentNumber> ShipmentNumbers { get; set; }
}
public class ShipmentNumber
{
public int ShipmentNumberId { get; set; }
public int ShipmentId { get; set; }
public virtual Shipment Shipment { get; set; }
}
Mapping:
modelBuilder.Entity<Shipment>()
.HasKey(i => i.ShipmentId);
modelBuilder.Entity<ShipmentNumber>()
.HasKey(i => i.ShipmentNumberId);
modelBuilder.Entity<Shipment>()
.HasMany(i => i.ShipmentNumbers)
.WithRequired(i => i.Shipment)
.HasForeignKey(i => i.ShipmentId)
.WillCascadeOnDelete(false);
Generated Migration:
CreateTable(
"dbo.Shipments",
c => new
{
ShipmentId = c.Int(nullable: false, identity: true),
})
.PrimaryKey(t => t.ShipmentId);
CreateTable(
"dbo.ShipmentNumbers",
c => new
{
ShipmentNumberId = c.Int(nullable: false, identity: true),
ShipmentId = c.Int(nullable: false),
})
.PrimaryKey(t => t.ShipmentNumberId)
.ForeignKey("dbo.Shipments", t => t.ShipmentId)
.Index(t => t.ShipmentId);
Another problem is the code you are using to add items to database.
ctx.Shipments.Add(shipment);
shipment.ShipmentNumber = new ShipmentNumber { Number = tracker.ShipmentNumber };
//this line is not necessary
ctx.ShipmentNumbers.Add(shipment.ShipmentNumber);
ctx.SaveChanges();
When you add a new Shipment all dependant objects will be inserted to database, if necessary.

EF Code first set a foreign key with an empty lookup table

I have a problem with EF code first migration related to a lookup table and foreign keys. Let's say I have this two classes in my code:
public class Test
{
[Key]
public long Id { get; set; }
[Required]
public string Title { get; set; }
[Required, DisplayName("Test type")]
public TestType TestType { get; set; }
}
public class TestType
{
public int Id { get; set; }
public string Name { get; set; }
}
TestType is a typical lookup table and I usually fill them up in the Seed() method:
context.TestTypes.AddOrUpdate(
it => it.Name,
new TestType() { Name = "Drug" },
new TestType() { Name = "Educational" },
new TestType() { Name = "Other" }
);
When I create the table with the relationship I get the following migration:
CreateTable(
"dbo.TestTypes",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
})
.PrimaryKey(t => t.Id);
AddColumn("dbo.Tests", "TestType_Id", c => c.Int(nullable: false));
CreateIndex("dbo.Tests", "TestType_Id");
AddForeignKey("dbo.Tests", "TestType_Id", "dbo.TestTypes", "Id", cascadeDelete: true);
Now, if I perform the migration of course I will get an error since the foreign key cannot be respected given the fact that the lookup table is still empty and the column created does not have a default value.
In DEVELOPMENT I am able to solve this by simply creating two migrations, the first one to create the lookup table and the second one to set the foreign key. If I run them separately then the Seed method after the first one will fill the table and I can tweak the column creation to pick up the values from the DB to prefill the column before creating the foreign key, a bit like this:
AddColumn("dbo.Tests", "TestType_Id", c => c.Int(nullable: false));
Sql("UPDATE dbo.Tests SET TestType_Id = (SELECT TOP 1 Id FROM dbo.TestTypes)");
CreateIndex("dbo.Tests", "TestType_Id");
AddForeignKey("dbo.Tests", "TestType_Id", "dbo.TestTypes", "Id", cascadeDelete: true);
Then when I run it everything works.
Now, in PRODUCTION I don't have the same luxury, since ALL the migrations are run before the Seed method is run, I will always have the same problem.
I know I could potentially run the migrations in stepped order on the production DB as well but that does not really solve the problem... Let's say a colleague of mine updates his working copy and runs the migrations, all will be run in order and he will encounter the error for sure.
I'm not sure on the current state of your database but I would define your models like this
public class Test
{
[Key]
public long Id { get; set; }
[Required]
public string Title { get; set; }
[Required]
[ForeignKey("TestType")]
public int TestTypeId { get; set; }
[DisplayName("Test type")]
public virtual TestType TestType { get; set; }
}
public class TestType
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
Which results in the following migration when the tables don't exist already. I always find describing the foreign keys explicitly works better.
public override void Up()
{
CreateTable(
"dbo.Tests",
c => new
{
Id = c.Long(nullable: false, identity: true),
Title = c.String(nullable: false),
TestTypeId = c.Int(nullable: false),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.TestTypes", t => t.TestTypeId)
.Index(t => t.TestTypeId);
CreateTable(
"dbo.TestTypes",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
})
.PrimaryKey(t => t.Id);
}
The seed should then work fine as long as the Test table is empty?

Renaming N to N table in Code First EF

I have two tables that are connect N to N:
[Table("Backoffice_Roles")]
public class Role
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid RoleId { get; set; }
public ICollection<User> Users { get; set; }
}
[Table("Backoffice_Users")]
public class User
{
// Primary key
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid UserId { get; set; }
public ICollection<Role> Roles { get; set; }
}
This all works fine and it creates 3 tables: Backoffice_Roles, Backoffice_Users and RoleUsers.
Is there a way to rename RoleUsers to Backoffice_RoleUsers ?
I tried renaming the table manually in the migration file but it gives this error:
System.Data.Entity.Infrastructure.DbUpdateException: An error occurred
while saving entities that do not expose foreign key properties for
their relationships. The EntityEntries property will return null
because a single entity cannot be identified as the source of the
exception. Handling of exceptions while saving can be made easier by
exposing foreign key properties in your entity types. See the
InnerException for details. --->
System.Data.Entity.Core.UpdateException: An error occurred while
updating the entries. See the inner exception for details. --->
System.Data.SqlClient.SqlException: Invalid object name
'dbo.RoleUsers'.
This the migration without changing the name of the last table manually:
public override void Up()
{
CreateTable(
"dbo.Backoffice_Users",
c => new
{
UserId = c.Guid(nullable: false, identity: true),
})
.PrimaryKey(t => t.UserId);
CreateTable(
"dbo.Backoffice_Roles",
c => new
{
RoleId = c.Guid(nullable: false, identity: true),
})
.PrimaryKey(t => t.RoleId);
CreateTable(
"dbo.RoleUsers",
c => new
{
Role_RoleId = c.Guid(nullable: false),
User_UserId = c.Guid(nullable: false),
})
.PrimaryKey(t => new { t.Role_RoleId, t.User_UserId })
.ForeignKey("dbo.Backoffice_Roles", t => t.Role_RoleId)
.ForeignKey("dbo.Backoffice_Users", t => t.User_UserId)
.Index(t => t.Role_RoleId)
.Index(t => t.User_UserId);
}
Use following mapping to provide name for junction table:
modelBuilder.Entity<Role>()
.HasMany(r => r.Users)
.WithMany(u => u.Roles)
.Map(m => m.ToTable("Backoffice_RoleUsers"));
You can provide mappings by overriding OnModelCreating method of your DbContext class.

Changing a POCO class name without causing a huge migration

Here's an example of an entity we have inherited:
[Table("Vehicles")]
public partial class Car
{
[Key]
public int Id { get; set; }
[Required]
public Make Make { get; set; }
[Required]
public Model Model { get; set; }
}
We want to refactor our code to rename this table "Vehicle". We can change the [Table] attribute to generate a migration file that does a RenameTable(name: "dbo.Cars", newName: "Vehicles"), all well and good.
However, if we try to change the class name, the migration scaffolder tries to create and THEN delete the same table. Here's a example:
public override void Up()
{
DropForeignKey("dbo.Vehicles", "Make_Id", "dbo.Makes");
DropForeignKey("dbo.Vehicles", "Model_Id", "dbo.Models");
DropIndex("dbo.Vehicles", new[] { "Make_Id" });
DropIndex("dbo.Vehicles", new[] { "Model_Id" });
CreateTable(
"dbo.Vehicles",
c => new
{
Id = c.Int(nullable: false, identity: true),
Make_Id = c.Int(nullable: false),
Model_Id = c.Int(nullable: false),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.Makes", t => t.Make_Id)
.ForeignKey("dbo.Models", t => t.Model_Id)
.Index(t => t.Make_Id)
.Index(t => t.Model_Id);
DropTable("dbo.Vehicles");
}
Is there any way of making a change to the class name in code only (keeping the [Table] attribute the same), without causing this kind of behaviour? Or is this simply something I shouldn't be doing?
You don't have to use the migration as it is being generated. It tries to understand what you changed and create the migration that fits your work, but apparently the algorithm is confused in this case.
You can just alter the migration to execute the RenameTable method. This should work.
Neverless, please submit a bug report to the EF team at http://entityframework.codeplex.com

Categories

Resources