138 lines
4.1 KiB
C#
138 lines
4.1 KiB
C#
using GerstITS.Data;
|
|
using GerstITS.Examples.Transactions.Entities;
|
|
|
|
namespace GerstITS.Examples.Transactions;
|
|
|
|
/// <summary>
|
|
/// Demonstrates the <see cref="IRepository" /> transaction API:
|
|
/// <list type="bullet">
|
|
/// <item>legacy auto-commit behaviour when no transaction is started,</item>
|
|
/// <item>a successful transaction that spans a Save and a Delete,</item>
|
|
/// <item>an automatic rollback on exception that discards partial work,</item>
|
|
/// <item>the guard that prevents starting a second, nested transaction.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
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<ProductEntity>()
|
|
.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<ProductEntity>()
|
|
.OrderBy(product => product.Name)
|
|
.Select(product => product.Name)
|
|
.ToList();
|
|
|
|
return names.Count == 0
|
|
? "<none>"
|
|
: string.Join(", ", names);
|
|
}
|
|
|
|
private static void Print(string message)
|
|
{
|
|
Console.WriteLine(message);
|
|
}
|
|
|
|
#endregion
|
|
}
|