Entity Framework 6 SaveChanges() override not consistently detecting changes - c#

I've extended a class in order to add a last modified timestamp to a record if there are material changes being made to it. This was done with code similar to this.
Here's my problem. SaveChanges() is firing for both changes, but the second one isn't getting into the loop: no objects are detected as needing changes.
However, the record does get updated by EF through the base.SaveChanges() call.
Here's the extension to MasterTable:
namespace AuditTestEF
{
public interface IHasAuditing
{
DateTime LastModifiedOn { get; set; }
int LastModifiedBy { get; set; }
}
public partial class MasterTable : IHasAuditing
{
}
public class AuditTestEntitiesWithAuditing : AuditTestEntities
{
public int TestingUserIs = 1;
public override int SaveChanges()
{
foreach (ObjectStateEntry entry in (this as IObjectContextAdapter)
.ObjectContext
.ObjectStateManager
.GetObjectStateEntries(EntityState.Added | EntityState.Modified))
{
// This loop is entered the first time, but not the second
if (entry.IsRelationship) continue;
var lastModified = entry.Entity as IHasAuditing;
if (lastModified == null) continue;
lastModified.LastModifiedOn = DateTime.UtcNow;
lastModified.LastModifiedBy = TestingUserIs;
}
return base.SaveChanges();
}
}
}
And here's the test harness:
[TestMethod]
public void TestMethod1()
{
MasterTable mtOriginal;
using (var audit = new AuditTestEntitiesWithAuditing())
{
var message = "Hello";
audit.TestingUserIs = 1;
mtOriginal = new MasterTable {TextField = message};
audit.MasterTable.Add(mtOriginal);
audit.SaveChanges();
// This test passes, TestingUser is set in the override
Assert.IsTrue(mtOriginal.LastModifiedBy == audit.TestingUserIs);
}
using (var audit = new AuditTestEntitiesWithAuditing())
{
var mt = audit.MasterTable.Find(mtOriginal.MasterTableId);
mt.TextField = "Goodbye";
audit.TestingUserIs = 4;
audit.SaveChanges();
// This test fails, the record is written with "Goodbye" but
// GetObjectStateEntries(EntityState.Added | EntityState.Modified) has no entries.
Assert.IsTrue(mt.LastModifiedBy == audit.TestingUserIs);
}
}
There's no other code. There's no weird turning off/on the entity tracking or anything. WYSIWYG.
What am I missing? How is the clearly modified object being missed by the check for Modified?

Annnd... answered my own question, of course, after talking to the duck.
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
This fixes everything. Thank you for your attention, I hope this helps someone else.

Related

Php to C# aspnet mvc

i have transform a php/js code to js/c#, but i stuck for update the new value.
The php code is :
`if (isset($_POST['update'])) {
foreach($_POST['positions'] as $position) {
$index = $position[0];
$newPosition = $position[1];
$conn->query("UPDATE country SET position = '$newPosition' WHERE id='$index'");
}
exit('success');
}`
My "empty" c# code
[HttpPost]
public ActionResult Index (userTable index)
{
picturesEntities MyDb = new picturesEntities();
homeViewModel HVM = new homeViewModel();
HVM.userTables = MyDb.userTables.ToList();
if (Request["update"] != null)
{
foreach (Request["positions"])
{
MyDb.SaveChanges();
}
return View(HVM);
}
}
If someone could help me for it that would be great, i'm stuck on it for days and i didn't find a workning solution yet.
Thanks to everyone who read my message.
Most ASP.NET will bind a custom class which will be compatible to your request.
public class UserPositionsRequest
{
public bool Update { get; set; }
// For orderly, this actually be a list of a custom class
public List<int[]> Positions { get; set; }
}
This by any means is not a complete and working solution, the following code was never been tested and can be consider as pseudo-like code.
Also, the .Id and .Position should be the same sensitivity as in Db.
// Binding our UserPositionsRequest class
public void Index(UserPositionsRequest request) {
// Checking if we should update, if you will change the request to boolean type: "true"
// ..on the client side, then you could actually change the condition to be: if (request.Update)
if (request.Update == 1) {
// Creating database connection using (I assume) EntityFramework
using (var picturesEntities = new picturesEntities()) {
// Building a dictionary for fast lookup. Key, Value as the 0, 1 arg respectfully
var usersDataToUpdate = request.Positions.ToDictionary(p => p[0], p => p[1]);
// Finding the entries that needs to be updated
var usersEntitiesToUpdate = picturesEntities.userTables.Where(cntry => usersDataToUpdate.ContainsKey(cntry.Id));
// Iterating over the entities
foreach (var userEntity in usersEntitiesToUpdate) {
// Updating their position.
userEntity.Position = usersDataToUpdate[userEntity.Id];
}
picturesEntities.SaveChanges();
}
}
// Probably you wanted to return something here, but it's probably an ajax and you can skip that.
}

How to limit linked entities in Entity Framework Core

