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 }