I'm using EF Code First but my models have NO relationships (PKs - FKs). So I'm trying to find a way to workaround it by using EF6 Reflections in order to avoid an entry deletion that would have relationships (same property name).
Lookup over all my context entities in which has any specific property (FK);
For every entity found, check if this entity has any entry;
If its true, instead of deleting my entry, set a property "Canceled" as true;
If its false, keep entity state deleted and save my context changes;
public override int SaveChanges()
{
foreach (var myEntity in ChangeTracker.Entries<IAuditable>())
{
if (myEntity.State == EntityState.Deleted)
{
ObjectContext objContext = ((IObjectContextAdapter)this).ObjectContext;
var container = objContext.MetadataWorkspace.GetEntityContainer(objContext.DefaultContainerName, DataSpace.CSpace);
var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(myEntity.Entity);
var entityKeys = objectStateEntry.EntityKey.EntityKeyValues;
var entity = myEntity;
var hasAnyFk = false;
foreach (var entityKey in entityKeys)
{
if (hasAnyFk)
{
break;
}
var keyName = entityKey.Key;
foreach (var entitySet in container.EntitySets)
{
hasAnyFk = entitySet.ElementType.Members.Any(es => es.Name == keyName);
if (hasAnyFk)
{
break;
}
}
}
if (hasAnyFk)
{
var deletedProperty = myEntity.OriginalValues.PropertyNames.Where(p => myEntity.Property(p).Name == "Deleted").FirstOrDefault();
if (deletedProperty != null)
{
myEntity.State = EntityState.Modified;
myEntity.CurrentValues[deletedProperty] = true;
}
}
}
}
return base.SaveChanges();
}
You can handle this is an overload of SaveChanges:
public override int SaveChanges()
{
foreach (var entry in this.ChangeTracker.Entries().Where(e => e.State ==
System.Data.Entity.EntityState.Deleted).ToList())
{
var delPropName = "IsDeleted";
if (entry.OriginalValues.PropertyNames.Contains(delPropName))
{
var delProp = entry.Property(delPropName);
delProp.CurrentValue = true;
entry.State = System.Data.Entity.EntityState.Modified;
}
}
return base.SaveChanges();
}
Here, entry.OriginalValues.PropertyNames is used to check if the property exists in the entity and then its value is set and the entry's state is changed to Modified. Note that I loop through this.ChangeTracker.Entries() after applying ToList(), otherwise the content of the collection changes while looping through it.
Related
I want to get changes occured in an entity and related datas attached to it.
I know how to get the property names that have changed in one entity:
dbContext.Entry(entity).Properties.Where(x => x.IsModified).Select(x => x.Metadata.Name).ToList();
How to do the same for related data in navigation properties ?
Based on this article (Entity Change Tracking using DbContext in Entity Framework 6), you should override SaveChanges() method to track entity changes and its related entities.
public override int SaveChanges()
{
return base.SaveChanges();
}
Actually, You should change the above code to the following sample:
public override int SaveChanges()
{
var modifiedEntities = ChangeTracker.Entries()
.Where(p => p.State == EntityState.Modified).ToList();
var now = DateTime.UtcNow;
foreach (var change in modifiedEntities)
{
var entityName = change.Entity.GetType().Name;
var primaryKey = GetPrimaryKeyValue(change);
foreach(var prop in change.OriginalValues.PropertyNames)
{
var originalValue = change.OriginalValues[prop].ToString();
var currentValue = change.CurrentValues[prop].ToString();
if (originalValue != currentValue) //Only create a log if the value changes
{
//Create the Change Log
}
}
}
return base.SaveChanges();
}
I can get all the entities marked as IsDeleted = true, applying query filters,
where IsDeleted is a field of my entities.
Now, my question is very simple, how to make a soft delete in cascade with Entity Framework Core when I am soft deleting an entity that has navigation properties that I want to mark as IsDeleted too.
I use following code to accomplish a cascading delete. Thanks to #Zinov and ajcvickers. Based on https://github.com/aspnet/EntityFrameworkCore/issues/11240
//..
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
foreach (var navigationEntry in entry.Navigations.Where(n => !n.Metadata.IsDependentToPrincipal()))
{
if (navigationEntry is CollectionEntry collectionEntry)
{
foreach (var dependentEntry in collectionEntry.CurrentValue)
{
HandleDependent(Entry(dependentEntry));
}
}
else
{
var dependentEntry = navigationEntry.CurrentValue;
if (dependentEntry != null)
{
HandleDependent(Entry(dependentEntry));
}
}
}
break;
}
private void HandleDependent(EntityEntry entry)
{
entry.CurrentValues["IsDeleted"] = true;
}
You can implement these two step basicly for make cascade soft delete.
First, write a event for override SaveChanges.
private void DbContextBase_SavingChanges(object? sender, SavingChangesEventArgs e)
{
var objectContext = (DbContextBase)sender;
var modifiedEntities =
objectContext.ChangeTracker.Entries().Where(c => c.State is EntityState.Added or EntityState.Modified or EntityState.Deleted);
foreach (var entry in modifiedEntities)
{
if (entry.State == EntityState.Added)
{
entry.Property("CreatedTime").CurrentValue = DateTime.UtcNow;
}
if (entry.State == EntityState.Modified)
{
entry.Property("ModifiedTime").CurrentValue = DateTime.UtcNow;
}
if (entry.State == EntityState.Deleted)
{
entry.Property("IsDeleted").CurrentValue = true;
entry.State = EntityState.Modified;
}
}
}
Implement from DbContext constructor
protected ExampleDbContext(DbContextOptions options) : base(options)
{
this.SavingChanges += DbContextBase_SavingChanges;
}
Secondly, just make navigation property cascade delete on configuration.
builder.HasMany<ExampleNavigationEntity>(c => c.ExampleNavigation).WithOne().OnDelete(DeleteBehavior.Cascade);
I am trying to override Entity Framework's SaveChanges() method to save auditing information. I begin with the following:
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;
List<ObjectStateEntry> objectStateEntryList = ctx.ObjectStateManager.GetObjectStateEntries(
EntityState.Added
| EntityState.Modified
| EntityState.Deleted).ToList();
foreach (ObjectStateEntry entry in objectStateEntryList)
{
if (!entry.IsRelationship)
{
//Code that checks and records which entity (table) is being worked with
...
foreach (string propertyName in entry.GetModifiedProperties())
{
DbDataRecord original = entry.OriginalValues;
string oldValue = original.GetValue(original.GetOrdinal(propertyName)).ToString();
CurrentValueRecord current = entry.CurrentValues;
string newValue = current.GetValue(current.GetOrdinal(propertyName)).ToString();
if (oldValue != newValue)
{
AuditEntityField field = new AuditEntityField
{
FieldName = propertyName,
OldValue = oldValue,
NewValue = newValue,
Timestamp = auditEntity.Timestamp
};
auditEntity.AuditEntityField.Add(field);
}
}
}
}
}
Problem I'm having is that the values I get in entry.OriginalValues and in entry.CurrentValues is always the new updated value:
The problem has been found:
public ActionResult Edit(Branch branch)
{
if (ModelState.IsValid)
{
db.Entry(branch).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(branch);
}
Saving an update in the above way seems to cause the ChangeTracker to not pick up the old values. I fixed this by simply making use of ViewModels for all the required updates:
public ActionResult Edit(BranchViewModel model)
{
if (ModelState.IsValid)
{
Branch branch = db.Branch.Find(model.BranchId);
if (branch.BranchName != model.BranchName)
{
branch.BranchName = model.BranchName;
db.SaveChanges();
}
return RedirectToAction("Index");
}
return View(model);
}
To get a copy of the old values from the database without updating the EF entity you can use
context.Entry(yourEntity).GetDatabaseValues().ToObject()
or
yourEntity.GetDatabaseValues().ToObject()
Caution: this does instigate a database call.
The easiest way to do for me was:
var cadastralAreaDb = await _context.CadastralAreas.FindAsync(id).ConfigureAwait(false);
var audit = new Audit
{
CreatedBy = "Edi"
};
_context.Entry(cadastralAreaDb).CurrentValues.SetValues(cadastralArea);
await _context.SaveChangesAsync(audit).ConfigureAwait(false);
This is tested in ASP NET Core 3.1
Whether attaching parent entity to the context again and updating it should update the child entity? Am i missing something?
Or should I necessarily write the updating EF logic (in DAL) for the child entity?
This is my sample code model:
ChildEntity entityChild;
if (ParentEntity.ChildEntity.SingleOrDefault() != null)
entityChild = ParentEntity.ChildEntity.SingleOrDefault();
else
{
entityChild = new ChildEntity();
ParentEntity.ChildEntity.Add(entityChild);
}
entityChild.ColumnA= txtA.Text;
entityChild.ColumnB= txtB.Text;
// Send entityParent for update
_objParent.Update(entityParent)
_objParent.Update() code:
context.vouchers.Attach(entityParent);
ObjectStateEntry objectState = context.ObjectStateManager.GetObjectStateEntry(entityParent);
objectState.ChangeState(System.Data.EntityState.Modified);
context.SaveChanges();
UPDATE (Parent Loading Code Sample)
public ParentEntity GetById(int id)
{
using (var context = new DBEntities())
{
ParentEntity _entity = context.ParentEntity
.Include("ChildEntity")
.Where(e => e.parent_id == id);
return (ParentEntity)_entity.SingleOrDefault()
}
}
EF only track changes from the point where it gets to know about the object. The most simple way to work with updates in EF is to load the existing object from the DB, update it and then save it back. In this case it means that you should load the ParentEntity object from the DB.
The other approach is to use Attach as you do. In that case you should first call attach the unchanged ParentEntity and then call ParentEntity.ChildEntity.Add(entityChild).
Another alternative is to explicitly add the new ChildEntity directly to the DbContext, then you could simply set the foreign key value of the ChildEntity to the key value of the ParentEntity to make them connected.
You should load and update your entities inside the using statement - this way all the changes will be tracked by the Entity Framework:
using (var context = new DBEntities())
{
// load and update entities
// ....
context.SaveChanges();
}
UPDATE - This is just an example, I'll keep it simple. I would create a service where I'd put my logic - something like this:
public class ParentService
{
// other service methods here
protected DBEntities CreateContext()
{
return new DBEntities();
}
public ParentEntity Update(int id, string columnA, string columnB)
{
ParentEntity _entity = null;
using (var context = CreateContext())
{
bool isNew = false;
_entity = context.ParentEntity
.Include("ChildEntity")
.SingleOrDefault(e => e.parent_id == id);
ChildEntity entityChild = ParentEntity.ChildEntity.SingleOrDefault();
if(entityChild == null)
{
entityChild = new ChildEntity();
isNew = true;
}
entityChild.ColumnA = columnA;
entityChild.ColumnB = columnB;
if(isNew)
{
ParentEntity.ChildEntity.Add(entityChild);
}
context.SaveChanges();
}
return _entity;
}
}
In the UI code:
string id = .....;
string columnA= txtA.Text;
strign columnB = txtB.Text;
var service = new ParentService();
ParentEntity parent = service.Update(id, columnA, columnB);
So I want to keep track of the last change done to any of my objects, this last change is saved on the "head" object. I'll sketch it.
I have an object Project containing a list of Activities, which in turn has a list of Actions both with different properties and so on.
The Project has a property LastUpdateDate on which I want to keep the last update done on any property of the Project itself or any of its 'subobjects'.
For this I override the SaveChanges method of the entity framework as follows:
public override int SaveChanges() {
var changedEntries = ChangeTracker.Entries();
if (changedEntries != null)
{
var dbEntityEntries = changedEntries as IList<DbEntityEntry> ?? changedEntries.ToList();
foreach (var entry in dbEntityEntries.Where(c => c.State != EntityState.Unchanged))
{
var proj = entry.Entity as Project;
if (proj != null)
{
proj.LastUpdateDate = DateTime.UtcNow;
}
var prop = entry.Entity as Activity;
if (prop != null)
{
var changedProject = dbEntityEntries.Single(x => (x.Entity is Project) && ((Project) x.Entity).Id == prop.ProjectId);
((Project) changedProject.Entity).LastUpdateDate = DateTime.UtcNow;
}
}
}
return base.SaveChanges();
}
My dataset is larger and has more properties than in the example above, therefor I dont want to be looping through every entity because this will take up too much time.
In stead of this, I would like to access my ObjectContext so I can say for instance: project[0].Activity[1]...
Has anyone here got any idea on how to reach this situation?
I can answer the first part of this question: Here's how you access the ObjectContext:
public override int SaveChanges()
{
this.ChangeTracker.DetectChanges();
var objectContext = ((IObjectContextAdapter)this).ObjectContext;
}
Try this:
public override int SaveChanges()
{
var changedEntries = ChangeTracker.Entries();
if (changedEntries != null)
{
var dbEntityEntries = changedEntries as IList<DbEntityEntry> ?? changedEntries.ToList();
dbEntityEntries.Select(i=>i.Entity).OfType<Project>().Distinct()
.Union(dbEntityEntries.Select(i=>i.Entity).OfType<Activity>().Select(activity=>activity.Project))
.Union(dbEntityEntries.Select(i=>i.Entity).OfType<Action>().Select(action => action.Activity.Project))
.Distinct().ToList()
.ForEach(p => p.LastUpdateDate = DateTime.UtcNow);
}
return base.SaveChanges();
}