Transactions: IsolationLevel set to Serializable - is this correct? - c#

Could you please help me out confirming/disconfirming my doubts using Serializable isolationlevel.
I want to make sure that I've chosen a proper IsolationLevel for the following method AddUserTournament. Whenever this transaction is being executed, I want to be 100% sure, that entity.seats is up-to-date - preventing any other transactions from adding users concurrently.
Can I implement this using a more loose IsolationLevel, or does this seems as the proper way?
public void AddUserTournament(Tournament entity, User user)
{
try
{
// Add the user to tournament. Do this with a snapshot-isolationlevel
using (var transaction = _dbContext.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
{
try
{
// Get all users and include their relations to tournament
entity =
_dbContext.Tournaments.Where(e => e.TournamentId == entity.TournamentId)
.Include(t => t.Users)
.FirstOrDefault();
// Get the user
user = _dbContext.Users.SingleOrDefault(e => e.UserId == user.UserId);
// Check if there are available seats in the tournament and if user exist amoung enrolled users
if (entity != null && entity.Seats > entity.Users.Count &&
entity.Users.All(e => user != null && e.UserId != user.UserId))
{
// Add user
entity?.Users.Add(user);
// Save changes
_dbContext.SaveChanges();
// Commit transaction
transaction.Commit();
}
}
catch (Exception)
{
transaction.Rollback();
}
}
}
catch (DbEntityValidationException ex)
{
throw new FaultException(new FormattedDbEntityValidationException(ex).Message);
}
}

I know this is not answering your question, but feel i should point out how to make it more readable.
Just showing how you could rewrite you 'if' into something readable.
//after you refactor it more you will see that this check is actually not needed.
if (entity != null)
{
//Called entity but represents a Tournament
//so the check is to see if the T has more seats than the current attending users
if(entity.Seats.Count > entity.Users.Count)
{
//this was to check if the user is already in the attending users
//if(entity.Users.All(e => user != null && e.UserId != user.UserId))
var userInSeats = entity.Users.Where(x=>x.UserId == user.UserId).SingleOrDefualt();
if(userInSeats == null )
{
// Add user
entity.Users.Add(user);
// Save changes
_dbContext.SaveChanges();
// Commit transaction
transaction.Commit();
}
}
}
you would then see that the null check is not needed as this method should not be able to be called with no entity... i would call things what they are unless you are using generics which you aint here.

Related

"Buy Items" with SQL Transaction in EF Core, if user send many requests, items are bought even if they can't afford it

I'm trying to make a game where the user can buy items with ingame coins. The coins and the items are stored in a SQL database, and user should (obviously) only be able to buy items that they can afford.
The Problem with this code is that if the user spams many requests at the same time, he gets more items than he can afford since the next request is sent before the previous one is done updating the database.
How do I prevent this "spam posting bug"?
public async Task<ActionResult<UserProfile>> BuyItem(ShopItem item)
{
try
{
//Begin Transaction
using var transaction = _context.Database.BeginTransaction();
var user = await _context.UserProfiles.FirstOrDefaultAsync(u => u.Name == User.Identity.Name);
await _context.Database.OpenConnectionAsync();
//If user can afford the item
if (user.Coins >= item.Price)
{
//Decrease money from user
user.Coins -= item.Price;
_context.Entry(user).State = EntityState.Modified;
await _context.SaveChangesAsync();
//Create new item in database that belongs to the user who bought it
var userItem = new UserItem()
{
ShopItemId = item.Id,
UserProfileId = user.Id
};
_context.UserItems.Add(userItem);
await _context.SaveChangesAsync();
}
else
{
//Throw exception, this should rollback the transaction?
throw new Exception();
}
//End Transaction
_context.Database.CloseConnection();
transaction.Commit();
return new JsonResult(user);
}
catch (Exception ex)
{
throw;
}
}
_context.Database.BeginTransaction(IsolationLevel.Serializable)
Specifies the following:
Statements cannot read data that has been modified but not yet
committed by other transactions.
No other transactions can modify data that has been read by the
current transaction until the current transaction completes.
Other transactions cannot insert new rows with key values that would
fall in the range of keys read by any statements in the current
transaction until the current transaction completes.
Docs:
https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?redirectedfrom=MSDN&view=sql-server-ver15

EF Core Same TABLE TWO ACTIONS (Remove item + update remaining ones)

I have a weird situation.
I have a list from where I need to remove an item from the DB, then all remaining ones I need to just increment and update them.
I use.AsNoTracking() on the list filtering but I cannot perform the above actions thought.
Keeps telling me that "
"The property 'ListNumber' on entity type 'LIST' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal."
I've tried also to build 1 method for remove and 1 for update the remaining items and call them from the controller (the first one is the remove then the second one is the update) but the same result.
Anyone can help me out because I'm stuck I would highly appreciate a clear example or approach on how I can deal with the above situation.
Thank you in advance.
This is how I call the methods from the controller
await _repo.RemoveFromList("1234", "1");
await _repo.ResetList("1234");
Below are the methods I'm using, remove one works but when I'm using the reset list I got an exception
public async Task RemoveFromList(string listNumber, string listItem)
{
try
{
var entity = await _context.LIST.Where(x => x.ListNumber == listNumber && x.ListItem == listItem).FirstOrDefaultAsync();
_context.LIST.Remove(entity);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
throw;
}
}
public async Task ResetList(string listNumber)
{
try
{
var entities = await _context.LIST.AsQueryable().Where(x => ListNumber== listNumber).ToListAsync();
var startFrom = 1;
foreach (var en in entities)
{
en.NewListNumber = startFrom.ToString().PadLeft(3, '0');
startFrom++;
_context.LIST.Update(en);
}
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
throw;
}
}

delete azure table storage row without checking for existence

I've been using azure table storage for years, and I'm not sure what the "proper" way to do this is with the newest WindowsAzure.Storage library, version 5.0.1-preview (for use in a new ASP.NET 5 application):
Problem:
Given a partition key and row key, delete the row without checking for existence first, and without failing if it does not exist.
Current Solution: This code works... but the exception handling is confusing:
public async Task DeleteRowAsync(CloudTable table, string partition, string row)
{
var entity = new DynamicTableEntity(partition, row);
entity.ETag = "*";
var op = TableOperation.Delete(entity);
try
{
await table.ExecuteAsync(op);
}
catch (Exception ex)
{
var result = RequestResult.TranslateFromExceptionMessage(ex.Message);
if (result == null || result.HttpStatusCode != 404)
throw ex;
}
}
Questions:
The exception itself pointed me to this TranslateFromExceptionMessage method... I can't find a whole lot of information on that and WrappedStorageException (the type of the exception that is thrown). Is this some kind of new/preferred way to check for 404 errors on storage exceptions? Does anyone know if all storage exceptions will now use this, or do I need to write code to test and figure it out?
There is an InnerException of type StorageException. Presumably our older code that used StorageException.RequestInformation.HttpStatusCode could access this inner exception in the same way. Is that "OK", or is parsing these new XML error messages better or more robust somehow?
Is there a different approach altogether that I should be considering for this case?
If you are using the latest client (Azure.Data.Tables), the delete method automatically swallows 404 responses and does not throw. This approach avoids the need to write code that introduces race conditions (checking first before performing an operations) or having to handle this condition with a try/catch block.
If you want to know if the operation actually deleted a table or it didn't exist, you can inspect the Status property of the response.
Response response = await tableClient.DeleteAsync();
if (response.Status == (int)HttpStatusCode.NotFound)
{
// entity didn't exist)
}
The RequestResult.TranslateFromExceptionMessage method is now marked [Obsolete] and I wanted a way to ignore 404's myself.
Based on your tip to check out the RequestInformation.HttpStatusCode I came up with the following:
try
{
await table.ExecuteAsync(op);
}
catch (StorageException storEx)
{
if (storEx.RequestInformation.HttpStatusCode != 404)
{
throw;
}
}
There is a similar approach found in the AspNet WebHooks project when configured to use Azure Table Storage. Take a look at the Microsoft.Aspnet.WebHooks.custom.AzureStorage StorageManager class.
I'm not sure this adds much on top of what you'd already found, but they handle everything without throwing an exception and always return a status code so you can react to that as necessary.
One difference here is they pass in the table and the operation to a multi-purpose ExecuteAsync method, rather than having one specifically for delete, but that's just an implementation detail.
Relevant code from their example:
public async Task<TableResult> ExecuteAsync(CloudTable table, TableOperation operation)
{
if (table == null)
{
throw new ArgumentNullException(nameof(table));
}
if (operation == null)
{
throw new ArgumentNullException(nameof(operation));
}
try
{
var result = await table.ExecuteAsync(operation);
return result;
}
catch (Exception ex)
{
var errorMessage = GetStorageErrorMessage(ex);
var statusCode = GetStorageStatusCode(ex);
var message = string.Format(CultureInfo.CurrentCulture, AzureStorageResources.StorageManager_OperationFailed, statusCode, errorMessage);
_logger.Error(message, ex);
return new TableResult { HttpStatusCode = statusCode };
}
}
public string GetStorageErrorMessage(Exception ex)
{
if (ex is StorageException storageException && storageException.RequestInformation != null)
{
var status = storageException.RequestInformation.HttpStatusMessage != null ?
storageException.RequestInformation.HttpStatusMessage + " " :
string.Empty;
var errorCode = storageException.RequestInformation.ExtendedErrorInformation != null ?
"(" + storageException.RequestInformation.ExtendedErrorInformation.ErrorMessage + ")" :
string.Empty;
return status + errorCode;
}
else if (ex != null)
{
return ex.Message;
}
return string.Empty;
}
public int GetStorageStatusCode(Exception ex)
{
return ex is StorageException se && se.RequestInformation != null ? se.RequestInformation.HttpStatusCode : 500;
}

Deadlock when previous query threw an exception

Using entity framework, I have a function that basically goes something like this:
using (var ctx = new Dal.MyEntities())
{
try
{
//...
// create a temp entity
Dal.Temp temp = new Dal.Temp();
// populate its children
// note that temp is set to cascade deletes down to it's children
temp.Children = from foo in foos
select new Dal.Children()
{
// set some properties...
Field1 = foo.field1,
Field2 = foo.field2
}
//...
// add temp row to temp table
ctx.Temp.Add(temp);
ctx.SaveChanges();
// some query that joins on the temp table...
var results = from d in ctx.SomeOtherTable
join t in temp.Children
on new { d.Field1, d.Field2 } equals new { t.Field1, d.Field2 }
select d;
if (results.Count() == 0)
{
throw new Exception("no results")
}
// Normal processing and return result
return results;
}
finally
{
if (temp != null && temp.ID != 0)
{
ctx.TempTables.Remove(temp);
ctx.SaveChanges();
}
}
}
The idea is that as part of the processing of a request I need to build a temporary table with some data that then gets used to join the main query and filter the results. Once the query has been processed, the temp table should be deleted. I put the deletion part in the finally clause so that if there is a problem with the query (an exception thrown), the temporary table will always get cleaned up.
This seems to work fine, except intermittently I have a problem were the SaveChanges in the finally block throws a deadlock exception with an error message along the lines of:
Transaction (Process ID 89) was deadlocked on lock resources with another process and
has been chosen as the deadlock victim. Rerun the transaction.
I can't reliably reproduce it, but it seems to happen most often if the previous query threw the "no results" exception. Note that, due to an error that was discovered on the front end, two identically requests were being submitted under certain circumstances, but nevertheless, the code should be able to handle that.
Does anybody have an clues as to what might be happening here? Is throwing an exception inside the using block a problem? Should I handle that differently?
Update, so the exception might be a red herring. I removed it altogether (instead returning an empty result) and I still have the problem. I've tried a bunch of variations on:
using (new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted })
using (var ctx = new Dal.MyEntities())
{
}
But despite what I've read, it doesn't seem to make any difference. I still get intermittent deadlocks on the second SaveChanges to remove the temp table.
how about adding a
using (var ctx = new Dal.MyEntities())
{
try
{
//...
Dal.TempTable temp = new Dal.TempTable();
//...
ctx.TempTables.Add(temp);
// some query that joins on the temp table...
if (no
results are
returned)
{
throw new Exception("no results")
}
// Normal processing and return result
}
catch
{
ctx.TempTables.Remove(temp);
ctx.SaveChanges();
}
finally
{
if (temp != null && temp.ID != 0)
{
ctx.TempTables.Remove(temp);
ctx.SaveChanges();
}
}
}

intermittent System.Data.Entity.Infrastructure.DbUpdateConcurrencyException

The following code is causing an intermittent exception:
public int UnblockJob(int jobId)
{
using (var connect = MakeConnect())
{
var tag = connect.JobTag.SingleOrDefault(jt => jt.JobId == jobId && jt.Name == Metrics.TagNameItemBlockCaller);
if (tag == null)
{
return 0;
}
connect.JobTag.Remove(tag);
return connect.SaveChanges();
}
}
How can I correct or troubleshoot it?
From the documentation for DbUpdateConcurrencyException:
Exception thrown by DbContext when it was expected that SaveChanges for an entity would result in a database update but in fact no rows in the database were affected.
This means that the record you are attempting to delete has since been removed from the database. It would appear that you have another process that is deleting records or this function is able to be called concurrently.
There are several solutions, here are a couple:
Fix the source problem Stop other processes affecting the data.
Catch the error Wrap this method in a try/catch block, after all you may only care that the record has been deleted:
try
{
//Existing code here
}
catch(DbUpdateConcurrencyException)
{
//Safely ignore this exception
}
catch(Exception e)
{
//Something else has occurred
throw;
}

Categories