Getting all changes made to an object in the Entity Framework - c#

Is there a way to get all the changes made to a object in the Entity Framework before it saves all changes. The reason for this is that i want to create a log table in our clients database:
so...
Is there a way to get the current database values(old) and the new values(current) before changes are saved?
If not, how can i achieve this in a generic way, so all my View Models can inherit from this?(I am using the MVVM + M Structure)

You can use ObjectContext's ObjectStateManager,GetObjectStateEntry to get an object's ObjectStateEntry, which holds its original and current values in the OriginalValues and CurrentValues properties. You can get the names of the properties that changed using the GetModifiedProperties method.
You can write something like:
var myObjectState=myContext.ObjectStateManager.GetObjectStateEntry(myObject);
var modifiedProperties=myObjectState.GetModifiedProperties();
foreach(var propName in modifiedProperties)
{
Console.WriteLine("Property {0} changed from {1} to {2}",
propName,
myObjectState.OriginalValues[propName],
myObjectState.CurrentValues[propName]);
}

For EF5 upwards you can log your changes in the SaveChanges() method like this:
public override int SaveChanges()
{
var changes = from e in this.ChangeTracker.Entries()
where e.State != System.Data.EntityState.Unchanged
select e;
foreach (var change in changes)
{
if (change.State == System.Data.EntityState.Added)
{
// Log Added
}
else if (change.State == System.Data.EntityState.Modified)
{
// Log Modified
var item = change.Cast<IEntity>().Entity;
var originalValues = this.Entry(item).OriginalValues;
var currentValues = this.Entry(item).CurrentValues;
foreach (string propertyName in originalValues.PropertyNames)
{
var original = originalValues[propertyName];
var current = currentValues[propertyName];
if (!Equals(original, current))
{
// log propertyName: original --> current
}
}
}
else if (change.State == System.Data.EntityState.Deleted)
{
// log deleted
}
}
// don't forget to save
base.SaveChanges();
}

I use this extension function that provides details on the entity being changed, the old and new values, the datatype, and the entity key.
This is tested with EF 6.1 using ObjectContext and uses log4net for output.
/// <summary>
/// dump changes in the context to the debug log
/// <para>Debug logging must be turned on using log4net</para>
/// </summary>
/// <param name="context">The context to dump the changes for</param>
public static void DumpChanges(this ObjectContext context)
{
context.DetectChanges();
// Output any added entries
foreach (var added in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added))
{
Log.DebugFormat("{0}:{1} {2} {3}",
added.State,
added.Entity.GetType().FullName,
added.Entity.ToString(),
string.Join(",",
added.CurrentValues.GetValue(1),
added.CurrentValues.GetValue(2))
);
}
foreach (var modified in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
{
// Put original field values into dictionary
var originalValues = new Dictionary<string,int>();
for (var i = 0; i < modified.OriginalValues.FieldCount; ++i)
{
originalValues.Add(modified.OriginalValues.GetName(i), i);
}
// Output each of the changed properties.
foreach (var entry in modified.GetModifiedProperties())
{
var originalIdx = originalValues[entry];
Log.DebugFormat("{6} = {0}.{4} [{7}][{2}] [{1}] --> [{3}] Rel:{5}",
modified.Entity.GetType(),
modified.OriginalValues.GetValue(originalIdx),
modified.OriginalValues.GetFieldType(originalIdx),
modified.CurrentValues.GetValue(originalIdx),
modified.OriginalValues.GetName(originalIdx),
modified.IsRelationship,
modified.State,
string.Join(",",
modified.EntityKey.EntityKeyValues
.Select(v => string.Join(" = ", v.Key, v.Value))
)
);
}
}
// Output any deleted entries
foreach (var deleted in context.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted))
{
Log.DebugFormat("{1} {0} {2}",
deleted.Entity.GetType().FullName,
deleted.State,
string.Join(",",
deleted.CurrentValues.GetValue(1),
deleted.CurrentValues.GetValue(2))
);
}
}

Use the IsModified field of each property, which is accessible by Context.Entry(Entity).Properties.
In this example, the modified entries are listed as a Tuple of the original and current values, indexed by name. Use any conversion that is required to build the audit log.
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
//...
// gets somewhere in the scope
DbContext Context;
// Some entity that has been modified, but not saved and is being tracked by Context
object Entity;
//...
Dictionary<string, System.Tuple<object, object>> modified =
Context.Entry(Entity)
.Properties.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name,
p => new System.Tuple<object,object>(p.OriginalValue, p.CurrentValue));
//...
Uses Entity Framework Core 3.1. Try it for EF 6.4, but it may not work.

Related

Replace property values in a class from List<Dictionary> values

