Entity Framework - Delete Doesn't Update Grandchild Entity from Database - entity-framework

I have Widget entities that belong to Company entities. There is a one-to-many relationship between Companies and Widgets.
I have WidgetDetail entities that belong to Widget entities. There is a one-to-many relationship between Widgets and WidgetDetails.
Companies have a CompanyId.
Widgets have a CompanyId and WidgetId.
WidgetDetails have a WidgetId and a WidgetDetailId.
Here's my first pass at the Delete method to Delete a WidgetDetail:
public async Task<ActionResult> DeleteWidgetDetail([FromRoute] int companyId, [FromRoute] int widgetId, int widgetDetailId)
{
WidgetDetail widgetDetail = await _myContext.Widgets
.Where(w => w.CompanyId == companyId && w.WidgetId == widgetId)
.AsNoTracking()
.SelectMany(w => w.WidgetDetails)
.Where(wd => wd.WidgetDetailId == widgetDetailId)
.FirstOrDefaultAsync();
if (widgetDetail == null)
{
return NotFound();
}
else
{
widgetDetail.IsDeleted = true;
await _myContext.SaveChangesAsync();
return Ok();
}
}
I can step into it, and see it set the IsDeleted property to false, but that change is not committed to the database.

D'oh... found my own problem.
I removed the AsNoTracking() method and that solved the problem. I misunderstood what it was doing.
see here:
https://learn.microsoft.com/en-us/ef/core/querying/tracking
for details

Related

How to get CascadeDeleted entities in change tracker

Since EF Core 3 the delete-behaviour changed. I want to implement soft delete, and I thought this might be helpful...
I have lazy loading enabled and a generic delete method:
this.context.Set<TEntity>().Remove(entity);
Before SaveChanges I add meta information.
First I try to find all ChangeTracker entries like this:
var entityEntries = dbContext.ChangeTracker.Entries()
.Where(x => x.Entity is IMyMetaDataBase
&& (x.State == EntityState.Added
|| x.State == EntityState.Modified
|| x.State == EntityState.Deleted));
But I only find the principal entity in above collection to perform soft delete like this:
if (entityEntry.Entity is IMyDeletableInterface deletable && entityEntry.State == EntityState.Deleted)
{
EntityEntry.State = EntityState.Modified;
deletable.Deleted = true;
}
If I set the EntityState modified (which is the thing to do I suppose) the principal entity is set deleted, but all dependent entities remain unchanged. If I donĀ“t change the EntityState, all dependent entities will be removed.
My main question: Is there a way to find the entities, EF will remove in cascade operation..?
netcoreapp3.1, ef core 3.1.8
Something like this would help.
public override int SaveChanges()
{
// Soft-Delete
var entities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted && e.Metadata.GetProperties()
.Any(x => x.Name == "IsDeleted"))
.ToList();
foreach (var entity in entities)
{
entity.State = EntityState.Unchanged;
entity.CurrentValues["IsDeleted"] = true;
}
return base.SaveChanges();
}

How to soft delete one to many relationships in EF core?

Let's say that I have a Category and Product entity with one to many relationship and when I delete Category I want to delete all products that belong to the category. And by deleting I mean setting IsDeleted flag to true since I don't want to delete it for real (which I could by specifying on delete cascade). I found out the way to set IsDeleted to true when Category is deleted however I can't figure out how to find products of that category and do the same thing for them. Any help?
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
foreach(var item in ChangeTracker.Entries<Category>().Where(e => e.State == EntityState.Deleted))
{
item.State = EntityState.Modified;
item.CurrentValues["IsDeleted"] = true;
}
return base.SaveChanges();
}
I also specified query filter so that I don't get deleted items
builder.Entity<Category>().Property<bool>("IsDeleted");
builder.HasQueryFilter(c => !EF.Property<bool>(c, "IsDeleted"));
Just use on cascade delete. When you delete category also all products of that category will be marked as deleted based on navigation property. This will be done by EFCore itself. I'm using this aproach in my code to soft delete one to many relation. Then use (so your code repeated on products):
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
foreach(var item in ChangeTracker.Entries<Category>().Where(e => e.State
== EntityState.Deleted))
{
item.State = EntityState.Modified;
item.CurrentValues["IsDeleted"] = true;
}
foreach(var item in ChangeTracker.Entries<Product>().Where(e => e.State
== EntityState.Deleted))
{
item.State = EntityState.Modified;
item.CurrentValues["IsDeleted"] = true;
}
return base.SaveChanges();
}

Eager Loading of Navigation Properties

