Entity Framework Core - Error Handling on multiple contexts

Viewed 48

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?

2 Answers

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.

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);
    }
}
Related