I have a method that takes a List<Dictionary<string,object>> as a parameter. The plan is to use that parameter, but only update the values held in a particular class. Here is the (partially written) method
public async Task<Errors> UpdatePageForProject(Guid projectId, List<Dictionary<string, object>> data)
{
if (!IsValidUserIdForProject(projectId))
return new Errors { ErrorMessage = "Project does not exist", Success = false };
if (data.Count == 0)
return new Errors { ErrorMessage = "No data passed to change", Success = false };
var page = await _context.FlowPages.FirstOrDefaultAsync(t => t.ProjectId == projectId);
foreach (var d in data)
{
}
return new Errors { Success = true };
}
My original plan is to take each dictionary, check if the key and the property in page match and then alter the value (so I can pass in 1 dictionary or 8 dictionaries in the list and then alter page to save back to my entity database).
I'd rather not use reflection due to the speed hit (though C#9 is really fast, I'd still rather not use it), but I'm not sure how else this can be done. I did consider using AutoMapper to do this, but for now would rather not (it's a PoC, so it is possibly overkill)
If you want to do this without Reflection (which I agree is a good idea, not just for performance reasons) then you could use a "map" or lookup table with actions for each property.
var map = new Dictionary<string,Action<Page,object>>()
{
{ "Title", (p,o) => p.Title = (string)o },
{ "Header", (p,o) => p.Field1 = (string)o },
{ "DOB", (p,o) => p.DateOfBirth = (DateTime)o }
};
You can then iterate over your list of dictionaries and use the map to execute actions that update the page.
foreach (var dictionary in data)
{
foreach (entry in dictionary)
{
var action = map[entry.Key];
action(page, entry.Value);
}
}

Override SaveChangesAsync Entity Framework 6 to log modified property values

I'm developing an ASP.NET Core project where in SaveChanges, I need to log the updated values
According to this I tried to override SaveChangesAsync but I get this error :
Unable to cast object of type 'MyProject.ApplicationDbContext' to type 'System.Data.Entity.Infrastructure.IObjectContextAdapter'.
Code:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ChangeTracker.DetectChanges();
ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;
List<ObjectStateEntry> objectStateEntryList = ctx.ObjectStateManager
.GetObjectStateEntries((System.Data.Entity.EntityState)(EntityState.Added | EntityState.Modified | EntityState.Deleted))
.ToList();
foreach (ObjectStateEntry entry in objectStateEntryList)
{
if (!entry.IsRelationship)
{
switch (entry.State)
{
case (System.Data.Entity.EntityState)EntityState.Added:
// write log...
break;
case (System.Data.Entity.EntityState)EntityState.Deleted:
// write log...
break;
case (System.Data.Entity.EntityState)EntityState.Modified:
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) // probably not necessary
{
var a = string.Format("Entry: {0} Original :{1} New: {2}",
entry.Entity.GetType().Name,
oldValue, newValue);
}
}
break;
}
}
}
return base.SaveChangesAsync();
}
Also I tried to compose the log string in the Action method but I get the same error.
public async Task<IActionResult> Edit(MyModel model)
{
...
// _context is ApplicationDbContext()
var myObjectState = _context.ObjectStateManager.GetObjectStateEntry(model);
var modifiedProperties = myObjectState.GetModifiedProperties();
foreach (var propName in modifiedProperties)
{
Console.WriteLine("Property {0} changed from {1} to {2}",
propName,
myObjectState.OriginalValues[propName],
myObjectState.CurrentValues[propName]);
}
}
I'm using EF 6.
I need to log the property name, old value and new value. I can't get why I'm getting that error, what am I doing wrong?
what am I doing wrong?
Using an example from EF 4.1? :)
You can utilize the ChangeTracker in EF 6 to accomplish what you are looking for. I would recommend setting up a separate DbContext for your logging containing just the logging table and FKs as needed:
Within your SaveChanges/SaveChangesAsync overrides:
var updatedEntities = ChangeTracker.Entries()
.Where(x => x.State == EntityState.Modified);
var insertedEntities = ChangeTracker.Entries()
.Where(x => x.State == EntityState.Added);
From here you can access the original and modified property values through OriginalValues and CurrentValues respectively.
** Update **
When reporting on changes the typical easy way is to use ToObject() on the OriginalValues and CurrentValues then serialize to something like Json to capture the before and after state of the object in question. However this will display all modified and unmodified values.
To iterate through the values to find actual changes is a bit more involved:
foreach(var entity in updatedEntities)
{
foreach (var propertyName in entity.OriginalValues.PropertyNames)
{
if (!object.Equals(entity.OriginalValues.GetValue<object>(propertyName),
entity.CurrentValues.GetValue<object>(propertyName)))
{
var columnName = propertyName;
var originalValue = entity.OriginalValues.GetValue<object>(propertyName)?.ToString() ?? "#null";
var updatedValue = entity.CurrentValues.GetValue<object>(propertyName)?.ToString() ?? "#null";
var message = $"The prop: {columnName} has been changed from {originalValue} to {updatedValue}";
}
}
}

