Access object context in override SaveChanges - c#

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();
}

Related

EntityFramework Core: Get changes occured in entity and related data

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();
}

Search all entities with specific property and their entries

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.

EF ObjectStateEntry OriginalValues contains CurrentValues

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

How to add/update child entities when updating a parent entity in EF

The two entities are one-to-many relationship (built by code first fluent api).
public class Parent
{
public Parent()
{
this.Children = new List<Child>();
}
public int Id { get; set; }
public virtual ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public string Data { get; set; }
}
In my WebApi controller I have actions to create a parent entity(which is working fine) and update a parent entity(which has some problem). The update action looks like:
public void Update(UpdateParentModel model)
{
//what should be done here?
}
Currently I have two ideas:
Get a tracked parent entity named existing by model.Id, and assign values in model one by one to the entity. This sounds stupid. And in model.Children I don't know which child is new, which child is modified(or even deleted).
Create a new parent entity via model, and attached it to the DbContext and save it. But how can the DbContext know the state of children (new add/delete/modified)?
What's the correct way of implement this feature?
Because the model that gets posted to the WebApi controller is detached from any entity-framework (EF) context, the only option is to load the object graph (parent including its children) from the database and compare which children have been added, deleted or updated. (Unless you would track the changes with your own tracking mechanism during the detached state (in the browser or wherever) which in my opinion is more complex than the following.) It could look like this:
public void Update(UpdateParentModel model)
{
var existingParent = _dbContext.Parents
.Where(p => p.Id == model.Id)
.Include(p => p.Children)
.SingleOrDefault();
if (existingParent != null)
{
// Update parent
_dbContext.Entry(existingParent).CurrentValues.SetValues(model);
// Delete children
foreach (var existingChild in existingParent.Children.ToList())
{
if (!model.Children.Any(c => c.Id == existingChild.Id))
_dbContext.Children.Remove(existingChild);
}
// Update and Insert children
foreach (var childModel in model.Children)
{
var existingChild = existingParent.Children
.Where(c => c.Id == childModel.Id && c.Id != default(int))
.SingleOrDefault();
if (existingChild != null)
// Update child
_dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
else
{
// Insert child
var newChild = new Child
{
Data = childModel.Data,
//...
};
existingParent.Children.Add(newChild);
}
}
_dbContext.SaveChanges();
}
}
...CurrentValues.SetValues can take any object and maps property values to the attached entity based on the property name. If the property names in your model are different from the names in the entity you can't use this method and must assign the values one by one.
OK guys. I had this answer once but lost it along the way. absolute torture when you know there's a better way but can't remember it or find it! It's very simple. I just tested it multiple ways.
var parent = _dbContext.Parents
.Where(p => p.Id == model.Id)
.Include(p => p.Children)
.FirstOrDefault();
parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;
_dbContext.SaveChanges();
You can replace the whole list with a new one! The SQL code will remove and add entities as needed. No need to concern yourself with that. Be sure to include child collection or no dice. Good luck!
I've been messing about with something like this...
protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
{
var dbItems = selector(dbItem).ToList();
var newItems = selector(newItem).ToList();
if (dbItems == null && newItems == null)
return;
var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();
var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));
var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
}
which you can call with something like:
UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)
Unfortunately, this kinda falls over if there are collection properties on the child type which also need to be updated. Considering trying to solve this by passing an IRepository (with basic CRUD methods) which would be responsible for calling UpdateChildCollection on its own. Would call the repo instead of direct calls to DbContext.Entry.
Have no idea how this will all perform at scale, but not sure what else to do with this problem.
If you are using EntityFrameworkCore you can do the following in your controller post action (The Attach method recursively attaches navigation properties including collections):
_context.Attach(modelPostedToController);
IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);
foreach(EntityEntry ee in unchangedEntities){
ee.State = EntityState.Modified;
}
await _context.SaveChangesAsync();
It is assumed that each entity that was updated has all properties set and provided in the post data from the client (eg. won't work for partial update of an entity).
You also need to make sure that you are using a new/dedicated entity framework database context for this operation.
public async Task<IHttpActionResult> PutParent(int id, Parent parent)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != parent.Id)
{
return BadRequest();
}
db.Entry(parent).State = EntityState.Modified;
foreach (Child child in parent.Children)
{
db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
}
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ParentExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return Ok(db.Parents.Find(id));
}
This is how I solved this problem. This way, EF knows which to add which to update.
This ought to do it...
private void Reconcile<T>(DbContext context,
IReadOnlyCollection<T> oldItems,
IReadOnlyCollection<T> newItems,
Func<T, T, bool> compare)
{
var itemsToAdd = new List<T>();
var itemsToRemove = new List<T>();
foreach (T newItem in newItems)
{
T oldItem = oldItems.FirstOrDefault(arg1 => compare(arg1, newItem));
if (oldItem == null)
{
itemsToAdd.Add(newItem);
}
else
{
context.Entry(oldItem).CurrentValues.SetValues(newItem);
}
}
foreach (T oldItem in oldItems)
{
if (!newItems.Any(arg1 => compare(arg1, oldItem)))
{
itemsToRemove.Add(oldItem);
}
}
foreach (T item in itemsToAdd)
context.Add(item);
foreach (T item in itemsToRemove)
context.Remove(item);
}
Because I hate repeating complex logic, here's a generic version of Slauma's solution.
Here's my update method. Note that in a detached scenario, sometimes your code will read data and then update it, so it's not always detached.
public async Task UpdateAsync(TempOrder order)
{
order.CheckNotNull(nameof(order));
order.OrderId.CheckNotNull(nameof(order.OrderId));
order.DateModified = _dateService.UtcNow;
if (_context.Entry(order).State == EntityState.Modified)
{
await _context.SaveChangesAsync().ConfigureAwait(false);
}
else // Detached.
{
var existing = await SelectAsync(order.OrderId!.Value).ConfigureAwait(false);
if (existing != null)
{
order.DateModified = _dateService.UtcNow;
_context.TrackChildChanges(order.Products, existing.Products, (a, b) => a.OrderProductId == b.OrderProductId);
await _context.SaveChangesAsync(order, existing).ConfigureAwait(false);
}
}
}
CheckNotNull is defined here.
Create these extension methods.
/// <summary>
/// Tracks changes on childs models by comparing with latest database state.
/// </summary>
/// <typeparam name="T">The type of model to track.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="childs">The childs to update, detached from the context.</param>
/// <param name="existingChilds">The latest existing data, attached to the context.</param>
/// <param name="match">A function to match models by their primary key(s).</param>
public static void TrackChildChanges<T>(this DbContext context, IList<T> childs, IList<T> existingChilds, Func<T, T, bool> match)
where T : class
{
context.CheckNotNull(nameof(context));
childs.CheckNotNull(nameof(childs));
existingChilds.CheckNotNull(nameof(existingChilds));
// Delete childs.
foreach (var existing in existingChilds.ToList())
{
if (!childs.Any(c => match(c, existing)))
{
existingChilds.Remove(existing);
}
}
// Update and Insert childs.
var existingChildsCopy = existingChilds.ToList();
foreach (var item in childs.ToList())
{
var existing = existingChildsCopy
.Where(c => match(c, item))
.SingleOrDefault();
if (existing != null)
{
// Update child.
context.Entry(existing).CurrentValues.SetValues(item);
}
else
{
// Insert child.
existingChilds.Add(item);
// context.Entry(item).State = EntityState.Added;
}
}
}
/// <summary>
/// Saves changes to a detached model by comparing it with the latest data.
/// </summary>
/// <typeparam name="T">The type of model to save.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="model">The model object to save.</param>
/// <param name="existing">The latest model data.</param>
public static void SaveChanges<T>(this DbContext context, T model, T existing)
where T : class
{
context.CheckNotNull(nameof(context));
model.CheckNotNull(nameof(context));
context.Entry(existing).CurrentValues.SetValues(model);
context.SaveChanges();
}
/// <summary>
/// Saves changes to a detached model by comparing it with the latest data.
/// </summary>
/// <typeparam name="T">The type of model to save.</typeparam>
/// <param name="context">The database context tracking changes.</param>
/// <param name="model">The model object to save.</param>
/// <param name="existing">The latest model data.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns></returns>
public static async Task SaveChangesAsync<T>(this DbContext context, T model, T existing, CancellationToken cancellationToken = default)
where T : class
{
context.CheckNotNull(nameof(context));
model.CheckNotNull(nameof(context));
context.Entry(existing).CurrentValues.SetValues(model);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
Just proof of concept Controler.UpdateModel won't work correctly.
Full class here:
const string PK = "Id";
protected Models.Entities con;
protected System.Data.Entity.DbSet<T> model;
private void TestUpdate(object item)
{
var props = item.GetType().GetProperties();
foreach (var prop in props)
{
object value = prop.GetValue(item);
if (prop.PropertyType.IsInterface && value != null)
{
foreach (var iItem in (System.Collections.IEnumerable)value)
{
TestUpdate(iItem);
}
}
}
int id = (int)item.GetType().GetProperty(PK).GetValue(item);
if (id == 0)
{
con.Entry(item).State = System.Data.Entity.EntityState.Added;
}
else
{
con.Entry(item).State = System.Data.Entity.EntityState.Modified;
}
}
So, I finally managed to get it working, although not fully automatically.
Notice the AutoMapper <3. It handles all the mapping of properties so you don't have to do it manually. Also, if used in a way where it maps from one object to another, then it only updates the properties and that marks changed properties as Modified to EF, which is what we want.
If you would use explicit context.Update(entity), the difference would be that entire object would be marked as Modified and EVERY prop would be updated.
In that case you don't need tracking but the drawbacks are as mentioned.
Maybe that's not a problem for you but it's more expensive and I want to log exact changes inside Save so I need correct info.
// We always want tracking for auto-updates
var entityToUpdate = unitOfWork.GetGenericRepository<Article, int>()
.GetAllActive() // Uses EF tracking
.Include(e => e.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active))
.First(e => e.Id == request.Id);
mapper.Map(request, entityToUpdate); // Maps it to entity with AutoMapper <3
ModifyBarcodes(entityToUpdate, request);
// Removed part of the code for space
unitOfWork.Save();
ModifyBarcodes part here.
We want to modify our collection in a way that EF tracking won't end up messed up.
AutoMapper mapping would, unforunately, create a completely new instance of collection, there fore messing up the tracking, although, I was pretty sure it should work.
Anyways, since I'm sending complete list from FE, here we actually determine what should be Added/Updated/Deleted and just handle the list itself.
Since EF tracking is ON, EF handles it like a charm.
var toUpdate = article.Barcodes
.Where(e => articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
.ToList();
toUpdate.ForEach(e =>
{
var newValue = articleDto.Barcodes.FirstOrDefault(f => f.Id == e.Id);
mapper.Map(newValue, e);
});
var toAdd = articleDto.Barcodes
.Where(e => !article.Barcodes.Select(b => b.Id).Contains(e.Id))
.Select(e => mapper.Map<Barcode>(e))
.ToList();
article.Barcodes.AddRange(toAdd);
article.Barcodes
.Where(e => !articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
.ToList()
.ForEach(e => article.Barcodes.Remove(e));
CreateMap<ArticleDto, Article>()
.ForMember(e => e.DateCreated, opt => opt.Ignore())
.ForMember(e => e.DateModified, opt => opt.Ignore())
.ForMember(e => e.CreatedById, opt => opt.Ignore())
.ForMember(e => e.LastModifiedById, opt => opt.Ignore())
.ForMember(e => e.Status, opt => opt.Ignore())
// When mapping collections, the reference itself is destroyed
// hence f* up EF tracking and makes it think all previous is deleted
// Better to leave it on manual and handle collecion manually
.ForMember(e => e.Barcodes, opt => opt.Ignore())
.ReverseMap()
.ForMember(e => e.Barcodes, opt => opt.MapFrom(src => src.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active)));
For VB.NET developers Use this generic sub to mark the child state, easy to use
Notes:
PromatCon: the entity object
amList: is the child list that you want to add or modify
rList: is the child list that you want to remove
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
If amList IsNot Nothing Then
For Each obj In amList
Dim x = PromatCon.Entry(obj).GetDatabaseValues()
If x Is Nothing Then
PromatCon.Entry(obj).State = EntityState.Added
Else
PromatCon.Entry(obj).State = EntityState.Modified
End If
Next
End If
If rList IsNot Nothing Then
For Each obj In rList.ToList
PromatCon.Entry(obj).State = EntityState.Deleted
Next
End If
End Sub
PromatCon.SaveChanges()
Here is my code that works just fine.
public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
decimal motohours, int driverID, List<int> commission,
string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
bool isTransportation, string violationConditions, DateTime shutdownStartTime,
DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
{
try
{
using (var db = new GJobEntities())
{
var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);
if (isExisting != null)
{
isExisting.AreaID = areaID;
isExisting.DriverID = driverID;
isExisting.IsTransportation = isTransportation;
isExisting.Mileage = mileage;
isExisting.Motohours = motohours;
isExisting.Notes = notes;
isExisting.DeviceShutdownDesc = deviceShutdownDesc;
isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
isExisting.ShutdownAtTime = shutdownAtTime;
isExisting.ShutdownEndTime = shutdownEndTime;
isExisting.ShutdownStartTime = shutdownStartTime;
isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
isExisting.ViolationConditions = violationConditions;
// Delete children
foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
{
db.DeviceShutdownFaults.Remove(existingChild);
}
if (faultIDs != null && faultIDs.Any())
{
foreach (var faultItem in faultIDs)
{
var newChild = new DeviceShutdownFault
{
ID = Guid.NewGuid(),
DDFaultID = faultItem,
DeviceShutdownID = isExisting.ID,
};
isExisting.DeviceShutdownFaults.Add(newChild);
}
}
// Delete all children
foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
{
db.DeviceShutdownComissions.Remove(existingChild);
}
// Add all new children
if (commission != null && commission.Any())
{
foreach (var cItem in commission)
{
var newChild = new DeviceShutdownComission
{
ID = Guid.NewGuid(),
PersonalID = cItem,
DeviceShutdownID = isExisting.ID,
};
isExisting.DeviceShutdownComissions.Add(newChild);
}
}
await db.SaveChangesAsync();
return true;
}
}
}
catch (Exception ex)
{
logger.Error(ex);
}
return false;
}
It's not the most elegant procedure, but it works. Cheers!
var entity = await context.Entities.FindAsync(id);
var newEntity = new AmazingEntity() {
p1 = child1
p2 = child2
p3 = child3.child4 //... nested collections
};
if (entity != null)
{
db.Entities.Remove(entity);
}
db.Entities.Add(newEntity);
await db.SaveChangesAsync();
Just remember to remove the PK.
var child4 = Tools.CloneJson(deepNestedElement);
child4.id = 0;
child3.Add(child4);
public static class Tools
{
public static JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
ObjectCreationHandling = ObjectCreationHandling.Replace,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
public static string JSerialize<T>(T source) {
return JsonConvert.SerializeObject(source, Formatting.Indented, jsonSettings);
}
public static T JDeserialize<T>(string source) {
return JsonConvert.DeserializeObject<T>(source, jsonSettings);
}
public static T CloneJson<T>(this T source)
{
return CloneJson<T, T>(source);
}
public static TOut CloneJson<TIn, TOut>(TIn source)
{
if (Object.ReferenceEquals(source, null))
return default(TOut);
return JDeserialize<TOut>(JSerialize(source));
}
}
Consider to use https://github.com/WahidBitar/EF-Core-Simple-Graph-Update.
It works well for me.
The library is simple, practically has only one extension method
T InsertUpdateOrDeleteGraph<T>(this DbContext context,
T newEntity, T existingEntity)
https://github.com/WahidBitar/EF-Core-Simple-Graph-Update/blob/master/src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs#L34
Compare to most of the answers to this question, it is generic (doesn’t use hardcoded table names, can be used for different models),
and includes unit tests for different model changes.
The author promptly responses to reported issues.
Refer below code snippet from one of my projects where I implemented the same thing. It will make save data if new entry, updates if existing and delete if record is not available in the posting json.
Json Data to help you understand the schema:
{
"groupId": 1,
"groupName": "Group 1",
"sortOrder": 1,
"filterNames": [
{
"filterId": 1,
"filterName1": "Name11111",
"sortOrder": 10,
"groupId": 1
} ,
{
"filterId": 1006,
"filterName1": "Name Changed 1",
"sortOrder": 10,
"groupId": 1
} ,
{
"filterId": 1007,
"filterName1": "New Filter 1",
"sortOrder": 10,
"groupId": 1
} ,
{
"filterId": 2,
"filterName1": "Name 2 Changed",
"sortOrder": 10,
"groupId": 1
}
]
}
public async Task<int> UpdateFilter(FilterGroup filterGroup)
{
var Ids = from f in filterGroup.FilterNames select f.FilterId;
var toBeDeleted = dbContext.FilterNames.Where(x => x.GroupId == filterGroup.GroupId
&& !Ids.Contains(x.FilterId)).ToList();
foreach(var item in toBeDeleted)
{
dbContext.FilterNames.Remove(item);
}
await dbContext.SaveChangesAsync();
dbContext.FilterGroups.Attach(filterGroup);
dbContext.Entry(filterGroup).State = EntityState.Modified;
for(int i=0;i<filterGroup.FilterNames.Count();i++)
{
if (filterGroup.FilterNames.ElementAt(i).FilterId != 0)
{
dbContext.Entry(filterGroup.FilterNames.ElementAt(i)).State = EntityState.Modified;
}
}
return await dbContext.SaveChangesAsync();
}

