How to log changed values to database on all action? - entity-framework

The controller above has a standard edit ActionResult. I simply find rows in a database by ID and update it. Before db.SaveChanges() there is log.Save() static function that saves all changes in model to separate tables in the database.It simply check old and new values from ChangeTracker.
The problem is, i want use log.Save() after db.SaveChanges(), not before, to be sure that data was really saved.
But after, in the ChangeTracker there aren't any changes so log.Save() doesn't have anything to save.
Controller:
[HttpPost]
public ActionResult edit(int id, MyModel model)
{
var hihi = db.MyModel.First(s => s.ID == model.ID);
hihi.col1 = model.col1;
hihi.col2 = model.col2;
...
log.Save(Log.ChangeType.Edit, db, id);
^ Here i save changes to log.
db.SaveChanges();
return RedirectToAction("Index");
}
Log Class:
public void Save(ChangeType changeType, DBContext parentContext, int id)
{
DBContext db = new DBContext();
foreach (System.Data.Entity.Infrastructure.DbEntityEntry ee in parentContext.ChangeTracker.Entries())
{
foreach (string column in ee.OriginalValues.PropertyNames)
{
string oldValue = ee.OriginalValues[column].ToString();
string newValue = ee.CurrentValues[column].ToString();
if (oldValue != newValue)
{
var model = new LogModel
{
Log_Time = DateTime.Now,
Log_Operator = User.Ope_ID,
Log_Table = ee.Entity.ToString().Replace("xxx.Models.", ""),
Log_Key = id,
Log_Column = column,
Log_Type = (int)changeType,
Log_OldValue = oldValue,
Log_NewValue = newValue
};
var log = db.Log.Add(model);
db.SaveChanges();
}
}
}
}
public enum ChangeType
{
Create = 1,
Delete = 2,
Edit = 3
}
... or maybe someone has another way to save all changes in a database to another table on all controller actions, so after the project release I can see what users do.
PS. I don't what user triggers.

SaveChanges in EF4 is virtual, so you can override it, add custom logging etc.

Why don't you use try{} catch{} within Log Class and change the return parameter from 'void' to 'bool'. This would return true if the db.SaveChanges() succeeds. Then within "ActionResult edit" use bool result = log.Save(Log.ChangeType.Edit, db, id); to retrieve if the log saved the changes, then use a simple if-sentence to validate if you can save all changes to db or not.

Related

Using ChangeTracker with EntityState.Modified

