How to set delete behavious for many to many in efcore 6 - entity-framework-core

I have a webapi for climbing routes where there is a many to many relationship between Routes and Holds. 1 Hold can be in many Routes, and 1 Route can have many holds.
When I delete a route, the hold should not be deleted but the many to many relationship entry should be. When I delete a hold, same deal, the route should not be delete, but the relationship should be.
If I set up my classes and run a migration I get a migration file that contains the many to many table:
migrationBuilder.CreateTable(
name: "CRouteHold",
columns: table => new
{
HoldsId = table.Column<int>(type: "int", nullable: false),
RoutesId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CRouteHold", x => new { x.HoldsId, x.RoutesId });
table.ForeignKey(
name: "FK_CRouteHold_Holds_HoldsId",
column: x => x.HoldsId,
principalTable: "Holds",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CRouteHold_Routes_RoutesId",
column: x => x.RoutesId,
principalTable: "Routes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
This obviously doesn't work. I have to change the onDelete property of the second block - name: "FK_CRouteHold_Routes_RoutesId", - to NoAction. Then it seems to work. However, while I have to overwrite this in the migration file manually before I run update database... for some reason I can't get the migration to do this automatically.
I've tried variations of the following in my DBContext onmodelcreating method:
modelBuilder
.Entity<CRoute>()
.HasMany(e => e.Holds)
.WithMany(e => e.Routes)
.UsingEntity<Dictionary<string, object>>(
"CRouteHold",
b => b.HasOne<Hold>().WithMany().OnDelete(DeleteBehavior.Cascade),
b => b.HasOne<CRoute>().WithMany().OnDelete(DeleteBehavior.NoAction));
But it doesn't actually add the noAction to the migration file. It just gets rid of the onDelete property entirely, which then fails to have the expected outcome. I keep having to add it in manually. If I try anyt other delete behaviour, i.e. Restrict, it gets added to the migration file. It's just NoAction that doesn't seem to work.
If I get rid of both OnDelete settings from this onmodelcreating script, then the unit tests pass, but the update-database command fails due to cascade cycles.
Am I writing this correctly?
Better yet, is it possible to make the columns on the join table nullable?

Related

EF Core migration taking old migrations changes along with new columns

I have added 2 more columns in aspnetusers table and added migrations. But it's failing and showing error stating FK_Projects_AspNetUsers_UserId' is not a constraint.
Could not drop constraint. See previous errors.
public string NavbarBackGroundImagePath { get; set; }
public string NavbarBackGroundColorCode { get; set; }
Added these 2 columns.
migrationBuilder.DropForeignKey(
name: "FK_Projects_AspNetUsers_UserId",
table: "Projects");
migrationBuilder.DropIndex(
name: "IX_Projects_UserId",
table: "Projects");
migrationBuilder.DropColumn(
name: "UserId",
table: "Projects");
migrationBuilder.AddColumn<int>(
name: "WorkspaceId",
table: "Projects",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "NavbarBackGroundColorCode",
table: "AspNetUsers",
maxLength: 10,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "NavbarBackGroundImagePath",
table: "AspNetUsers",
maxLength: 200,
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Projects_WorkspaceId",
table: "Projects",
column: "WorkspaceId");
migrationBuilder.AddForeignKey(
name: "FK_Projects_Workspaces_WorkspaceId",
table: "Projects",
column: "WorkspaceId",
principalTable: "Workspaces",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
But in migration file it's taking all these things.
According to the error message, it seems that this error happens when you want to drop the foreign key constraint in the Projects table, if the Projects table doesn't contain the "FK_Projects_AspNetUsers_UserId" constraint or doesn't contain the "UserId" property, it will show this error.
So, please check the Projects table in the database, whether you have removed the UerId column. And whether the Projects table have the foreign key constraint with the AspNetUsers table. If the Projects table doesn't contain this constraint, try to remove the DropForeignKey related code:
migrationBuilder.DropForeignKey(
name: "FK_Projects_AspNetUsers_UserId",
table: "Projects");

Entity Framework change Key Type

I have created a model with various values but stupidly used a GUID for my key, I am currently attempting to change that to an Int but am getting an error when I do so.
I have run the enable migration command:
Enable-Migrations -Force -ContextTypeName project.Models.MyContext
This creates the migration I would expect but when I run:
Update-Database -Force
The error I'm getting is:
Operand type clash: uniqueidentifier is incompatible with int
I don't care about the data currently contained within the database since it is just using a SQL Server Express database just now, but I would prefer to find a way to migrate this instead of just having to drop the DB altogether, what's the best way to do this?
I have already got
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<MyContext>());
in Global.asax.
I would expect that the generated migration is using AlterColumn to try and change the type of the field from guid to int. This is not possible, so you'll need to modify the generated migration yourself:
Assuming your table is dbo.People and the key is called Id, you probably have this at the moment:
DropPrimaryKey("dbo.People");
AlterColumn("dbo.People", "Id", c => c.Int(nullable: false, identity: true));
AddPrimaryKey("dbo.People", "Id");
Change it to:
DropPrimaryKey("dbo.People");
DropColumn("dbo.People", "Id");
AddColumn("dbo.People", "Id", c => c.Int(nullable: false, identity: true));
AddPrimaryKey("dbo.People", "Id");
Note that if you've got this key referenced elsewhere, this technique will not work if you've got any data present, as the keys are all being regenerated.
Update for EF Core generated migrations:
dotnet : System.Data.SqlClient.SqlException: Operand type clash: int is incompatible with uniqueidentifier
change
migrationBuilder.AlterColumn<Guid>(
name: "VATID",
schema: "catalogue",
table: "Products",
nullable: false,
oldClrType: typeof(int));
into
migrationBuilder.DropColumn(
name: "VATID",
schema: "catalogue",
table: "Products");
migrationBuilder.AddColumn<Guid>(
name: "VATID",
schema: "catalogue",
table: "Products",
nullable: false);
Of course, this will destroy your data for the certain column. But they obviously cannot be converted into GUID.
I am trying to add something to the selected answer since I don't have enough reputation to add comment there :
Please also add code to drop and recreate indexes if any else it will fail to drop column. e.g.
DropPrimaryKey("dbo.People");
DropIndex("IX_...") // If exists before
DropColumn("dbo.People", "Id");
AddColumn("dbo.People", "Id", c => c.Int(nullable: false,
identity: true));
AddPrimaryKey("dbo.People", "Id");
CreateIndex("IX_...", unique:(true/false)) // If existed before
The AlterColumn will Drop and Add keys (primary,foreign etc) but will not touch indexes. I have faced this.

How to change a clustered index in Entity Framework 6.1 Code First model and apply it to an Azure database

Using the Entity Framework 6.1 code first model, what is the best way to go about changing the clustered index on a table from the default ID to another set of columns. Azure doesn't allow a table without a clustered index.
public partial class UserProfile
{
public override Guid ID { get; set; }
[Index( "CI_UserProfiles_UserID", IsClustered = true)]
public Guid UserID { get; set; }
[Required]
public Guid FieldID { get; set; }
[Required]
[StringLength(400)]
public string Value { get; set; }
}
On the table UserProfiles, ID is already the primary key and clustered index. Adding
[Index( "CI_UserProfiles_UserID", IsClustered = true)]
to UserID creates this migration:
CreateIndex("dbo.UserProfiles", "UserID", clustered: true, name: "IX_UserProfiles_UserID");
Executing the migration generates the following error:
Cannot create more than one clustered index on table 'dbo.UserProfiles'. Drop the existing clustered index
'PK_dbo.UserProfiles' before creating another.
To solve your problem, after you generate your migration file, you must modify the generated code by disabling clustered index for your primary key by assigning false as a value of clustered parameter of PrimaryKey.
After your modifications you must have something like this into your migration file:
CreateTable(
"dbo.UserProfiles",
c => new
{
Id = c.Guid(nullable: false),
UserID = c.Guid(nullable: false),
FieldID = c.Guid(nullable: false),
Value = c.String(nullable: false, maxLength: 400),
})
.PrimaryKey(t => t.Id, clustered: false)
.Index(t => t.UserID, clustered: true, name: "CI_UserProfiles_UserID");
This is not done in OnModelCreating method by using Fluent API like Manish Kumar said, but in migration file. The file that is created when you use Add-Migration command.
Existing Database
As you say in comments, your database already exist. After executing Add-Migration command, you will have this line on your DbMigration file in your Up() method:
public override void Up()
{
CreateIndex("dbo.UserProfiles", "UserID", clustered: true, name: "CI_UserProfiles_UserID");
}
You must modify the Up() method to have this code:
public override void Up()
{
this.Sql("ALTER TABLE dbo.UserProfiles DROP CONSTRAINT \"PK_dbo.UserProfiles\"");
this.Sql("ALTER TABLE dbo.UserProfiles ADD CONSTRAINT \"PK_dbo.UserProfiles\" PRIMARY KEY NONCLUSTERED (Id);");
this.CreateIndex("dbo.UserProfiles", "UserID", clustered: true, name: "CI_UserProfiles_UserID");
}
In the code above I assumed that the created clustered index is named PK_dbo.UserProfiles in your database. If not then put at this place the correct name.
This is truly an area where EntityFramwork (Core) had to advance and it still is hard.
So, I could not use IsClustered(false) for my GUID / string Primary keys, for the simple reason, the project having DbContexts was DB - agnostic. So you needed to Add EntityFrameworkCore.SqlServer and IsClustered is available then, and only.
So, my solution was simple. Add no nuget package but this attribute.
This ONLY works on EF Core.
I have tested this on SQL. Though, not sure if the other providers would allow this string not having any meaning. (e.g. SQLite does not know clustered indexes)
p.HasKey(k => k.Id).HasAnnotation("SqlServer:Clustered", false);
You need to remove the existing clustered index from your current PK 'ID' which is created by default for any "KEY" property in code first. It can be done using fluent API:
.Primarykey(x=>x.ID,clustered:false)
Once existing clustered index is removed from ID, your migration to add the clustered index on UserID should run smoothly.
After the migration file is created, modify the generated code, disabling the clustered index for the primary key by setting the clustered property to false.
Being that Azure does not allow a table without a clustered index, and there is no utility in SQL Server to 'change' a clustered index on a table, it is necessary create a new table with the clustered index and migrate the existing data to it. The code below renames the original table, migrates the data to the new table that was created with the new clustered index and drops the original table.
RenameTable("dbo.UserProfiles", "UserProfiles_PreMigrate");
CreateTable(
"dbo.UserProfiles",
c => new
{
Id = c.Guid(nullable: false),
UserID = c.Guid(nullable: false),
FieldID = c.Guid(nullable: false),
Value = c.String(nullable: false, maxLength: 400),
})
.PrimaryKey(t => t.Id, clustered: false)
.Index(t => t.UserID, clustered: true, name: "CI_UserProfiles_UserID");
Sql(#"
INSERT [dbo].[UserProfiles]
(ID,
UserID,
FieldID,
Value)
SELECT
ID,
UserID,
FieldID,
Value
FROM dbo.UserProfiles_PreMigrate
");
DropTable("UserProfiles_PreMigrate");
Any existing table constraints will be lost in this operation, so it will be necessary to recreate and indexes,foreign keys, etc on the table.

How to declare cascade delete in EF when child table isn't mapped as separate entity

I have got a database first approach with EF5 and here is a fragment of mappings:
internal class xxxMapping : EntityTypeConfiguration<Order>
{
public xxxMapping ()
{
ToTable("my_table");
//......
HasMany(it => it.Documents)
.WithMany()
.Map(
m =>
{
m.ToTable("dependent_table");
m.MapLeftKey("left_key_id");
m.MapRightKey("right_key_id");
});
}
What is the best way to declare, using fluent API, that when some row is deleted from my_table, then dependent rows from dependent_table will be deleted too (Cascade delete option in FK)
UPD It seems to be working without any additional code (Of course - if foreign key in table is configured properly). But i'm not sure it's a good practice to do so

Entity Framework 5 and Code First - Can I get it to do a Rename instead of Drop and Create?

I'm using EF 5 Code First. I have the following Fluent API code that sets a navigation property for 'SaleZipCode'.
private void MyTable(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<MyType>()
.HasRequired(a => a.SaleZipCode)
.WithMany()
.Map(map => map.MapKey("SaleZipCodeId"));
}
I realize I misnamed 'SaleZipCode' and it should be 'ZipCode'. However, when I do this, EF attempts to drop the SaleZipCode column and add a ZipCode column when I do an Update-Database, instead of just doing a rename. This doesn't work because I have existing data in the table. Is there a way I can get EF to do a rename and not a drop and recreate?
Renaming property in your entity should not affect database, because there is no SaleZipCode column in database. Foreign key is mapped to SaleZipCodeId column, and only changing this foreign key column mapping will affect database. E.g. if you will change mapping to
modelBuilder.Entity<MyType>()
.HasRequired(a => a.Foo) // changing this will not affect database
.WithMany()
.Map(map => map.MapKey("ZipCodeId")); // column name changed
Then following migration will be generated
public override void Up()
{
RenameColumn(table: "dbo.MyTypes",
name: "SaleZipCodeId", newName: "ZipCodeId");
}
public override void Down()
{
RenameColumn(table: "dbo.MyTypes",
name: "ZipCodeId", newName: "SaleZipCodeId");
}
Simple renaming columns, data will be preserved.