When using Entity Framework Core, we've got two linked entities, as a simplified example a course and a booking. A course has a list of bookings. The course also has a property for the number of allowed bookings (This can be different on each course). When inserting a record into the bookings table for a particular course we want to check that the count of bookings is less than the Allowed Bookings. What would be the best way to do this avoiding concurrency conflicts? Multiple people will be creating bookings at the same time.
I've tried using EF to run the select of the count before inserting but if two are inserting at the same time this can cause the bookings to exceed the limit.
I've thought about using a Date Modified column on the Course table and using that as a concurrency check and thereby updating that every time we add a booking but that would cause a concurrency error when the count may have gone from 4 to 5 and is still under the limit of 10, so should go through without any issue. It would also mean we're updating an extra table when really all we're interested in here is inserting the booking if possible.
--EDIT 1
Here's some code I'm trying. This is in the repository and is called from the controller. The problem with this is if multiple requests come in at once it can go over the limit of AvailableSpaces
public async Task<Models.Booking> AddBooking(Models.Booking booking)
{
var bookingEntity = _mapper.Map<Entities.Booking>(booking);
var e = await _context.Events.Include(x => x.Bookings).FirstOrDefaultAsync(c => c.Id == booking.EventId);
if (e.AvailableSpaces > 0)
{
_context.Add(bookingEntity);
}
await _context.SaveChangesAsync();
return _mapper.Map<Models.Booking>(bookingEntity);
}
--EDIT 2
I've tried using a transaction but this still fails for some reason and goes above the AvailableSpaces limit. Surely this should lock the row until the transaction is committed?
public async Task<Models.Booking> AddBooking(Models.Booking booking)
{
var bookingEntity = _mapper.Map<Entities.Booking>(booking);
var strategy = _context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using (var transaction = _context.Database.BeginTransaction(IsolationLevel.Serializable))
{
try
{
var e = await _context.Events.Include(x => x.Bookings).FirstOrDefaultAsync(c => c.Id == booking.EventId);
if (e.AvailableSpaces == 0)
throw new Exception("No free spaces");
_context.Add(bookingEntity);
await _context.SaveChangesAsync();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
});
return _mapper.Map<Models.Booking>(bookingEntity);
}
--EDIT 3
public class Event
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Title { get; set; }
public int TotalSpaces { get; set; }
public int AvailableSpaces { get { return TotalSpaces - AllocatedSpaces; } }
public int AllocatedSpaces { get { return Bookings?.Count ?? 0; } }
[Timestamp]
public byte[] RowVersion { get; set; }
public string CourseCode { get; set; }
public virtual ICollection<Booking> Bookings { get; set; }
}
--EDIT 4
Added AvailableSpaces to database and persist this through which enables the concurrency check on the event, but feel this should not be required as I can work this out from the bookings assigned to the event.
public async Task<Models.Booking> AddBooking(Models.Booking booking)
{
if (booking == null)
throw new ArgumentNullException(nameof(booking));
var bookingEntity = _mapper.Map<Entities.Booking>(booking);
bool saveFailed;
var e = await _context.Events.Where(x => x.Bookings.Count < x.TotalSpaces).Include(x => x.Bookings).SingleOrDefaultAsync(c => c.Id == bookingEntity.EventId);
do
{
saveFailed = false;
try
{
if (e == null || e.AvailableSpaces == 0)
throw new Exception("No free spaces");
e.Bookings.Add(bookingEntity);
e.AvailableSpaces -= 1;
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
Console.WriteLine($"Concurrency Exception for event id: {booking.EventId}");
ex.Entries?.Single()?.Reload();
}
} while (saveFailed);
return _mapper.Map<Models.Booking>(bookingEntity);
}
Also tried setting the AvailableSpaces by doing a count of the bookings but then it stops working for some reason and goes over the limit on multiple calls.
e.AvailableSpaces = e.TotalSpaces - (e.Bookings?.Count() ?? 0);

EF Core how to implement audit log of changes to value objects