Given the following function the variable currentModel is already the modified model that I want to update and it might have some properties different from the ones in the database and this function correctly updates the modified values.
Now I want to track the changes made before the update, the problem is that the ChangeTracker is detecting all properties as modified even when only one is acctualy different from the original model.
Is there a way to use ChangeTracker while also updating the statement with EntityState.Modified (reference)?
Here is the function used:
public void SaveCustomer(Clients currentModel)
{
var objFromDbAct = _db.Clients.Local.FirstOrDefault(u => u.Recid == currentModel.Recid);
if (objFromDbAct != null) { _db.Entry(objFromDbAct).State = EntityState.Detached; }
_db.Entry(currentModel).State = EntityState.Modified;
_db.ChangeTracker.DetectChanges();
string trackChanges = _db.ChangeTracker.DebugView.LongView;
_db.SaveChanges();
}
Here is the return from ChangeTracker.DebugView.LongView (I have removed some fields to simplify, but the same applies to all of them. In this case only Zip was changed.
Clients {Recid: 6391} Modified
Recid: 6391 PK
Additional: '' Modified
Addr1: '12345 Somewhere' Modified
Addr2: '' Modified
Addr3: <null> Modified
Zip: '000002222' Modified
PortalUsers: <null>
You can use existing methods on DbEntityEntry to reload database values.
public void SaveCustomer(Clients currentModel)
{
var objFromDbAct = _db.Clients.Local.FirstOrDefault(u => u.Recid == currentModel.Recid);
if (objFromDbAct != null) { _db.Entry(objFromDbAct).State = EntityState.Detached; }
_db.Entry(currentModel).GetDatabaseValues();
_db.ChangeTracker.DetectChanges();
string trackChanges = _db.ChangeTracker.DebugView.LongView;
_db.SaveChanges();
}
Optionally, if you track only this exact entity, you may clear change tracker to simplify the code.
public void SaveCustomer(Clients currentModel)
{
_db.ChangeTracker.Clear();
_db.Entry(currentModel);.GetDatabaseValues();
_db.ChangeTracker.DetectChanges();
string trackChanges = _db.ChangeTracker.DebugView.LongView;
_db.SaveChanges();
}
UPDATE:
I have initially missed one additional method call as GetDatabaseValues method returns values from database, but not internally setting those anywhere. Also _db.Entry(value); is adding entity into the change tracking, but in detached state, thus changes are not detected.
public void SaveCustomer(Clients currentModel)
{
// clear tracked entries
_db.ChangeTracker.Clear();
// attach current model
var entry = _db.Attach(currentModel);
// get database values
var originalValues = entry.GetDatabaseValues();
// set database values as original values
entry.OriginalValues.SetValues(originalValues);
string trackChanges = _db.ChangeTracker.DebugView.LongView;
_db.SaveChanges();
}
Related links:
DbEntityEntry.GetDatabaseValues Method
DbPropertyValues.SetValues Method
ChangeTracker.Clear Method
It is possible to copy the entity's values from a other class instance as follows :
public void SaveCustomer(Client currentModel)
{
var objFromDbAct = _db.Clients.Local.FirstOrDefault(c => c.Recid == currentModel.Recid);
if (objFromDbAct == null)
{
_db.Update(currentModel);
}
else
{
var preEntity = _db.Entry(objFromDbAct);
preEntity.CurrentValues.SetValues(currentModel);
}
_db.ChangeTracker.DetectChanges();
string trackChanges = _db.ChangeTracker.DebugView.LongView;
_db.SaveChanges();
}
trackChanges value :
Client {Recid: 1} Modified
Recid: 1 PK
Addr1: 'Address 1'
Addr2: 'Address 2'
Zip: 'Modified' Modified Originally 'Zip'
And the SQL executed by SaveChanges:
UPDATE [Clients] SET [Zip] = #p0
WHERE [Recid] = #p1;

efcore change modified state to update entity is not working with sub data objects

I set up a Generic repository using this code for update
private void AttachIfNot(TEntity entityToActive)
{
if (_dbContext.Entry(entityToActive).State == EntityState.Detached)
{
_dbSet.Attach(entityToActive);
}
}
private void UpdateEntity(TEntity entityToUpdate)
{
AttachIfNot(entityToUpdate);
_dbContext.Entry(entityToUpdate).State = EntityState.Modified;
}
It just attach the entity and set the modified state to save.
But when I use efocre ownsone to map a value object,the update entity function is not working.
I found out that it only works when I set Valueobject to modified too.
_dbContext.Entry(entityToUpdate).State = EntityState.Modified;
_dbContext.Entry(entityToUpdate.Valueobject).State = EntityState.Modified;
But It is hard for me to specify all the value objects in a Generic Repository.
This is code also has problems with one to many or other relations.
The working way is like this:
Classroom classroom = new Classroom
{
Id = 1,
Name = "b",
Students = new List<Student>
{
new Student()
{
Name = "aa",
Id = 2
}
}
};
if (_defaultDbContext.Entry(classroom).State == EntityState.Detached)
{
_defaultDbContext.Classrooms.Attach(classroom);
foreach(var stu in classroom.Students)
{
_defaultDbContext.Students.Attach(stu);
}
}
_defaultDbContext.Entry(classroom).State = EntityState.Modified;
foreach (var stu in classroom.Students)
{
_defaultDbContext.Entry(stu).State = EntityState.Modified;
}
_defaultDbContext.SaveChanges();
I found out one way is get the entity form repo then update it using automapper:
targetEntity = repo.GetById(entityId);
automapper.map(souceEntity,targetEntity);
//or
automapper.map(souceDto,targetEntity);
_dbContext.Save();
The entity comes by query, so the change will be tracked.
But I have to configure the automapper with this entity map when I want to change entity
CreateMap<EntityType, EntityType>();
I think it's not the best solution. Is there a bettere way?
DbContext.Update would be fine to fix this problem.
see:
https://www.learnentityframeworkcore.com/dbcontext/change-tracker

EntityFramework not saving null and false value

I'm trying to update a record using EntityFramework 6 by attaching a disconnected entity. I want to update a single boolean field to false but it doesn't work. I have used sql server profiler and EF doesn't generate an update statement when calling SaveChanges on the context. But if i set the value to true, it works. Example:
This doesn't work:
private void UpdateUser()
{
var user = new User { ID = 5 };
this.Context.Users.Attach(user);
user.Locked = false;
this.Context.SaveChanges();
}
This works:
private void UpdateUser()
{
var user = new User { ID = 5 };
this.Context.Users.Attach(user);
user.Locked = true;
this.Context.SaveChanges();
}
I can fix by marking the property as modified like this:
this.Context.Entry(user).Property(e => e.Locked).IsModified = true;
But don't understand why does it work if the value is true and not if the value is false. There is a similar issue when trying to set a field to null.
There's probably something wrong with the entity data model or database but can't figure out what.
Default value for Boolean is false, so setting to false after attaching won't flag the property as changed.
You can load the entity first, then do your modifications then save changes.
var user = db.Users.Find(5);
user.Locked = false;
db.SaveChanges();
Or you can attach the entity with the field you want to update set to a different value for Boolean fields
var user = new User { Id = 5, Locked = true; };
db.Users.Attach(user);
user.Locked = false;
db.SaveChanges();
False is the default value of boolean field, and, thus, setting false again will not report to change tracker that the property was changed. as a result it is not part of insert/update statement.
If it is database first approach, your property should look similar to this(see the setter):
public global::System.Boolean Locked
{
get
{
return _Locked;
}
set
{
if (_Locked != value)
{
OnLockedChanging(value);
ReportPropertyChanging("Locked");
_Locked = StructuralObject.SetValidValue(value);
ReportPropertyChanged("Locked");
OnLockedChanged();
}
}
}

How can I update my DTO's ID when inserting multiple new entities

I'm using EF4. I'm adding a series of new entities from a list of DTOs, and I'm not saving changes until after all of them are added. I'm wanting to set the IDs of the DTOs to what the new entities' IDs are. How on earth do I do this? Does EF provide a mechanism for this?
With a single entity I would do this:
public void InsertMyDto(MyDto a_dto)
{
var newEntity = new MyEntity
{
Name = a_dto.Name,
Type = a_dto.Type.ToString(),
Price = a_dto.Price
};
_dataContext.MyEntities.AddObject(newEntity);
_dataContext.SaveChanges();
a_dto.ID = newEntity.ID;
}
This works fine, but what do I do in this case?
public void InsertMyDtos(IEnumerable<MyDto> a_dtos)
{
foreach (var myDto in a_dtos)
{
var newEntity = new MyEntity
{
Name = myDto.Name,
Type = myDto.Type.ToString(),
Price = myDto.Price
};
// Does some validation logic against the database that might fail.
_dataContext.MyEntities.AddObject(newEntity);
}
_dataContext.SaveChanges();
// ???
}
I want to save all at once, because I have validation work (not shown above) that is done against the database and fails before it gets to SaveChanges, and if it fails I want it to fail as a whole transaction (i.e. rollback).
I don't think that EF can help you here. It even can't help you for a single instance which forces you to write a_dto.ID = newEntity.ID. The counterpart of this code for multiple entites is to keep track of the pairs of dtos and new entities:
public void InsertMyDtos(IEnumerable<MyDto> a_dtos)
{
Dictionary<MyDto, MyEntity> dict = new Dictionary<MyDto, MyEntity>();
foreach (var myDto in a_dtos)
{
var newEntity = new MyEntity
{
Name = myDto.Name,
Type = myDto.Type.ToString(),
Price = myDto.Price
};
dict.Add(myDto, newEntity);
// Does some validation logic against the database that might fail.
_dataContext.MyEntities.AddObject(newEntity);
}
_dataContext.SaveChanges();
foreach (var item in dict)
item.Key.ID = item.Value.ID; // Key is MyDto, Value is MyEntity
}

Requiring an Id from SaveChanges, whilst saving? Entity Framework

I am inserting a record into the database, which looks like this:
class Transaction
{
int Id;
}
What I want, is when I insert this object, I want to create another record, like this:
class TransactionUpdate
{
int StartingTransactionId;
int EndingTransactionId;
}
What I have so far, is a loop in my SaveChanges on the DbContext, which takes new Transaction objects that will be created and creates TransationUpdate objects and attaches these to the DbContext.
public override int SaveChanges()
{
foreach(var entry in this.ChangeTracker.Entries())
{
if(entry.Entity is Transaction)
{
var update = new TransactionUpdate();
update.StartingTransactionId = ((Transaction)entry.Entity).PreviousTransactionId;
update.EndingTransactionId = ((Transaction)entry.Entity).Id; // This is zero because the entity has not been inserted.
this.TransactionUpdates.Add(update);
}
}
}
The problem is, I cannot properly create a TransactionUpdate because I do not have 'EndingTransactionId', or, the Id of the Transaction I am currently inserting.
How can I solve this problem?
Many Thanks.
SOLVED
I have done what Ladislav suggested and am now creating a list of items to add, along with references to the objects that are required to insert them. Thus:
public override int SaveChanges()
{
var transactionUpdatesToAdd = new List<Tuple<TransactionUpdate, Transaction>>();
foreach (var entry in this.ChangeTracker.Entries<Transaction>())
{
if (entry.State == EntityState.Added)
{
var update = new TransactionUpdate();
update.StartingTransactionId = ((Transaction)entry.Entity).PreviousTransactionId;
transactionUpdatesToAdd.Add(new Tuple<TransactionUpdate, Transaction>(update, entry.Entity));
}
}
using(var scope = new TransactionScope())
{
// Save new Transactions
base.SaveChanges();
// Update TransactionUpdates with new IDs
foreach (var updateData in transactionUpdatesToAdd)
{
updateData.Item1.EndingTransactionId = updateData.Item2.Id;
this.TransactionUpdates.Add(updateData.Item1);
}
// Insert the new TransactionUpdate entities.
return base.SaveChanges();
}
Based on your description I guess you are using autogenerated Id in database. You will not receive this Id befere executing SaveChanges on the context. You have to divide operation into two separate modifications:
public override int SaveChanges()
{
// call base context saving operation to insert all Transactions
base.SaveChanges();
foreach(var entry in this.ChangeTracker.Entries())
{
if(entry.Entity is Transaction)
{
var update = new TransactionUpdate();
update.StartingTransactionId = ((Transaction)entry.Entity).PreviousTransactionId;
update.EndingTransactionId = ((Transaction)entry.Entity).Id;
this.TransactionUpdates.Add(update);
}
}
// save changes again to insert all TransactionUpdates
base.SaveChanges();
}
You should wrap it into TransactionScope to perform whole saving as atomic operation.
If you haven't inserted TransactionId, you have it anyway in your object. Pass your object as parameter to an overloaded method SaveChanges and use it to pass the Id