In my repository I'm trying to use eager loading to load related entities. Not sure why but it seems like when I return all instances of a particular entity, related entities are returned, but when I limit the results returned the related entities are not included in the results.
This code in the service layer is returning all orders, including related Customer, OrderItem, and Product entities:
public async Task<IEnumerable<Order>> GetOrdersAsync()
{
return await _repository.GetAsync(null, q => q.OrderByDescending(p => p.CreatedDate), "Customer", "OrderItems", "OrderItems.Product");
}
In the repository:
public async Task<IEnumerable<Order>> GetAsync(Expression<Func<Order, bool>> where = null, Func<IQueryable<Order>, IOrderedQueryable<Order>> orderBy = null, params string[] navigationProperties)
{
IQueryable<Order> query = _context.Set<Order>();
if (where != null)
{
query = query.Where(where);
}
//Apply eager loading
foreach (string navigationProperty in navigationProperties)
query = query.Include(navigationProperty);
if (orderBy != null)
{
return await orderBy(query).ToListAsync();
}
else
{
return await query.ToListAsync();
}
}
This code in the service layer is getting an order by id, but for whatever reason is not returning related Customer, OrderItem, and Product entities:
public async Task<Order> GetOrderByIdAsync(long id)
{
return await _repository.GetByIdAsync(id, "Customer", "OrderItems", "OrderItems.Product");
}
In the repository:
public async Task<Order> GetByIdAsync(long id, params string[] navigationProperties)
{
DbSet<Order> dbSet = _context.Set<Order>();
foreach (string navigationProperty in navigationProperties)
dbSet.Include(navigationProperty);
return await dbSet.FindAsync(id);
}
The one difference I see between the two repository methods is one is casting the _context.Set() to IQueryable before including navigation properties, while the other is calling Include directly on the DbSet itself. Should this matter?
So the repository method that was not working was using DBSet.Include incorrectly. Include is actually a method on the DBQuery class, which DBSet inherits from. I needed to use DBQuery.Include to replace the query with a new instance of the query for each navigation property I included. So I changed the implementation to:
public async Task<Order> GetByIdAsync(long id, params string[] navigationProperties)
{
DbQuery<Order> dbQuery = _context.Set<Order>();
foreach (string navigationProperty in navigationProperties)
dbQuery = dbQuery.Include(navigationProperty);
return await dbQuery.Where(o => o.Id == id).FirstOrDefaultAsync();
}

Clear related items followed by parent entity update - Many to Many EntityFramework with AutoMapper

I am trying to remove all references followed by adding them back from a list of disconnected objects.
using(var scope = new TransactionScope())
{
_autoIncidentService.AddNewCompanyVehicles(
autoIncidentModel.CompanyVehiclesInvolved.Where(v => v.Id == 0));
_autoIncidentService.ClearCollections(autoIncidentModel.Id);
_autoIncidentService.Update(autoIncidentModel);
scope.Complete();
}
return Json(ResponseView);
The ClearCollections removes items references. The GetAutoIncident includes the collection.
public void ClearCollections(int id)
{
var autoIncident = GetAutoIncident(id);
foreach (var vehicle in autoIncident.CompanyVehiclesInvolved.ToArray())
{
autoIncident.CompanyVehiclesInvolved.Remove(vehicle);
}
db.SaveChanges();
}
When I try to update the entity right after the ClearCollections method it fails.
The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.
I am using a singleton to get the DbContext so there shouldn't be any situation where the context is different. It is being stored in the HttpContext.Current.Items.
The update method is as follows:
public override void Update(AutoIncidentModel model)
{
var data = GetData(model.Id);
Mapper.CreateMap<AutoIncidentModel, AutoIncident>()
.ForMember(m => m.CompanyVehiclesInvolved, opt => opt.ResolveUsing(m =>
{
var ids = m.CompanyVehiclesInvolved.Select(v => v.Id);
return db.Vehicles.Where(v => ids.Contains(v.Id)).ToList();
}));
Mapper.Map(model, data);
db.SaveChanges();
}
Obviously, I am missing something important here. Do the entites from my ResolveUsing method need to somehow be associated with the parent entity or is automapper overwriting the property (CompanyVehiclesInvolved) and causing a problem?

Cascading Properties in Entity Framework via Repository Pattern

Say I have a class Person that has an "IsArchived" property. A Person can have a Collection - each of which can also be Archived.
Right now I have an UpdatePerson method in my repository that looks like this:
public Person UpdatePerson(Person person)
{
_db.Persons.Attach(person);
var entry = _db.Entry(person);
entry.Property(e => e.Name).IsModified = true;
entry.Property(e => e.Friends).IsModified = true;
SaveChanges();
return entry.Entity;
}
And I have a separate Repository method for Archive:
public void ArchivePerson(int personId)
{
var person = _db.Persons.FirstOrDefault(c => c.PersonId == personId);
if (person != null)
{
person.IsArchived = true;
foreach (var friend in person.Friends)
{
ArchivePerson(friend.PersonId);
}
SaveChanges();
}
}
This means that from my WebAPI, I have to dedicate a function to Archiving/Unarchiving. It would be cool if I could just call Update and set the Archived for the given entity, and apply this to all child entities where possible. Is there any way to accomplish this, or am I best off sticking to the ArchivePerson repository method?