How can I log all entities change, during .SaveChanges() using EF code first?

I'm using EF code first. I'm using a base Repository for all my repositories and an IUnitofWork that inject to the repositories, too:
public interface IUnitOfWork : IDisposable
{
IDbSet<TEntity> Set<TEntity>() where TEntity : class;
int SaveChanges();
}
public class BaseRepository<T> where T : class
{
protected readonly DbContext _dbContext;
protected readonly IDbSet<T> _dbSet;
public BaseRepository(IUnitOfWork uow)
{
_dbContext = (DbContext)uow;
_dbSet = uow.Set<T>();
}
//other methods
}
e.g my OrderRepository is like this:
class OrderRepository: BaseRepository<Order>
{
IUnitOfWork _uow;
IDbSet<Order> _order;
public OrderRepository(IUnitOfWork uow)
: base(uow)
{
_uow = uow;
_order = _uow.Set<Order>();
}
//other methods
}
And I use it in this way:
public void Save(Order order)
{
using (IUnitOfWork uow = new MyDBContext())
{
OrderRepository repository = new OrderRepository(uow);
try
{
repository.ApplyChanges<Order>(order);
uow.SaveChanges();
}
}
}
Is there any way to log change histories of all entities(include their navigation properties) during .SaveChanges()? I want to log original values(before save occurs) and changed values(after save occurs).
You can get the before and after values for all changed entities by going through DbContext.ChangeTracker. Unfortunately the API is a little verbose:
var changeInfo = context.ChangeTracker.Entries()
.Where (t => t.State == EntityState.Modified)
.Select (t => new {
Original = t.OriginalValues.PropertyNames.ToDictionary (pn => pn, pn => t.OriginalValues[pn]),
Current = t.CurrentValues.PropertyNames.ToDictionary (pn => pn, pn => t.CurrentValues[pn]),
});
You can modify that to include things like the type of the entity if you need that for your logging. There is also a ToObject() method on the DbPropertyValues (the type of OriginalValues and CurrentValues) you could call if you already have a way to log whole objects, although the objects returned from that method will not have their navigation properties populated.
You can also modify that code to get all entities in the context by taking out the Where clause, if that makes more sense given your requirements.
I have overridded the default SaveChanges method to log changes for add/update/delete in entity. Though it does not cover navigation property changes.
Based on this article: Using entity framework for auditing
public int SaveChanges(string userId)
{
int objectsCount;
List<DbEntityEntry> newEntities = new List<DbEntityEntry>();
// Get all Added/Deleted/Modified entities (not Unmodified or Detached)
foreach (var entry in this.ChangeTracker.Entries().Where
(x => (x.State == System.Data.EntityState.Added) ||
(x.State == System.Data.EntityState.Deleted) ||
(x.State == System.Data.EntityState.Modified)))
{
if (entry.State == System.Data.EntityState.Added)
{
newEntities.Add(entry);
}
else
{
// For each changed record, get the audit record entries and add them
foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId))
{
this.AuditLogs.Add(changeDescription);
}
}
}
// Default save changes call to actually save changes to the database
objectsCount = base.SaveChanges();
// We don't have recordId for insert statements that's why we need to call this method again.
foreach (var entry in newEntities)
{
// For each changed record, get the audit record entries and add them
foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId, true))
{
this.AuditLogs.Add(changeDescription);
}
// TODO: Think about performance here. We are calling db twice for one insertion.
objectsCount += base.SaveChanges();
}
return objectsCount;
}
#endregion
#region Helper Methods
/// <summary>
/// Helper method to create record description for Audit table based on operation done on dbEntity
/// - Insert, Delete, Update
/// </summary>
/// <param name="dbEntity"></param>
/// <param name="userId"></param>
/// <returns></returns>
private List<AuditLog> GetAuditRecordsForEntity(DbEntityEntry dbEntity, string userId, bool insertSpecial = false)
{
List<AuditLog> changesCollection = new List<AuditLog>();
DateTime changeTime = DateTime.Now;
// Get Entity Type Name.
string tableName1 = dbEntity.GetTableName();
// http://stackoverflow.com/questions/2281972/how-to-get-a-list-of-properties-with-a-given-attribute
// Get primary key value (If we have more than one key column, this will need to be adjusted)
string primaryKeyName = dbEntity.GetAuditRecordKeyName();
int primaryKeyId = 0;
object primaryKeyValue;
if (dbEntity.State == System.Data.EntityState.Added || insertSpecial)
{
primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName, true);
if(primaryKeyValue != null)
{
Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
}
// For Inserts, just add the whole record
// If the dbEntity implements IDescribableEntity,
// use the description from Describe(), otherwise use ToString()
changesCollection.Add(new AuditLog()
{
UserId = userId,
EventDate = changeTime,
EventType = ModelConstants.UPDATE_TYPE_ADD,
TableName = tableName1,
RecordId = primaryKeyId, // Again, adjust this if you have a multi-column key
ColumnName = "ALL", // To show all column names have been changed
NewValue = (dbEntity.CurrentValues.ToObject() is IAuditableEntity) ?
(dbEntity.CurrentValues.ToObject() as IAuditableEntity).Describe() :
dbEntity.CurrentValues.ToObject().ToString()
}
);
}
else if (dbEntity.State == System.Data.EntityState.Deleted)
{
primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);
if (primaryKeyValue != null)
{
Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
}
// With deletes use whole record and get description from Describe() or ToString()
changesCollection.Add(new AuditLog()
{
UserId = userId,
EventDate = changeTime,
EventType = ModelConstants.UPDATE_TYPE_DELETE,
TableName = tableName1,
RecordId = primaryKeyId,
ColumnName = "ALL",
OriginalValue = (dbEntity.OriginalValues.ToObject() is IAuditableEntity) ?
(dbEntity.OriginalValues.ToObject() as IAuditableEntity).Describe() :
dbEntity.OriginalValues.ToObject().ToString()
});
}
else if (dbEntity.State == System.Data.EntityState.Modified)
{
primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);
if (primaryKeyValue != null)
{
Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
}
foreach (string propertyName in dbEntity.OriginalValues.PropertyNames)
{
// For updates, we only want to capture the columns that actually changed
if (!object.Equals(dbEntity.OriginalValues.GetValue<object>(propertyName),
dbEntity.CurrentValues.GetValue<object>(propertyName)))
{
changesCollection.Add(new AuditLog()
{
UserId = userId,
EventDate = changeTime,
EventType = ModelConstants.UPDATE_TYPE_MODIFY,
TableName = tableName1,
RecordId = primaryKeyId,
ColumnName = propertyName,
OriginalValue = dbEntity.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntity.OriginalValues.GetValue<object>(propertyName).ToString(),
NewValue = dbEntity.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntity.CurrentValues.GetValue<object>(propertyName).ToString()
}
);
}
}
}
// Otherwise, don't do anything, we don't care about Unchanged or Detached entities
return changesCollection;
}
you have scared people away with the extra requirement
Include their navigation properties
This is simply a non trivial exercise.
And if this is important, you should manage/track changes across references with code.
this is a sample covering this topic
Undo changes in entity framework entities
There is a sample doing close top what you want here
undo changes
It can easily be converted to load before and after images elsewhere.
Given the ObjectState entry after DetectChanges is called, you can implement a simple entity by entity option. and per UOW. But the navigation / references version makes this very complex as you worded the requirement.
EDIT : How to access the changeList
public class Repository<TPoco>{
/....
public DbEntityEntry<T> Entry(T entity) { return Context.Entry(entity); }
public virtual IList<ChangePair> GetChanges(object poco) {
var changes = new List<ObjectPair>();
var thePoco = (TPoco) poco;
foreach (var propName in Entry(thePoco).CurrentValues.PropertyNames) {
var curr = Entry(thePoco).CurrentValues[propName];
var orig = Entry(thePoco).OriginalValues[propName];
if (curr != null && orig != null) {
if (curr.Equals(orig)) {
continue;
}
}
if (curr == null && orig == null) {
continue;
}
var aChangePair = new ChangePair {Key = propName, Current = curr, Original = orig};
changes.Add(aChangePair);
}
return changes;
}
///... partial repository shown
}
// FYI the simple return structure
public class ChangePair {
public string Key { get; set; }
public object Original { get; set; }
public object Current { get; set; }
}
DbContext has ChangeTracker property.
You can override .SaveChanges() in your context and log changes.
I don't think that entity framework can do it for you. Probably, you must detect changes directly in your model classes.
I've expanded on Steve's answer to provide a check for Changed, Added, and Deleted entities and print them in a sensible way.
(My use case is to ensure there are no unsaved changes before disposing of a DbContext instance, but this check could be done at any point)
/// <summary>Helper method that checks whether the DbContext had any unsaved changes before it was disposed.</summary>
private void CheckForUnsavedChanges(DbContext dbContext)
{
try
{
List<DbEntityEntry> changedEntityEntries = dbContext.ChangeTracker.Entries()
.Where(t => t.State != EntityState.Unchanged && t.State != EntityState.Detached).ToList();
if (!changedEntityEntries.Any())
return;
throw new Exception("Detected that there were unsaved changes made using a DbContext. This could be due to a missing call to `.SaveChanges()` or possibly " +
"some read-only operations that modified the returned entities (in which case you might wish to use `.AsNoTracking()` in your query). Changes:\n " +
String.Join("\n ", changedEntityEntries.Select(entry => $"{entry.Entity.GetType()} {entry.State}:\n " + String.Join("\n ",
entry.State == EntityState.Modified ? entry.CurrentValues.PropertyNames
// Only output properties whose values have changed (and hope they have a good ToString() implementation)
.Where(pn => entry.OriginalValues?[pn] != entry.CurrentValues[pn])
.Select(pn => $"{pn} ({entry.OriginalValues?[pn]} -> {entry.CurrentValues[pn]})") :
// Added or Deleted entities are output in their entirety
entry.State == EntityState.Added ? entry.CurrentValues.PropertyNames.Select(pn => $"{pn} = {entry.CurrentValues[pn]}") :
/* entry.State == EntityState.Deleted ? */ entry.CurrentValues.PropertyNames.Select(pn => $"{pn} = {entry.OriginalValues[pn]}")))));
}
catch (Exception ex)
{
_logger.Error("Issue encountered when checking for unsaved changes.", ex);
}
}

Categories

Resources