I am using EF Core/.NET Core 2.1, and following DDD. I need to implement an audit log of all changes to my entities, and have done so using code from this blog post (relevant code from this post included below). This code works and tracks changes to any properties, however when it logs changes to my value objects, it only lists the new values, and no old values.
Some code:
public class Item
{
protected Item(){}
//truncated for brevity
public Weight Weight { get; private set; }
}
public class Weight : ValueObject<Weight>
{
public WeightUnit WeightUnit { get; private set; }
public double WeightValue { get; private set; }
protected Weight() { }
public Weight(WeightUnit weightUnit, double weight)
{
this.WeightUnit = weightUnit;
this.WeightValue = weight;
}
}
and the audit tracking code from my context class
public class MyContext : DbContext
{
//truncated for brevity
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
var auditEntries = OnBeforeSaveChanges();
var result = base.SaveChanges(acceptAllChangesOnSuccess);
OnAfterSaveChanges(auditEntries);
return result;
}
private List<AuditEntry> OnBeforeSaveChanges()
{
if (!this.AuditingAndEntityTimestampingEnabled)
{
return null;
}
ChangeTracker.DetectChanges();
var auditEntries = new List<AuditEntry>();
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
{
continue;
}
var auditEntry = new AuditEntry(entry)
{
TableName = entry.Metadata.Relational().TableName
};
auditEntries.Add(auditEntry);
foreach (var property in entry.Properties)
{
if (property.IsTemporary)
{
// value will be generated by the database, get the value after saving
auditEntry.TemporaryProperties.Add(property);
continue;
}
string propertyName = property.Metadata.Name;
if (property.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[propertyName] = property.CurrentValue;
continue;
}
switch (entry.State)
{
case EntityState.Added:
auditEntry.NewValues[propertyName] = property.CurrentValue;
break;
case EntityState.Deleted:
auditEntry.OldValues[propertyName] = property.OriginalValue;
break;
case EntityState.Modified:
if (property.IsModified)
{
auditEntry.OldValues[propertyName] = property.OriginalValue;
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
break;
}
}
}
// Save audit entities that have all the modifications
foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties))
{
Audits.Add(auditEntry.ToAudit());
}
// keep a list of entries where the value of some properties are unknown at this step
return auditEntries.Where(_ => _.HasTemporaryProperties).ToList();
}
}
Here is a screenshot of how the audit changes persist to the database. The non-value object properties on Item have their old/new values listed, where changes to value objects only list the new values:
Is there a way to get the previous values of the value objects?
UPDATE:
So, the reason the OldValues column is null for changes to my value objects is due to the State of the value object being Added when it has been changed. I added a call to the IsOwned() method to the switch statement, and try to grab the property.OriginalValue within like this:
case EntityState.Added:
if (entry.Metadata.IsOwned())
{
auditEntry.OldValues[propertyName] = property.OriginalValue;
}
auditEntry.NewValues[propertyName] = property.CurrentValue;
break;
However, this simply logs the current value that the value object is being updated to.
So the question still stands - is there any way to get the previous value of a value object using the EF Core ChangeTracker, or do I need to re-think my use of DDD Value Objects due to my audit requirement?
isstead of
auditEntry.OldValues[propertyName] = property.OriginalValue;
use
auditEntry.OldValues[propertyName] = entry.GetDatabaseValues().GetValue<object>(propertyName).ToString();

EF6 Cannot insert duplicate key - I cannot find cause of this exception

I've got problem when I am trying to use method AddTimeSeriesDefinition(TimeSeries series) or AddTimeSeriesMetaData(TimeSeriesMetaData tsData) inside Parallel.ForEach()
I am struggling with this for couple of hours and I cannot believe that I cannot find any solution or even theoretical cause.
Inside my class Data which contains my db context DBEntity db = new DBEntity() I've got AddTimeSeriesDefinition(TimeSeries series) and AddTimeSeriesMetaData() methods :
public class Data : IDisposable
{
private DBEntity db;
public Data()
{
db = new DBEntity();
}
public TimeSeries AddTimeSeriesDefinition(TimeSeries series)
{
var timeSeries = db.TimeSeries.Where(ts => ts.Key1 == series.Key1 )
.Where(ts => ts.Key2 == series.Key2 )
.Where(ts => ts.Key3 == series.Key3 )
.FirstOrDefault();
if ( timeSeries == null )
{
timeSeries = db.TimeSeries.Add(series);
db.SaveChanges();
}
return timeSeries;
}
public void AddTimeSeriesMetaData(TimeSeriesMetaData tsData)
{
var tsd = db.TimeSeriesMetaData.Where(ts => ts.Key1 == tsData.Key1 )
.Where(ts => ts.Key2== tsData.Key2)
.FirstOrDefault();
if (tsd == null)
db.TimeSeriesMetaData.Add(tsData);
else
tsd.Value = tsData.Value;
try
{
db.SaveChanges();
}
catch (Exception ex)
{
Log.Error($"Error occurred (...) Key1:{tsData.Key1} Key2:{tsData.Key2}", ex);
}
}
Dispose()
{...}
}
However when I am using them in my main class, for example :
private MainClass
{
Data DB { get { value = new Data() } }
...
Parallel.ForEach( // arguments )
{
...
using( var db = DB )
{
db.AddTimeSeriesDefinition(timeSeries);
}
...
}
}
it's sometimes, totally randomly crashing in line
db.SaveChanges();
with exception :
Violation of PRIMARY KEY constraint 'PK_TimeSeriesMetaDatas'. Cannot insert duplicate key in object 'dbo.TimeSeriesMetaData'. The duplicate key value is ("Key1", "Key2"). The statement has been terminated.
For example my TimeSeriesMetaData EF class:
[Table("TimeSeriesMetaData")]
public partial class TimeSeriesMetaData
{
[Key]
[Column(Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Key1 { get; set; }
[Key]
[Column(Order = 1)]
public string Key2 { get; set; }
[Required]
public string Key3 { get; set; }
}
I've read that creating Entity Framework DBContext each time for each operation should be also Thread-safe.
What can be cause of this problem if I've always checked if record exist?
I will appreciate any help.
The problem is that DbSet is not ThreadSafe. You are having a run condition withing your Parallel.ForEach loop. You have to lock your call to both of your methods. https://msdn.microsoft.com/en-us/library/c5kehkcz.aspx
Hope it helps

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