EntityFramework: How to configure Cascade-Delete to nullify Foreign Keys - entity-framework

EntityFramework's documentation states that the following behavior is possible:
If a foreign key on the dependent entity is nullable, Code First does
not set cascade delete on the relationship, and when the principal is
deleted the foreign key will be set to null.
(from http://msdn.microsoft.com/en-us/jj591620)
However, I cannot achieve such a behavior.
I have the following Entities defined with code-first:
public class TestMaster
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<TestChild> Children { get; set; }
}
public class TestChild
{
public int Id { get; set; }
public string Name { get; set; }
public virtual TestMaster Master { get; set; }
public int? MasterId { get; set; }
}
Here is the Fluent API mapping configuration:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<TestMaster>()
.HasMany(e => e.Children)
.WithOptional(p => p.Master).WillCascadeOnDelete(false);
modelBuilder.Entity<TestChild>()
.HasOptional(e => e.Master)
.WithMany(e => e.Children)
.HasForeignKey(e => e.MasterId).WillCascadeOnDelete(false);
}
Foreign Key is nullable, navigation property is mapped as Optional, so I expect the cascade delete to work as described as MSDN - i.e. to nullify MasterID's of all children and then delete the Master object.
But when I actually try to delete, I get the FK violation error:
using (var dbContext = new TestContext())
{
var master = dbContext.Set<TestMaster>().Find(1);
dbContext.Set<TestMaster>().Remove(master);
dbContext.SaveChanges();
}
On SaveChanges() it throws the following:
System.Data.Entity.Infrastructure.DbUpdateException : An error occurred while updating the entries. See the inner exception for details.
----> System.Data.UpdateException : An error occurred while updating the entries. See the inner exception for details.
----> System.Data.SqlClient.SqlException : The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.TestChilds_dbo.TestMasters_MasterId". The conflict occurred in database "SCM_Test", table "dbo.TestChilds", column 'MasterId'.
The statement has been terminated.
Am I doing something wrong or did I misunderstood what the MSDN says?

It works indeed as described but the article on MSDN misses to emphasize that it only works if the children are loaded into the context as well, not only the parent entity. So, instead of using Find (which only loads the parent) you must use eager loading with Include (or any other way to load the children into the context):
using (var dbContext = new TestContext())
{
var master = dbContext.Set<TestMaster>().Include(m => m.Children)
.SingleOrDefault(m => m.Id == 1);
dbContext.Set<TestMaster>().Remove(master);
dbContext.SaveChanges();
}
This will delete the master from the database, set all foreign keys in the Child entities to null and write UPDATE statements for the children to the database.

After following #Slauma's great answer I was still getting same error as OP.
So don't be as naive as me and think that the examples below will end up with same result.
dbCtx.Entry(principal).State = EntityState.Deleted;
dbCtx.Dependant.Where(d => d.PrincipalId == principalId).Load();
// code above will give error and code below will work on dbCtx.SaveChanges()
dbCtx.Dependant.Where(d => d.PrincipalId == principalId).Load();
dbCtx.Entry(principal).State = EntityState.Deleted;
First load the children into context before setting entity state to deleted (if you are doing it that way).

Related

EF Core - duplicate entry in table index when adding record to table with composite keys

