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}";
}
}
}
Question: having a generic DbSet<T>, how can I know if the class T has an identity key?
This is my case:
public static string SqlCodeGenerator<T>(DbContext context) where T : class
{
var query = string.Emtpy;
var set = context.Set<T>();
if (set.HasIdentity())
query += "SET IDENTITY_INSERT ON;\n";
// Continue building query...
}
I don't need to know which property, only if it has or has not (although gives extra points to the answer).
More flexible approach, not directly linked to SQL Server, but it covers Identity columns. At least those properties which are marked with [DatabaseGenerated(DatabaseGeneratedOption.Identity)] attribute
foreach (var key in dbContext.Model.FindEntityType(typeof(Entity)).GetKeys())
{
foreach (var property in key.Properties)
{
if (property.ValueGenerated == Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAdd)
{
Console.WriteLine("gotcha!");
}
}
}
Solved both with some creativity and Linq:
var efEntity = context.Model.FindEntityType(typeof(T));
var efProperties = efEntity.GetProperties();
var hasIdentity = efProperties.Any(p => (SqlServerValueGenerationStrategy)
p.FindAnnotation("SqlServer:ValueGenerationStrategy").Value
== SqlServerValueGenerationStrategy.IdentityColumn);
var identityProperty = efProperties.FirstOrDefault(p => (SqlServerValueGenerationStrategy)
p.FindAnnotation("SqlServer:ValueGenerationStrategy").Value
== SqlServerValueGenerationStrategy.IdentityColumn);
With libraries:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Linq;
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");
}
}
I have the following method to save order properties on a purchase order:
public void SetOrderProperty(string orderPropertyName, string value)
{
PurchaseOrder purchaseOrder = TransactionLibrary.GetBasket().PurchaseOrder;
OrderProperty orderProperty = purchaseOrder.OrderProperties.FirstOrDefault(x => x.Key == orderPropertyName);
if (orderProperty != null)
{
orderProperty.Value = value;
orderProperty.Save();
}
else
{
OrderProperty op = new OrderProperty
{
Key = orderPropertyName,
Value = value,
Order = purchaseOrder
};
op.Save();
}
purchaseOrder.Save();
TransactionLibrary.ExecuteBasketPipeline();
}
When I save a value using this I can see it appear against the order in the uCommerce_OrderProperty table.
However, with some properties, when I try to read them back out they are missing:
public string GetOrderProperty(string orderPropertyName)
{
PurchaseOrder purchaseOrder;
using (new CacheDisabler())
{
purchaseOrder = TransactionLibrary.GetBasket().PurchaseOrder;
}
OrderProperty orderProperty = purchaseOrder.OrderProperties.FirstOrDefault(x => x.Key == orderPropertyName);
if (orderProperty != null)
{
return orderProperty.Value;
}
return string.Empty;
}
I have also tried this code from the uCommerce site:
public string GetOrderProperty(string orderPropertyName)
{
PurchaseOrder purchaseOrder = SiteContext.Current.OrderContext.GetBasket().PurchaseOrder;
return purchaseOrder[orderPropertyName];
}
If I inspect purchaseOrder I can see the OrderProperties are missing. I have 7 properties at any one time but purchaseOrder only ever seems to have a max of 5 even though there is 7 in the table.
These are Order Properties and not Order Line Properties. Can anyone give me any pointers as to why I am seeing this behaviour?
EDIT
This line does get the value I am looking for:
OrderProperty op = OrderProperty.FirstOrDefault(x => x.Order.OrderId == purchaseOrder.OrderId && x.Key == orderPropertyName);
Even when this line (called the line after) returns null:
OrderProperty orderProperty = purchaseOrder.OrderProperties.FirstOrDefault(x => x.Key == orderPropertyName);
(Both are looking for the same Order Property)
Can anyone tell me why?
I have a comment, but I'm not allowed because of missing reputation.
Everything seems to be fine regarding your code. Can I persuade you to show the
uCommerce_OrderProperty table?
- I just want to check that the OrderLineId column is empty for you order properties.
You should be able to set and get it like this:
var property = order[orderPropertyName];
order[orderPropertyName] = "VALUE";
Regards
Mads
We also recommend that Ucommerce related question is posted at http://eureka.ucommerce.net/, the response time is often faster.
I'm trying to create an audit log of any changes at point of save using Entity Framework. So far I have it working fairly well, storing all changes made to each field using the code below:
foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{
// For updates, we only want to capture the columns that actually changed
if (!object.Equals(dbEntry.OriginalValues.GetValue<object>(propertyName), dbEntry.CurrentValues.GetValue<object>(propertyName)))
{
result.Add(new AuditLog()
{
UserID = UserId,
EventDateUTC = changeTime,
EventType = "M", // Modified
TableName = tableName,
RecordID = primaryKey.ToString(),
ColumnName = propertyName,
OriginalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
NewValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString()
});
}
}
The issue I'm facing is how to get values for any foreign keys that belong to this object. For example: I have a vehicle object that has relationships to a series of lookup tables, such as gearbox, model etc. If these values change the audit table will store the changed id, but I want to store the actual value.
Is there a way of getting the foreign key value in this situation?
Alrighty... this is an old question but I've spent the last while working this out because I had the exact same requirements. Maybe there is an easier way, but here's the code I used:
Your original code, slightly modified for my purposes (RecordID is always an int), and calling the new method to calculate the new value
foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{
// For updates, we only want to capture the columns that actually changed
if (!Equals(dbEntry.OriginalValues.GetValue<object>(propertyName), dbEntry.CurrentValues.GetValue<object>(propertyName)))
{
var newVal = getNewValueAsString(dbEntry, tableName, propertyName);
result.Add(new AuditLog
{
UserID = currentUser.ID,
Timestamp = changeTime,
EventType = EventType.Modified,
TableName = tableName,
RecordID = dbEntry.OriginalValues.GetValue<int>(keyName),
ColumnName = propertyName,
OriginalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
NewValue = newVal
}
);
}
}
A new attribute called "IsName"
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class IsNameAttribute : Attribute
{
}
Marking the "name" property of foreign key models with the IsName attribute (note the code will default to a property called "Name" if it doesn't find one)
[Required]
[IsName]
public string Name { get; set; }
And the heavy lifting code
private string getNewValueAsString(DbEntityEntry dbEntry, string tableName, string propertyName)
{
var fkVal = getForeignKeyValue(tableName, propertyName, dbEntry.CurrentValues.GetValue<object>(propertyName));
return fkVal != null ? fkVal.ToString()
: (dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null
: dbEntry.CurrentValues.GetValue<object>(propertyName).ToString());
}
private object getForeignKeyValue(string tableName, string propertyName, object foreignKeyID)
{
// if this property is part of a foreign key, we need to instead look that up and store the value of the
// foreign key
// first get all the foreign keys in the system
var workspace = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
var items = workspace.GetItems<AssociationType>(DataSpace.CSpace);
if (items == null) return null;
var fk = items.Where(a => a.IsForeignKey).ToList();
// now we look into the FK attributes and find that the "To Role" is out current table, and the
// "To Property" is out current property. The underscore is a bit of an assumption that the foreign
// key name built by EF will be ENTITY_BLAH_BLAH
var thisFk = fk.Where(x => x.ReferentialConstraints[0].ToRole.Name.StartsWith(tableName + "_"))
.FirstOrDefault(x => x.ReferentialConstraints[0].ToProperties[0].Name == propertyName);
// if fkname has no results, this is not a foreign key and we are done
if (thisFk == null) return null;
// Now that we know the foriegn key, we need to lookup the Name value in the other table
// find the assembly
var assembly = Assembly.GetCallingAssembly();
// build the type for the foreign key entity
// e.g. if the current entity is Task, and the property is StatusID, we are
// getting the "TaskStatus" type with reflection
// "User" class is an object in the Models namespace - you could just hardcode the string if you want
var foreignKeyType = assembly.GetType(typeof(User).Namespace + "." +
thisFk.ReferentialConstraints[0].FromRole.GetEntityType().Name);
// get the DbSet, same as: "(new DBContext()).EntityName"
var fkSet = Set(foreignKeyType);
// and find the row in that table
var fkItem = fkSet.Find(foreignKeyID);
// find the first column marked with the "IsName" attribute, otherwise default to "Name"
var nameColProperty = foreignKeyType.GetProperties()
.FirstOrDefault(p => p.GetCustomAttributes(typeof(IsNameAttribute), false).Any());
string nameCol = "Name";
if (nameColProperty != null) nameCol = nameColProperty.Name;
var nameColProperty2 = fkItem.GetType().GetProperty(nameCol);
if (nameColProperty2 == null) return null;
// get the value
var fkValue = nameColProperty2.GetValue(fkItem, null);
// and now, my brain hurts
return fkValue;
}
This solution is based on #JamesR's answer.
My goal was to make the code more generic so it could be used for multiple foreign keys connecting to different tables.
Improvements worth noting:
I moved the code that gets the list of foreign keys outside of the propertyName foreach loop. Since the list of FKs doesn't change based on the specific property, there is no reason to retrieve a new list every time. If there are many FKs in the system, this can take a while, so you don't want to repeat the process unnecessarily.
Instead of hard-coding a specific class type like GetType(typeof(User), I retrieved the foreign key table name from the FK using:
string lookUpTableName = thisFk.ReferentialConstraints[0].FromRole.Name;
Then, although the referenced FK property name will usually be ID, since it can vary, I retrieved the FK property name as well:
string lookUpPropertyName = thisFk.ReferentialConstraints[0].FromProperties[0].Name;
I then used ObjectContext.ExecuteStoreQuery to dynamically plug in the table and column name and retrieve the foreign key text value.
If a property is a FK, I get the FK text value for both the original a new value.
Complete code:
First, get a list of all the foreign keys in the system.
IObjectContextAdapter contextAdapter = ((IObjectContextAdapter)this);
MetadataWorkspace workspace = contextAdapter.ObjectContext.MetadataWorkspace;
var items = workspace.GetItems<AssociationType>(DataSpace.CSpace);
List<AssociationType> FKList = items == null ? null
: items.Where(a => a.IsForeignKey).ToList();
Then, loop through the list of properties and replace the original and current values with the foreign key values when a FK exists.
foreach (string propertyName in entry.OriginalValues.PropertyNames)
{
var original = entry.OriginalValues.GetValue<object>(propertyName);
var current = entry.CurrentValues.GetValue<object>(propertyName);
if (FKList != null)
{
GetPossibleForeignKeyValues(tableName, propertyName, ref original, ref current,
FKList, contextAdapter);
}
if ((original == null && current != null) ||
(original != null && !original.Equals(current)))
{
result.Add(new AuditLog()
{
UserID = UserId,
EventDateUTC = changeTime,
EventType = "M", // Modified
TableName = tableName,
RecordID = primaryKey.ToString(),
ColumnName = propertyName,
OriginalValue = original != null ? original.ToString() : "NULL",
NewValue = current != null ? current.ToString() : "NULL"
});
}
}
Here is the actually foreign key finding code:
private void GetPossibleForeignKeyValues(string tableName, string propertyName,
ref object originalFKValue, ref object newFKValue,
List<AssociationType> FKList, IObjectContextAdapter contextAdapter)
{
// If this property is part of a foreign key, look up and set the FKValue to the text
// value of the foreign key. Otherwise, just leave the FKValue alone.
// Look into the FK attributes and find that the "To Role" is out current table,
// and the "To Property" is out current property.
AssociationType thisFk = FKList.FirstOrDefault(x =>
tableName.Contains(x.ReferentialConstraints[0].ToRole.Name)
&& propertyName.Contains(x.ReferentialConstraints[0].ToProperties[0].Name));
// If fkname has no results, this is not a foreign key and we are done.
if (thisFk != null)
{
// Now that we know the foriegn key, look up the Name value in the other table.
string lookUpTableName = thisFk.ReferentialConstraints[0].FromRole.Name;
string lookUpPropertyName = thisFk.ReferentialConstraints[0].FromProperties[0].Name;
//Assuming the FK column name is "Name".
//Use the idea in #JamesR's solution or some sort of LookUp table if it is not.
string commandText = BuildCommandText("Name", lookUpTableName, lookUpPropertyName);
originalFKValue = contextAdapter.ObjectContext
.ExecuteStoreQuery<string>(commandText, new SqlParameter("FKID", originalFKValue))
.FirstOrDefault() ?? originalFKValue;
newFKValue = contextAdapter.ObjectContext
.ExecuteStoreQuery<string>(commandText, new SqlParameter("FKID", newFKValue))
.FirstOrDefault() ?? originalFKValue;
}
}
This is the method I used to build the SQL CommandText:
private string BuildCommandText(string columnName, string lookUpTableName,
string lookUpPropertyName)
{
StringBuilder builder = new StringBuilder();
builder.Append("SELECT ");
builder.Append(columnName);
builder.Append(" FROM ");
builder.Append(lookUpTableName);
builder.Append(" WHERE ");
builder.Append(lookUpPropertyName);
builder.Append(" = #FKID");
//The result query will look something like:
//SELECT ColumnName FROM TableName WHERE PropertyName = #FKID
return builder.ToString();
}