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();
}
Related
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
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();
}
SaveChanges is not being reflected in the database
I have the following code
public async Task<int> Post([FromBody] UserEntry userEntry)
{
UserEntry dbUserEntry;
using (DBContext db = new DBContext())
{
// update
dbUserEntry = this.db.UserEntries
.Where(u => u.UserEntryID == userEntry.UserEntryID)
.Include(u => u.EntryPlayers.Select(y => y.Player))
.FirstOrDefault();
dbUserEntry.TeamName = userEntry.TeamName;
dbUserEntry.EntryPlayers = userEntry.EntryPlayers;
//db.Entry(dbUserEntry).State = EntityState.Modified;
return db.SaveChanges();
}
}
I read somewhere that I need to set the state to modified but if I uncomment the line
//db.Entry(dbUserEntry).State = EntityState.Modified;
I get the error:-
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
Any ideas on how I can get SaveChanges to work?
Use Find() method instead of FirstOrDefault()
dbUserEntry = this.db.UserEntries
.Include(u => u.EntryPlayers.Select(y => y.Player))
.Find(u => u.UserEntryID == userEntry.UserEntryID)
Is there a nice way to execute code when an entity is being saved to the database using EF/code-first?
I have a Url property on many of my entities. If a URL has not been given explicitly, I would like to calculate one as the object is persisted, eg.:
public void OnModelSaving()
{
// If a URL has not been specified, generate one from the name.
if (this.Url == null)
{
this.Url = Helper.GenerateSafeUrl(this.Title);
}
}
Ideally, I'd like all this code to be stored inside the Model, but since I don't have an EF-owned base/partial class, I suspect if it's possible, I'd have to register/wire it up elsewhere. Question is - is it possible, and if so, how do I wire it up?
The only way is to override SaveChanges method on your context, iterate through changed entities and check the Url
public override int SaveChanges()
{
var entities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added ||
e.State == EntityState.Modified)
.Select(e => e.Entity())
.OfType<YourEntityType();
foreach (var entity in entities)
{
entity.Url = ...;
}
return base.SaveChanges();
}
If you have many entity types providing Url you can try to define interface with that Url implemented by all that entity types and in OfType use that interface.
I transfer data between the entity framework and the business layer and user layer by using Data Transfer Objects. I do have some doubt, if I retrieve an object which is converted to a DTO, how do I update the correct object in the entity framework and not just insert a duplicate?
The following code will update an EF 4 entity that has been created as a controller parameter in MVC from a strongly typed view:
It seems the trick is to use the ObjectStateManager to change the state from Added to Modified once the entity has been added to the context.
MyEntities db = new MyEntities();
db.Product.AddObject(product);
db.ObjectStateManager.ChangeObjectState(product, System.Data.EntityState.Modified);
return db.SaveChanges() > 0;
As per #Sean Mills comment if you are using EF5 use:
((IObjectContextAdapter) db).ObjectContext.ObjectStateManager.ChangeObjectState(entity, System.Data.EntityState.Added);
an old question, but just in case someone needs a code solution:
http://www.mikesdotnetting.com/Article/110/ASP.NET-MVC-Entity-Framework-Modifying-One-to-Many-and-Many-to-Many-Relationships
Example:
public void EditArticle(
Article article, string articleTypeId, string[] categoryId)
{
var id = 0;
Article art = de.ArticleSet
.Include("ArticleTypes")
.Include("Categories")
.Where(a => a.ArticleID == article.ArticleID)
.First();
var count = art.Categories.Count;
for (var i = 0; i < count; i++)
{
art.Categories.Remove(art.Categories.ElementAt(i));
count--;
}
foreach (var c in categoryId)
{
id = int.Parse(c);
Category category = de.CategorySet
.Where(ct => ct.CategoryID == id).First();
art.Categories.Add(category);
}
art.Headline = article.Headline;
art.Abstract = article.Abstract;
art.Maintext = article.Maintext;
art.DateAmended = DateTime.Now;
art.ArticleTypesReference.EntityKey = new EntityKey(
"DotnettingEntities.ArticleTypeSet",
"ArticleTypeID",
int.Parse(articleTypeId)
);
de.SaveChanges();
}
//I am replacing player :)
public ActionResult ProductEdit(string Id, Product product)
{
int IdInt = DecyrptParameter(Id);
MyEntities db = new MyEntities();
var productToDetach = db.Products.FirstOrDefault(p=> p.Id == IdInt);
if (product == null)
throw new Exception("Product already deleted"); //I check if exists, maybe additional check if authorised to edit
db.Detach(productToDetach);
db.AttachTo("Products", product);
db.ObjectStateManager.ChangeObjectState(product, System.Data.EntityState.Modified);
db.SaveChanges();
ViewData["Result"] = 1; // successful result
return View();
}
You would need to include a primary or alternate key in the DTO, then match that key back to the correct EF entity upon update.
This should work for EF 5: https://stackoverflow.com/a/11749716/540802:
db.Entry(product).State = EntityState.Modified;