using GerstITS.Data;
using GerstITS.Examples.Transactions.Entities;
namespace GerstITS.Examples.Transactions;
///
/// Demonstrates the transaction API:
///
/// - legacy auto-commit behaviour when no transaction is started,
/// - a successful transaction that spans a Save and a Delete,
/// - an automatic rollback on exception that discards partial work,
/// - the guard that prevents starting a second, nested transaction.
///
///
internal sealed class TransactionRunner
{
#region Fields
private readonly IRepository _repository;
private readonly ProductDbContext _dbContext;
#endregion
#region Constructors
public TransactionRunner(IRepository repository,
ProductDbContext dbContext)
{
_repository = repository;
_dbContext = dbContext;
}
#endregion
#region Methods
public void Run()
{
// The SQLite in-memory database starts out empty, so create the schema once up front.
_dbContext.Database.EnsureCreated();
RunAutoCommitBehavior();
RunSuccessfulTransaction();
RunRollbackOnException();
RunNestedTransactionGuard();
}
private void RunAutoCommitBehavior()
{
Print("=== Legacy auto-commit (no BeginTransaction) ===");
// Without BeginTransaction() every Save/Delete auto-commits immediately (unchanged legacy behaviour).
_repository.Save(new ProductEntity { Name = "Obsolete Widget", Price = 9.99m });
Print($"Products after auto-committed insert: {NamesInStore()}");
}
private void RunSuccessfulTransaction()
{
Print(string.Empty);
Print("=== Successful transaction spanning a Save and a Delete ===");
var obsolete = _repository.Query()
.Single(product => product.Name == "Obsolete Widget");
using (var transaction = _repository.BeginTransaction())
{
_repository.Save(new ProductEntity { Name = "New Gadget", Price = 19.99m });
_repository.Delete(obsolete);
transaction.Commit();
}
Print($"Products after committed transaction: {NamesInStore()}");
}
private void RunRollbackOnException()
{
Print(string.Empty);
Print("=== Rollback on exception (partial work is discarded) ===");
try
{
using var transaction = _repository.BeginTransaction();
_repository.Save(new ProductEntity { Name = "Half-Baked Product", Price = 1.00m });
// Something fails before Commit() is reached. Because the transaction is disposed without a
// prior Commit(), the framework rolls it back automatically - so the insert above is undone.
throw new InvalidOperationException("Simulated failure after partial work.");
}
catch (InvalidOperationException exception)
{
Print($"Caught expected exception: {exception.Message}");
}
Print($"Products after rolled-back transaction: {NamesInStore()} (the half-baked product was discarded)");
}
private void RunNestedTransactionGuard()
{
Print(string.Empty);
Print("=== Starting a second transaction while one is active throws ===");
using var transaction = _repository.BeginTransaction();
try
{
_repository.BeginTransaction();
}
catch (InvalidOperationException exception)
{
Print($"Caught expected exception: {exception.Message}");
}
transaction.Rollback();
}
private string NamesInStore()
{
var names = _repository.Query()
.OrderBy(product => product.Name)
.Select(product => product.Name)
.ToList();
return names.Count == 0
? ""
: string.Join(", ", names);
}
private static void Print(string message)
{
Console.WriteLine(message);
}
#endregion
}