Transactions
In the world of software development, especially when dealing with relational databases, transactions are a fundamental concept to ensure data integrity and consistency. A transaction is a sequence of operations performed as a single logical unit of work. In the context of Entity Framework Core (EF Core), transactions play a crucial role in managing database changes effectively and reliably.
What is a Transaction?
A transaction is an all-or-nothing operation. It ensures that either all operations within the transaction are successfully completed, or none of them are applied to the database. Transactions are guided by four key principles often abbreviated as ACID:
Atomicity: Ensures that all operations within a transaction are treated as a single unit. If any operation fails, the entire transaction is rolled back.
Consistency: Guarantees that the database remains in a valid state before and after the transaction.
Isolation: Ensures that concurrent transactions do not interfere with each other.
Durability: Once a transaction is committed, its changes are permanent, even in the event of a system failure.
Why Use Transactions in EF Core?
Transactions are crucial for applications that perform multiple database operations where the success of one operation depends on the success of others. Here are some common scenarios where transactions are indispensable:
Ensuring Data Integrity: For example, in an e-commerce application, deducting stock from inventory and charging a customer’s account must both succeed or both fail. A transaction ensures consistency in such cases.
Handling Errors Gracefully: In the event of an error during a sequence of database operations, a transaction allows the application to roll back changes, leaving the database in a consistent state.
Concurrent Operations: Transactions prevent race conditions and data corruption when multiple operations are performed on the database simultaneously.
Complex Business Logic: When implementing business rules involving multiple data entities, transactions help enforce rules atomically.
Transactions in EF Core
EF Core provides built-in support for transactions, making it easier for developers to manage database operations. There are two common ways to use transactions in EF Core:
Implicit Transactions: EF Core automatically wraps
SaveChanges
calls in a transaction, ensuring the atomicity of all operations within that save call.Explicit Transactions: Developers can manually control the transaction lifecycle using the
DbContext.Database.BeginTransaction
method. This approach is useful for more complex scenarios where multipleSaveChanges
calls or multiple contexts are involved.
Before seeing how the transactions can be used, let's investigate the new SaveChanges methods for REN Unit Of Work (the async version of this method has same logic):
public virtual void SaveChanges()
{
if (_currentTransaction != null)
{
_context.SaveChanges();
return;
}
using var ctxTransaction = _context.Database.BeginTransaction();
try
{
_context.SaveChanges();
ctxTransaction.Commit();
}
catch (Exception)
{
ctxTransaction.Rollback();
throw;
}
}
Here, if there is already a transaction, the REN Helper will only use SaveChanges method. But if there is no transaction created, then REN creates an inner transaction and disposes as soon as the transaction fulfills it's purpose.
Since the new SaveChanges method is being investigated, let's see how to use transactions within RENUnitOfWork:
[Route("api/[controller]")]
[ApiController]
public class HomeController : ControllerBase
{
private readonly IRENRepository<User> _userRepository;
private readonly IRENRepository<Side> _sideRepository;
private readonly IRENUnitOfWork<RENDbContext> _uow;
public HomeController(IRENUnitOfWork<RENDbContext> uow)
{
_sideRepository = uow.GetRENRepository<Side>();
_userRepository = uow.GetRENRepository<User>();
_uow = uow;
}
[HttpPost]
[Route("InsertWithTransaction")]
public async Task<IActionResult> InsertWithTransaction([FromQuery] int size)
{
var sideIds = await _sideRepository
.GetQueryable()
.Select(_ => _.Id)
.ToListAsync();
var userFaker = new Faker<User>()
.RuleFor(u => u.Name, f => f.Name.FirstName())
.RuleFor(u => u.Surname, f => f.Name.LastName())
.RuleFor(u => u.SideId, f => f.PickRandom(sideIds));
var users = userFaker.Generate(size);
_uow.BeginTransaction();
try
{
await _userRepository.InsertAsync(users);
await _uow.SaveChangesAsync();
await _sideRepository.InsertAsync(new Side { Name = "My Side" });
await _uow.SaveChangesAsync();
await _uow.CommitTransactionAsync();
}
catch (Exception)
{
await _uow.RollbackTransactionAsync();
return BadRequest();
}
return Ok();
}
}
Here, a new transaction will be created with:
_uow.BeginTransaction();
This method creates a new transaction and assigns it to the transaction object within the REN Unit Of Work object.
Since there is a transaction created and not finished yet, the code;
await _uow.SaveChangesAsync();
does not create new inner transaction.
await _uow.CommitTransactionAsync();
and
await _uow.RollbackTransactionAsync();
finalizes the transaction operation via committing or rollbacking the changes on the database. These operations disposes the transaction object which allows to create new transactions or, create inner transaction while saving changes via SaveChanges() methods.
Last updated