I have a class with a composite key. When I try to save a record where a part of that key is contained in another record's key, I am getting an exception although the composite keys as a whole are unique.
Data type with navigation:
public class ProxyInfo
{
[Key, Column(Order = 0, TypeName = "varchar")]
[StringLength(80)]
public string AccountID { get; set; } = string.Empty;
[Key, Column(Order = 1, TypeName = "varchar")]
[StringLength(80)]
public string ProxyID { get; set; } = string.Empty;
[ForeignKey(nameof(ProxyID))]
public virtual UserInfo? Proxy { get; set; }
// a few more properties
}
OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProxyInfo>()
.HasKey(e => new { e.AccountID, e.ProxyID })
.HasName("PK_ProxyInfo");
modelBuilder.Entity<ProxyInfo>()
.HasOne(p => p.Proxy)
.WithOne()
.OnDelete(DeleteBehavior.NoAction);
// more code follows
}
Here's the relevant controller code:
[Route(Routes.Base + "[controller]")]
[ApiController]
public class ProxyInfoApiController : ControllerBase
{
private readonly DS2DbContext _context;
public ProxyInfoApiController(DS2DbContext context)
{
_context = context;
}
[HttpPost("Add")]
[AllowAnonymous]
public async Task<IActionResult> Add([FromBody] ProxyInfo proxyInfo)
{
try
{
_context.ProxyInfo.Add(proxyInfo);
await _context.SaveChangesAsync();
}
catch (Exception e)
{
ServerGlobals.Logger?.Error($"ProxyInfoController.Add: Error '{e.Message}' occurred.");
}
return Ok(); // Created(proxyInfo.ID.ToString(), proxyInfo);
}
}
The error message reads:
A row with a duplicate key cannot be inserted in the dbo.ProxyInfo
object with the unique IX_ProxyInfo_ProxyID-Index. The duplicate
key value is "X".
The complex key I tried to insert was {"B","X"}. The only other record in the ProxyInfo table has key {"A", "X"}; So there should be two different ProxyInfo records referencing the same UserInfo record.
The problem seems to be that an index is being tried to be updated with a value it already contains. However, the indices of both records can be identical, as multiple ProxyInfos can reference the same UserInfo. So actually, no duplicate entry is created. It's just that a 2nd ProxyInfo record uses the same user as the 1st one.
I just found out that the relevant index is created during initial migration with a unique:true attribute. The question is whether I can make EF Core skip updating the index when it already contains an index that it is trying to add again.
I found the problem. It was this statement:
modelBuilder.Entity<ProxyInfo>()
.HasOne(p => p.Proxy)
.WithOne()
.OnDelete(DeleteBehavior.NoAction);
which should have been
modelBuilder.Entity<ProxyInfo>()
.HasOne(p => p.Proxy)
.WithMany()
.OnDelete(DeleteBehavior.NoAction);
What I also didn't know is that when I make changes like this one, I need to create and execute a migration. Once I did that, the index got changed into a non unique one.
I am only doing EF Core/Blazor for a year and I am dealing more with application development than with the framework around it, so this is all new to me and it took me a while to figure it out.

"The association has been severed but the relationship is either marked as 'Required' or is implicitly required..."

