1

I am building an API where I get a specific object sent as a JSON and then it gets converted into another object of another type, so we have sentObject and convertedObject. Now I can do this:

using (var dbContext = _dbContextFactory.CreateDbContext())
using (var dbContext2 = _dbContextFactory2.CreateDbContext())
{
    await dbContext.AddAsync(sentObject);
    await dbContext.SaveChangesAsync();
    await dbContext2.AddAsync(convertedObject);
    await dbContext2.SaveChangesAsync();
}

Now I had a problem where the first SaveChanges call went ok but the second threw an error with a datefield that was not properly set. The first SaveChanges call happened so the data is inserted in the database while the second SaveChanges failed, which cannot happen in my use-case.

What I want to do is if the second SaveChanges call goes wrong then I basically want to rollback the changes that have been made by the first SaveChanges.

My first thought was to delete cascade but the sentObject has a complex structure and I don't want to run into circular problems with delete cascade.

Is there any tips on how I could somehow rollback my changes if one of the SaveChanges calls fails?

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
Florent
  • 111
  • 10

2 Answers2

0

You can call context.Database.BeginTransaction as follows:

                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    context.Database.ExecuteSqlCommand(
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'"
                        );

                    var query = context.Posts.Where(p => p.Blog.Rating >= 5);
                    foreach (var post in query)
                    {
                        post.Title += "[Cool Blog]";
                    }

                    context.SaveChanges();

                    dbContextTransaction.Commit();
                }

(taken from the docs)

You can therefore begin a transaction for dbContext in your case and if the second command failed, call dbContextTransaction.Rollback();

Alternatively, you can implement the cleanup logic yourself, but it would be messy to maintain that as your code here evolves in the future.

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
  • Hi thanks for your answer! So if I want to do this with my second context then I have to open a new transaction? Or call both savechanges in one transaction? Calling both savechanges in one transaction throws me an error which is discussed in this topic: https://stackoverflow.com/questions/67254556/execution-strategy-sqlserverretryingexecutionstrategy-does-not-support-user-in. I can use this in .net5 too. – Florent Aug 10 '22 at 09:36
  • @Florent it has been quite a few years since I was working with .NET for the last time, but as far as I remember (maybe wrongly), you can keep the `SaveChanges` in different contexts/transactions and `Rollback` the first if the second fails. I would experiment with this if I were you, because this very much seems like a workable plan, even though, not without technical difficulties. If all else fails, then you can implement the cleanup code yourself and call that. I have edited my answer. – Lajos Arpad Aug 10 '22 at 09:43
  • @Florent cascade will not work for you, because the second command fails, so you have nothing to remove and therefore nothing to cascade, at least on the level of the RDBMS. However, you can implement a cleanup code either in a stored procedure or .NET. – Lajos Arpad Aug 10 '22 at 09:44
  • @Florent Also, you can nest transactions via `TransactionScope`, see: https://stackoverflow.com/questions/2794761/transaction-within-a-transaction-in-c-sharp So, you can create two transaction scopes, one for each of the contexts and if the second fails, `Rollback` the first... But I'm very rusty with .NET, trust your eyes more than my letters (even though you see my letters with your eyes - to add a little paradoxical joke to the technicalities) – Lajos Arpad Aug 10 '22 at 09:46
  • If the second savechanges fails I could cascade delete the sentObject which happened beforehand thats why i suggested cascading the first object but yea its a bit complex whatever complex means at least for me its complex – Florent Aug 10 '22 at 09:49
  • 1
    @Florent ah, you mean cascade on the level of .NET. That makes sense and I did not remember about this. But nevertheless, nested transaction scopes seem to be the way to go for you. – Lajos Arpad Aug 10 '22 at 09:51
0

Here is an example code that is working for me, no need for calling the rollback function. Calling the rollback function can fail. If you do it inside the catch block for example then you have a silent exception that gets thrown and you will never know about it. The rollback happens automatically when the transaction object in the using statement gets disposed. You can see this if you go to SSMS and look for the open transactions while debugging. See this for reference: https://github.com/dotnet/EntityFramework.Docs/issues/327 Using Transactions or SaveChanges(false) and AcceptAllChanges()?

using (var transactionApplication = dbContext.Database.BeginTransaction())
{
    try
    {
        await dbContext.AddAsync(toInsertApplication);
        await dbContext.SaveChangesAsync();

        using (var transactionPROWIN = dbContextPROWIN.Database.BeginTransaction())
        {
            try
            {
                await dbContext2.AddAsync(convertedApplication);
                await dbContext2.SaveChangesAsync();
                transaction2.Commit();
                insertOperationResult = ("Insert successfull", false);
            }
            catch (Exception e)
            {
                Logger.LogError(e.ToString());
                insertOperationResult = ("Insert converted object failed", true);
                return;
            }
        }
        transactionApplication.Commit();
    }
    catch (DbUpdateException dbUpdateEx)
    {
        Logger.LogError(dbUpdateEx.ToString());

        if (dbUpdateEx.InnerException.ToString().ToLower().Contains("overflow"))
        {
            insertOperationResult = ("DateTime overflow", true);
            return;
        }
        //transactionApplication.Rollback();
        insertOperationResult = ("Duplicated UUID", true);
    }
    catch (Exception e)
    {
        Logger.LogError(e.ToString());
        transactionApplication.Rollback();
        insertOperationResult = ("Insert Application: Some other error happened", true);
    }
}
Florent
  • 111
  • 10