How to add a partial entry into relational DB with entity core

The Program:
My program reads from a file and then stores the values in a relational db.
The Models
ClientDetails, ClientVehicle, ClientOrder
The Relationship:
1 client can have many vehicles
1 vehicle can have many orders
The Problem:
The program does not support duplicate data.
The Struggle
I need to be able to not add a client if the client already exists but still have the client linked to the vehicle when storing the vehicle (Entity core handles the foreign keys for me, but when simply skipping half the record I get foreign key constraints). This same process needs to work if a vehicle with the same registration plate gets added.
The Struggle Continues
To top off this problem I also need to be able to do these checks on the file itself as I only save the context changes into the db at the end of the whole process so if the file contains 2 records with the same user but different vehicles (1 client can have many vehicles) then I need to only add the user once.
The Hope
I am hoping that somebody can tell me if there is a built in entity core method that can sort this out for me. I have seen some entity framework things that I estimate would fix this but I can't find the entity core equivalent. for example _context.AddIfNotExists. Or something along the lines of _context.AddOrUpdate(entity, k = k.id = record[0]). If a post like this already exist please direct me as I could not find it myself.
NOTE
When a duplicate client is found the rest of the entry should still happen as this is an "order", its only the client that needs to be skipped (or vehicle in the event of the same registration plate).
The Code Snippet In Question
public void LoadCsv(string path)
{
var lines = File.ReadAllLines(path).Where(x => !string.IsNullOrWhiteSpace(x));
_now = DateTime.Now;
var count = 0;
foreach (var line in lines)
{
if (count > 0)
{
var record = line.Split('|').ToList();
var entityClientDetails = AddClientDetails(record, _now);
var entityClientVehicle = AddClientVehicle(record, _now);
var entityClientOrder = AddClientOrder(record, _now);
entityClientDetails.ClientVehicles.Add(entityClientVehicle);
entityClientVehicle.ClientOrders.Add(entityClientOrder);
_context.Add(entityClientDetails);
}
count++;
}
_context.SaveChanges();
}
Try this
public void LoadCsv(string path)
{
var clients = _context.ClientDetails.AsQueryable();
var lines = File.ReadAllLines(path).Where(x => !string.IsNullOrWhiteSpace(x));
_now = DateTime.Now;
var count = 0;
foreach (var line in lines)
{
if (count > 0)
{
var record = line.Split('|').ToList();
var entityClientDetails = AddClientDetails(record, _now);
var entityClientVehicle = AddClientVehicle(record, _now);
var entityClientOrder = AddClientOrder(record, _now);
var clientExist = clients.Where(x => x.RegistrationPlate == entityClientDetails.RegistrationPlate).FirstOrDefault();
entityClientVehicle.ClientOrders.Add(entityClientOrder);
if (clientExist != null)
{
entityClientVehicle.ClientDetailID = clientExist.Id;
_context.Add(entityClientVehicle);
}
else
{
entityClientDetails.ClientVehicles.Add(entityClientVehicle);
_context.Add(entityClientDetails);
}
}
count++;
}
_context.SaveChanges();
}

How to refactor Relational() to NetCore3.0

I don't know how to choose the property after the revision.
private void ApplySnakeCaseNames(ModelBuilder modelBuilder)
{
var mapper = new NpgsqlSnakeCaseNameTranslator();
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
// modify column names
foreach (var property in entity.GetProperties())
{
property.Relational().ColumnName = mapper.TranslateMemberName(property.Relational().ColumnName);
}
// modify table name
entity.Relational().TableName = mapper.TranslateMemberName(entity.Relational().TableName);
// move asp_net tables into schema 'identity'
if (entity.Relational().TableName.StartsWith("asp_net_"))
{
entity.Relational().TableName = entity.Relational().TableName.Replace("asp_net_", string.Empty);
entity.Relational().Schema = "identity";
}
}
}
I expect the output of keep the original.
This is breaking change in Entity Framework Core 3
IProperty.Relational().ColumnName -> IProperty.GetColumnName()
Reference: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#id=%22provider-specific-metadata-api-changes%22
For IMutableEntityType and IMutableProperty, they expose the method instead of properties to change the Name.
Try something like below:
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
// modify column names
foreach (var property in entity.GetProperties())
{
property.SetColumnName(mapper.TranslateMemberName(property.GetColumnName()));
}
// modify table name
entity.SetTableName(mapper.TranslateMemberName(entity.GetTableName()));
// move asp_net tables into schema 'identity'
if (entity.GetTableName().StartsWith("asp_net_"))
{
entity.SetTableName(entity.GetTableName().Replace("asp_net_", string.Empty));
entity.SetSchema("identity");
}
}

