An action item from the security scan is to address the following error
The database transaction is not always rolled back on errors entity framework in DatabaseContext.cs
Open database connection with try {} block
Begin database transaction with try {} block
No unfinished transaction before closing database
Ensure the transaction is rolled back in the catch { } block or finally { } block
The method that causes this issue is
public async Task CommitTransactionAsync(IDbContextTransaction transaction)
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}
try
{
await this.SaveChangesAsync().ConfigureAwait(false);
transaction.Commit();
}
catch
{
this.RollbackTransaction();
throw;
}
finally
{
if (this.currentTransaction != null)
{
this.currentTransaction.Dispose();
this.currentTransaction = null;
}
}
}
As far as I see, Catch block handles the roll back in case of any errors. Am I missing anything here?
Update:
Below is BeginTransaction method
public async Task<IDbContextTransaction> BeginTransactionAsync()
{
if (this.currentTransaction != null)
{
return null;
}
this.currentTransaction = await this.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted).ConfigureAwait(false);
return this.currentTransaction;
}
and it is used like
using (var transaction = await this.demoRepository.UnitOfWork.BeginTransactionAsync().ConfigureAwait(false))
{
await this.demoRepository.UnitOfWork.CommitTransactionAsync(transaction).ConfigureAwait(false);
}
We wrote our own simple execution strategy to retry saving any data using our DbContext when it runs into a table lock timeout.
public class RetryTransactionExecutionStrategy : DbExecutionStrategy
{
public RetryTransactionExecutionStrategy() : base()
{
}
protected override bool ShouldRetryOn(Exception exception)
{
while (exception != null)
{
if (exception is MySqlException ex
&& ex.Number == 1205) // Deadlock error code
{
return true;
}
exception = exception.InnerException;
}
return false;
}
}
We register it by using the DbConfig class, in the same folder as the context class.
public class DbConfig : DbConfiguration
{
public DbConfig()
{
SetExecutionStrategy(MySqlProviderInvariantName.ProviderName, () => new RetryTransactionExecutionStrategy());
}
}
Now most regular usage of the context will use the retry execution strategy. However, transactions are a more special case. Microsoft mentions usage of them in their documentation, and tells the user to manually call the execution strategy, like this:
var executionStrategy = new RetryTransactionExecutionStrategy();
executionStrategy.Execute(() =>
{
using (PigDbAccountEntities pigDbAccountEntities = new PigDbAccountEntities())
{
using (var dbtransaction = pigDbAccountEntities.Database.BeginTransaction())
{
try
{
//work on some data
pigDbAccountEntities.SaveChanges();
//work on some more data
pigDbAccountEntities.SaveChanges();
//work on even more data
pigDbAccountEntities.SaveChanges();
dbtransaction.Commit();
isSaved = true;
}
catch (Exception ex)
{
dbtransaction.Rollback();
Logger.Instance.Log(LogLevel.ERROR, LogSource.DB, "error in AccountEntityManager.SaveApplicationUser", ex);
}
}
}
});
And yet we still get this error message:
The configured execution strategy 'RetryTransactionExecutionStrategy' does not support user initiated transactions. See http://go.microsoft.com/fwlink/?LinkId=309381 for additional information.
Any idea on what to do/check?
I have the following code, which stores information in two different tables in the same method
public static async Task<Response> AddStockTransaction(StockTransactionsHeader header, List<StockTransactionsDetails> details)
{
using (DataContext dbContext = new DataContext())
{
try
{
dbContext.StockTransactionsHeader.Add(header);
await dbContext.SaveChangesAsync();
int hearderID = header.TransactionHeaderID;
foreach (var item in details)
{
item.TransactionHeaderID = hearderID;
}
dbContext.StockTransactionsDetails.AddRange(details);
await dbContext.SaveChangesAsync();
return new Response
{
IsSuccess = true
};
}
catch (Exception ex)
{
return new Response
{
IsSuccess = false,
Message = ex.Message
};
}
}
}
How can I do, in case there is an exception in the second SaveChanges () to revert the first one?
Once SaveChanges has been called, your datat is stored on your database. You should not call SaveChanges more than once in a call, unless you are willingly to persist the intermediate steps.
You can use a transaction scope to create managed transactions :
using (TransactionScope scope = CreateTransactionScope())
{
DoSomthing(context);
scope.Complete();
}
however, if the failure of the second part involves rolling back the first one, this means that both parts belong to the same transaction, therefore simply omitting the first SaveChanges would turn your code into a single transaction.
From my another awnser: You could use DbTransaction class.
private void TestTransaction()
{
var context = new MyContext(connectionString);
using (var transaction = context.Database.BeginTransaction())
{
try
{
// do your stuff
// commit changes
transaction.Commit();
}
catch
{
// 'undo' all changes
transaction.Rollback();
}
}
}
I am using a similar code like this and I was wondering if using return in this context is possible/acceptable as a best practice coding.
public static async void Run()
{
using (var transaction = dbContext.Database.BeginTransaction())
{
[some code]
if (list == null || list.Count == 0) return;
[continue code]
transaction.Commit();
}
catch
{
transaction.Rollback();
}
}
Will the transaction either commit or rollback ?
I have an app that reads a lot of data into memory and processes it in a batches.
What I want is for entity framework to ignore DbUpdateConcurrencyException when deleting an entity that has already been deleted.
The reason is that by the time an entity has been processed and marked for deletion, it may already have been deleted from the DB.
Obliviously deleting a row that has already been deleted isn't a problem and shouldn't cause an error, I just need a way to tell entity framework that :)
Example
Db.Entry(itemToRemove).State = EntityState.Deleted;
Db.SaveChanges();
Causes an error if itemToRemove has already been deleted.
Note: Db.Configuration.ValidateOnSaveEnabled = false; doesn't fix this as another thread suggested.
How about?
Db.Entry(itemToRemove).State = EntityState.Deleted;
bool saveFailed;
do
{
saveFailed = false;
try
{
Db.SaveChanges();
}
catch(DbUpdateConcurrencyException ex)
{
saveFailed = true;
var entry = ex.Entries.Single();
//The MSDN examples use Single so I think there will be only one
//but if you prefer - do it for all entries
//foreach(var entry in ex.Entries)
//{
if(entry.State == EntityState.Deleted)
//When EF deletes an item its state is set to Detached
//http://msdn.microsoft.com/en-us/data/jj592676.aspx
entry.State = EntityState.Detached;
else
entry.OriginalValues.SetValues(entry.GetDatabaseValues());
//throw; //You may prefer not to resolve when updating
//}
}
} while (saveFailed);
More here:
Resolving optimistic concurrency exceptions
I posted this question a long time ago but it has recently had some attention so I though I would add the solution I actually use.
//retry up to 5 times
for (var retries = 0; retries < 5; retries++)
{
try
{
Db.SaveChanges();
break;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entity in ex.Entries)
{
entity.State = EntityState.Detached;
}
}
}
Things I considered - I did NOT want to use ReloadAsync() or ObjectContext.Refresh as I wanted to ignore items deleted in another process WITHOUT any additional database overhead.
I added in the for loop as a simple protection against infinite loops - not something that should be able to happen, but I'm a belt and braces approach man and not a fan of while(true) if it can be avoided.
No need to a local variable like isDone or saveFailed - simply break if we saved successfully.
No need to cast ex.Entries to a list in order to enumerate it - just because you can write something on one line doesn't make it better.
You could handle the DbUpdateConcurrencyException and then call Refresh(RefreshMode,IEnumerable) with RefreshMode.StoreWins and your deleted entities as parameter.
try{
Db.Entry(itemToRemove).State = EntityState.Deleted;
Db.SaveChanges();
}
catch(DbUpdateConcurrencyException)
{
IObjectContextAdapter adapter = Db;
adapter.ObjectContext.Refresh(RefreshMode.StoreWins, context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Deleted));
Db.SaveChanges();
}
Based on the code from https://msdn.microsoft.com/en-US/data/jj592904 but where I added an infite loop counter (just in case, you never know, right?) and looping through all the entries in the exception's list.
var maxTriesCounter = 20;
bool saveFailed;
do
{
saveFailed = false;
maxTriesCounter--;
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
foreach (var entry in ex.Entries)
{
entry.Reload();
}
}
} while (saveFailed && maxTriesCounter > 0);
Here is what I use. Detach all problem records after the save.
Db.Entry(itemToRemove).State = EntityState.Deleted;
while(true)
try {
Db.SaveChanges();
break;
} catch (DbUpdateConcurrencyException ex) {
ex.Entries.ToList().ForEach(x=>x.State=EntityState.Detached);
}
Or you could add a custom SaveChanges function to your DbContext class and use it instead whenever you need to ignore those errors.
public int SaveChanges_IgnoreConcurrencyExceptions () {
while(true)
try {
return this.SaveChanges();
} catch (DbUpdateConcurrencyException ex) {
ex.Entries.ToList().ForEach(x => x.State=EntityState.Detached);
}
}
This is my approach:
public async Task DeleteItem(int id)
{
bool isDone = false;
while (!isDone)
{
var item= await dbContext.Items.AsNoTracking().SingleOrDefaultAsync(x=> x.id== id);
if (item== null)
return;
dbContext.Items.Delete(item);
try
{
await dbContext.CommitAsync();
return;
}
catch (DbUpdateConcurrencyException ex)
{
}
}
}
This is another approach:
context.Delete(item);
bool saveFailed;
do
{
saveFailed = false;
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
var entity = ex.Entries.Single();
await entity.Single().ReloadAsync();
if (entity.State == EntityState.Unchanged)// entity is already updated
context.Delete(item);;
else if (entity.State == EntityState.Detached) // entity is already deleted
saveFailed =false;
}
} while (saveFailed);
ReloadAsync() method as of Microsoft docs :
Reloads the entity from the database overwriting any property values
with values from the database.
The entity will be in the Unchanged state after calling this method,
unless the entity does not exist in the database, in which case the
entity will be Detached. Finally, calling Reload on an Added entity
that does not exist in the database is a no-op. Note, however, that an
Added entity may not yet have had its permanent key value created.