I am getting the following error when trying to add a migration:
PS C:\Code\morpher.ru\Morpher.Database> dotnet ef migrations add AddQazaqFeatures --startup-project=../Morpher.Database.Design
Build started...
Build succeeded.
System.InvalidOperationException: The association between entity types 'Service' and 'Deployment' has been severed but the relationship is either m
arked as 'Required' or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a requi
red relationship is severed, then setup the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLoggin
g' to see the key values.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.HandleConceptualNulls(Boolean sensitiveLoggingEnabled, Boolean forc
e, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.CascadeDelete(InternalEntityEntry entry, Boolean force, IEnumerable`1 fore
ignKeys)
...
My code:
public class Deployment
{
public int Id { get; set; }
public virtual Service Service { get; set; }
public int ServiceId { get; set; }
public string Host { get; set; }
public short? Port { get; set; }
public string BasePath { get; set; }
}
public class Service
{
public int Id { get; set; }
public string Name { get; set; }
public string UrlSlug { get; set; }
public virtual ICollection<Endpoint> Endpoints { get; set; }
public virtual ICollection<Deployment> Deployments { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Service>().HasData(new Service
{
Name = "Веб-сервис «Морфер»",
UrlSlug = "ws",
Id = 1
});
modelBuilder.Entity<Deployment>().HasData(new Deployment
{
Host = "ws3.morpher.ru",
ServiceId = 1,
Id = 1
});
modelBuilder.Entity<Deployment>(entity =>
{
entity.Property(e => e.Host).IsRequired().HasMaxLength(256);
entity.Property(e => e.BasePath).HasMaxLength(512);
entity.HasOne(deployment => deployment.Service)
.WithMany(service => service.Deployments)
.HasForeignKey(d => d.ServiceId)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_Deployments_Services");
});
}
There are numerous StackOverflow questions mentioning the same error (1, 2, 3), but they are mostly to do with removing entities while not having a CASCADE delete policy or a nullable foreign key.
In my case, I am trying to add new rows and I don't see why it is considering the relationship 'severed'. Is setting ServiceId = 1 not enough?
I was able to reproduce the issue in latest at this time EF Core 3.1 version (3.1.28) by first removing the model data seeding code (HasData calls), then adding migration for just creating the tables/relationships, then adding the data seeding code and attempting to add new migration.
It does not happen in latest EF Core 6.0, so apparently you are hitting EF Core 3.1 defect/bug which has been fixed somewhere down on the road. So you either need to upgrade to a newer EF Core version (with all associated burdens like retesting everything, breaking changes etc.), or use the workaround below.
The workaround is to replace the DeleteBehavior.Restrict with either ClientNoAction or NoAction. Values of that enum and documentation of what they do is kind of messy, but all these 3 values seem to generate one and the same regular enforced FK constraint (with no cascade) in the database, and differ only by client side behavior, or in other words, what does EF Core change tracker do with related tracked entities when "deleting" a principal entity. And in this particular case, `Restrict" throws exception when there are tracked (loaded) related entity instances, while the other two won't.
I know you think you are just "adding data", but EF Core model data seeding is more than that - it tries to keep that data, so in some circumstances it needs to update or delete previously added data. Which in general works, except when there are bugs in the EF Core codebase, like in this case.

Deleting association between one or zero to one entities with EntityFramework

I have entities set up something like this:
public class MyThing
{
public int Id { get; set; }
public virtual MyOtherThing { get;set; }
}
public class MyOtherThing
{
public int Id { get; set; }
public virtual MyThing MyThing { get; set; }
}
My intention is that 'MyThing' can have one or none of MyOtherThing, and I also want a navigation link from MyOtherThing to it's parent.
I have configured the following EntityBaseConfiguration for the 'MyOtherThing' entity:
this.HasOptional(x => x.MyThing)
.WithOptionalPrincipal(x => x.MyOtherThing);
I can assign and modify MyOtherThing to MyThing no problem, but when I want to unassign 'MyOtherThing' from 'MyThing', how do I do this?
I tried the following:
myThing.MyOtherThing = null;
and then editing the entity by setting the EntityState.Modified state, but this didn't remove the association between the entities.
I tried adding the following to my MyThing entity, but this resulted in an EF 'Multiplicity is not valid' error when updating my database model:
public int? MyOtherThingId{ get; set; }
Thanks in advance!
I tried the following:
myThing.MyOtherThing = null;
If you want to remove an optional dependent entity (here: MyOtherThing) from a principal entity (here MyThing) by setting it to null, you have to pull the entity from the database with the dependent entity included, for example:
var mything = context.MyThings.Include(m => m.MyOtherThing)
.Single(t => t.Id == idValue);
(It's also OK when the belonging MyOtherThing is loaded into the context later, for example by lazy loading).
Without Include, myThing.MyOtherThing already is null and EF doesn't detect any change. Note that the statement myThing.MyOtherThing = null; doesn't execute lazy loading, which is a bit confusing because with collections the behavior is different.
By the way, the dependent entity can also be removed from the database directly, which is more efficient.
var ot = context.Set<MyOtherThing>().Find(idValue);
context.Set<MyOtherThing>().Remove(ot);
context.SaveChanges();

Entity Framework 6.X and one-to-one relationship

I have the following model:
public partial class Driver
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Nickname { get; set; }
public virtual AspNetUser AspNetUser { get; set; }
......
}
public partial class AspNetUser
{
public string Id { get; set; }
public string UserName { get; set; }
public virtual Driver Driver { get; set; }
......
}
and the following mapping:
this.HasOptional(c => c.Driver)
.WithOptionalPrincipal(a => a.AspNetUser)
.Map(m => m.MapKey("AspNetUserId"));
It creates correct DB model, adds nullable AspNetUserId FK to Driver table.
But how to link one object with another in code. I don't have AspNetUserId property, so, I try to set object, like this:
_db.Drivers.Attach(driver);
_db.AspNetUsers.Attach(aspNetUser);
driver.AspNetUser = aspNetUser;
_db.SaveChanges();
but then I got an exception :
"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."
"Store update, insert, or delete statement affected an unexpected
number of rows (0). Entities may have been modified or deleted since
entities were loaded. See
http://go.microsoft.com/fwlink/?LinkId=472540 for information on
understanding and handling optimistic concurrency exceptions."
How to solve it with EF 6.X ?
This is happening when the Driver is already associated with AspNetUser. When you attach the driver with AspNetUser property being null, EF assumes the original value of AspNetUserId being null and generates update statement with AspNetUserId IS NULL additional criteria, which of course does not match the existing record, the command returns 0 records affected and EF generates the exception in question.
The solution is (1) to load the original Driver.AspNetUser property value from the database before setting the new value. Also, in order to correctly handle the case when the new AspNetUser is already associated with a different Driver, you should (2) load AspNetUser.Driver property as well:
_db.Drivers.Attach(driver);
_db.AspNetUsers.Attach(aspNetUser);
_db.Entry(driver).Reference(e => e.AspNetUser).Load(); // (1)
_db.Entry(aspNetUser).Reference(e => e.Driver).Load(); // (2)
driver.AspNetUser = aspNetUser;
_db.SaveChanges();

Why do I get a "relationship multiplicity violation" accessing a relation property?

I have the following classes in which I am trying to map the entity object to the view model:
public class ConsumerIndexItem: MappedViewModel<Consumer>
{
public string UserName { get; set; }
public string RoleDescription { get; set; }
public override void MapFromEntity(Consumer entity)
{
base.MapFromEntity(entity);
UserName = entity.User.UserName;
}
}
public class Consumer: AuditableEntity
{
public virtual User User { get; set; }
public virtual Role Role { get; set; }
}
public class IndexModel<TIndexItem, TEntity> : ViewModel where TEntity : new()
{
public IndexModel()
{
Items = new List<TIndexItem>();
}
public List<TIndexItem> Items { get; set; }
public virtual void MapFromEntityList(IEnumerable<TEntity> entityList)
{
Items = Mapper.Map<IEnumerable<TEntity>, List<TIndexItem>>(entityList);
}
}
public class ConsumerIndexModel: IndexModel<ConsumerIndexItem, Consumer>
However, if I drive the mapping with the following code:
var model = new ConsumerIndexModel();
var list = _repository.List().Where(c => c.Parent == null).ToList();
model.MapFromEntityList(list);
return View(model);
on the line UserName = entity.User.UserName; in ConsumerIndexItem I get the following exception:
A relationship multiplicity constraint violation occurred: An EntityReference can have no more than one related object, but the query returned more than one related object. This is a non-recoverable error.
If I execute ?entity.User.UserName in the Immediate Window I get the expected user name value. What could be wrong here?
Let me explain why I had this exception and you may be able to correlate it with your own situation. I had EF Code First model mapped to the existing database. There was one-to-many relationship between two of the entities. The child table had composite primary consisting of the Id and Date. However, I missed the second segment of the primary key in my fluent map:
this.HasKey(t => t.Id);
The strange part of that was that the model worked but was throwing an exception in certain cases and it was very hard to understand why. Apparently when EF was loading the parent of child entity there were more than one parent since the key had not only Id but Date as well. The resolution was to include the second part of the key:
this.HasKey(t => new { t.Id, t.Date });
The tool that helped me to pinpoint the problem was EF Power Tools, currently it is in Beta 3. The tool gives a context menu for the EF context class where one the item is View Entity Model DDL SQL. Although I could have found this just by checking the code, the tool is nice in showing how close the EF model matches the actual database.
I believe that you're getting this exception because for a some reasons the multiplicity of relationship is violated. In my case it was incorrect mapping, in your it may be something else, I can't tell by looking at your code.
I think the problem maybe that you suppose that every user has only one consumer while this is not correct regarding data.
I had the same problem and it was because the relationship was on-to-many and I made it one-to-one.