Code first track database changes [duplicate]

Is there a way to get all the changes made to a object in the Entity Framework before it saves all changes. The reason for this is that i want to create a log table in our clients database:
so...
Is there a way to get the current database values(old) and the new values(current) before changes are saved?
If not, how can i achieve this in a generic way, so all my View Models can inherit from this?(I am using the MVVM + M Structure)
You can use ObjectContext's ObjectStateManager,GetObjectStateEntry to get an object's ObjectStateEntry, which holds its original and current values in the OriginalValues and CurrentValues properties. You can get the names of the properties that changed using the GetModifiedProperties method.
You can write something like:
var myObjectState=myContext.ObjectStateManager.GetObjectStateEntry(myObject);
var modifiedProperties=myObjectState.GetModifiedProperties();
foreach(var propName in modifiedProperties)
{
Console.WriteLine("Property {0} changed from {1} to {2}",
propName,
myObjectState.OriginalValues[propName],
myObjectState.CurrentValues[propName]);
}
For EF5 upwards you can log your changes in the SaveChanges() method like this:
public override int SaveChanges()
{
var changes = from e in this.ChangeTracker.Entries()
where e.State != System.Data.EntityState.Unchanged
select e;
foreach (var change in changes)
{
if (change.State == System.Data.EntityState.Added)
{
// Log Added
}
else if (change.State == System.Data.EntityState.Modified)
{
// Log Modified
var item = change.Cast<IEntity>().Entity;
var originalValues = this.Entry(item).OriginalValues;
var currentValues = this.Entry(item).CurrentValues;
foreach (string propertyName in originalValues.PropertyNames)
{
var original = originalValues[propertyName];
var current = currentValues[propertyName];
if (!Equals(original, current))
{
// log propertyName: original --> current
}
}
}
else if (change.State == System.Data.EntityState.Deleted)
{
// log deleted
}
}
// don't forget to save
base.SaveChanges();
}
I use this extension function that provides details on the entity being changed, the old and new values, the datatype, and the entity key.
This is tested with EF 6.1 using ObjectContext and uses log4net for output.
/// <summary>
/// dump changes in the context to the debug log
/// <para>Debug logging must be turned on using log4net</para>
/// </summary>
/// <param name="context">The context to dump the changes for</param>
public static void DumpChanges(this ObjectContext context)
{
context.DetectChanges();
// Output any added entries
foreach (var added in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added))
{
Log.DebugFormat("{0}:{1} {2} {3}",
added.State,
added.Entity.GetType().FullName,
added.Entity.ToString(),
string.Join(",",
added.CurrentValues.GetValue(1),
added.CurrentValues.GetValue(2))
);
}
foreach (var modified in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
{
// Put original field values into dictionary
var originalValues = new Dictionary<string,int>();
for (var i = 0; i < modified.OriginalValues.FieldCount; ++i)
{
originalValues.Add(modified.OriginalValues.GetName(i), i);
}
// Output each of the changed properties.
foreach (var entry in modified.GetModifiedProperties())
{
var originalIdx = originalValues[entry];
Log.DebugFormat("{6} = {0}.{4} [{7}][{2}] [{1}] --> [{3}] Rel:{5}",
modified.Entity.GetType(),
modified.OriginalValues.GetValue(originalIdx),
modified.OriginalValues.GetFieldType(originalIdx),
modified.CurrentValues.GetValue(originalIdx),
modified.OriginalValues.GetName(originalIdx),
modified.IsRelationship,
modified.State,
string.Join(",",
modified.EntityKey.EntityKeyValues
.Select(v => string.Join(" = ", v.Key, v.Value))
)
);
}
}
// Output any deleted entries
foreach (var deleted in context.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted))
{
Log.DebugFormat("{1} {0} {2}",
deleted.Entity.GetType().FullName,
deleted.State,
string.Join(",",
deleted.CurrentValues.GetValue(1),
deleted.CurrentValues.GetValue(2))
);
}
}
Use the IsModified field of each property, which is accessible by Context.Entry(Entity).Properties.
In this example, the modified entries are listed as a Tuple of the original and current values, indexed by name. Use any conversion that is required to build the audit log.
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
//...
// gets somewhere in the scope
DbContext Context;
// Some entity that has been modified, but not saved and is being tracked by Context
object Entity;
//...
Dictionary<string, System.Tuple<object, object>> modified =
Context.Entry(Entity)
.Properties.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name,
p => new System.Tuple<object,object>(p.OriginalValue, p.CurrentValue));
//...
Uses Entity Framework Core 3.1. Try it for EF 6.4, but it may not work.

Categories