Based on Colin's answer, fully detailed information on EF persistence failure can be provided like this:
public bool SaveChangesEx()
{
try
{
SaveChanges();
return true;
}
catch (DbEntityValidationException exc)
{
// just to ease debugging
foreach (var error in exc.EntityValidationErrors)
{
foreach (var errorMsg in error.ValidationErrors)
{
// logging service based on NLog
Logger.Log(LogLevel.Error, $"Error trying to save EF changes - {errorMsg.ErrorMessage}");
}
}
throw;
}
catch (DbUpdateException e)
{
var sb = new StringBuilder();
sb.AppendLine($"DbUpdateException error details - {e?.InnerException?.InnerException?.Message}");
foreach (var eve in e.Entries)
{
sb.AppendLine($"Entity of type {eve.Entity.GetType().Name} in state {eve.State} could not be updated");
}
Logger.Log(LogLevel.Error, e, sb.ToString());
throw;
}
}
Beside validation errors, update exception will output both general error and context information.
Note: C# 6.0 is required for this code to work, as it uses null propagation and string interpolation.
For .NET Core the code is slightly changed since possible raised exceptions have a different structure / are populated differently:
public void SaveChangesEx()
{
try
{
// this triggers defined validations such as required
Context.Validate();
// actual save of changes
Context.SaveChangesInner();
}
catch (ValidationException exc)
{
Logger.LogError(exc, $"{nameof(SaveChanges)} validation exception: {exc?.Message}");
throw;
}
catch (DbUpdateException exc)
{
Logger.LogError(exc, $"{nameof(SaveChanges)} db update error: {exc?.InnerException?.Message}");
throw;
}
catch (Exception exc)
{
// should never reach here. If it does, handle the more specific exception
Logger.LogError(exc, $"{nameof(SaveChanges)} generic error: {exc.Message}");
throw;
}
}
The Context can be enhanced to automatically reject changes on failure, if the same context is not immediately disposed:
public void RejectChanges()
{
foreach (var entry in ChangeTracker.Entries().Where(e => e.Entity != null).ToList())
{
switch (entry.State)
{
case EntityState.Modified:
case EntityState.Deleted:
entry.State = EntityState.Modified; //Revert changes made to deleted entity.
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}
public bool SaveChangesInner()
{
try
{
SaveChanges();
return true;
}
catch (Exception)
{
RejectChanges();
throw;
}
}