Update to latest version and show additional features

This commit is contained in:
2026-07-29 13:25:23 +02:00
parent 25690690d0
commit e44f2e7380
20 changed files with 804 additions and 31 deletions
@@ -0,0 +1,13 @@
using GerstITS.Data;
namespace GerstITS.Examples.Transactions.Entities;
public sealed class ProductEntity : EntityBase<int>
{
#region Properties
public string Name { get; set; }
public decimal Price { get; set; }
#endregion
}
@@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Company>Gerst ITS</Company>
<Authors>Gerst ITS</Authors>
<Copyright>© 2025 Gerst ITS</Copyright>
<Product>Gerst ITS Examples transaction console application</Product>
<Description>Example demonstrating the IRepository transaction API (BeginTransaction / Commit / Rollback).</Description>
<Version>0.0.0.0</Version>
<AssemblyVersion>0.0.0.0</AssemblyVersion>
<AssemblyInformationalVersion>0.0.0.0</AssemblyInformationalVersion>
<FileVersion>0.0.0.0</FileVersion>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<WarningsAsErrors />
<NoWarn />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<WarningsAsErrors />
<NoWarn />
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="GerstITS.Data" />
<PackageReference Include="GerstITS.Data.EntityFramework" />
<PackageReference Include="GerstITS.IoC" />
<PackageReference Include="GerstITS.System" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<!-- Pin the native SQLite bundle to 2.1.12 to pick up the fix for GHSA-2m69-gcr7-jv3q. -->
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
</ItemGroup>
<ItemGroup>
<ProjectCapability Include="ConfigurableFileNesting" />
<ProjectCapability Include="ConfigurableFileNestingFeatureEnabled" />
</ItemGroup>
</Project>
@@ -0,0 +1,64 @@
using GerstITS.Data.EntityFramework;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace GerstITS.Examples.Transactions.Migrations;
/// <summary>
/// Configures <see cref="ProductDbContext" /> to use a self-contained SQLite in-memory database.
///
/// SQLite is a RELATIONAL provider and therefore supports real ACID transactions, which is what the
/// transaction example needs to show an atomic rollback that actually discards partial work.
///
/// The EF Core InMemory provider (<c>UseInMemoryDatabase</c>) is deliberately NOT used here: it does
/// not support transactions at all - <c>Database.BeginTransaction()</c> raises the
/// <c>TransactionIgnoredWarning</c> - so it cannot demonstrate an atomic rollback. In production the
/// same code runs unchanged against PostgreSQL or SQL Server.
///
/// A single <see cref="SqliteConnection" /> is opened and kept open for the lifetime of the process so
/// the in-memory database is shared across every <see cref="ProductDbContext" /> instance (an in-memory
/// SQLite database only lives as long as its connection is open).
/// </summary>
public sealed class SqliteInMemoryDatabaseConfigurator : DatabaseConfiguratorBase, IDisposable
{
#region Fields
private readonly SqliteConnection _connection;
#endregion
#region Constructors
public SqliteInMemoryDatabaseConfigurator(IConfiguration configuration)
: base(configuration)
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
}
#endregion
#region DatabaseConfiguratorBase
public override void ConfigureModelCreating(ModelBuilder modelBuilder)
{
// No relational-specific mapping is required; ProductEntity is mapped by convention.
}
protected override void ConfigureDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{
optionsBuilder.UseSqlite(_connection);
}
#endregion
#region IDisposable
public void Dispose()
{
_connection.Dispose();
}
#endregion
}
@@ -0,0 +1,26 @@
using GerstITS.Data.EntityFramework;
using GerstITS.Examples.Transactions.Entities;
using Microsoft.EntityFrameworkCore;
namespace GerstITS.Examples.Transactions;
public sealed class ProductDbContext : EntityFrameworkDbContextBase, IDbContext
{
#region Properties
public DbSet<ProductEntity> Products => Set<ProductEntity>();
protected override string ConnectionStringName => "ProductDbContext";
#endregion
#region Constructors
public ProductDbContext(IDatabaseConfigurator databaseConfigurator,
DbContextOptions<ProductDbContext> dbContextOptions)
: base(databaseConfigurator, dbContextOptions)
{
}
#endregion
}
+50
View File
@@ -0,0 +1,50 @@
using GerstITS.Data;
using GerstITS.Data.EntityFramework;
using GerstITS.Examples.Transactions.Migrations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace GerstITS.Examples.Transactions;
internal class Program
{
#region Methods
private static void Main(string[] args)
{
using var provider = BuildProvider();
using var scope = provider.CreateScope();
new TransactionRunner(scope.ServiceProvider.GetRequiredService<IRepository>(),
scope.ServiceProvider.GetRequiredService<ProductDbContext>()).Run();
}
private static ServiceProvider BuildProvider()
{
var configuration = new ConfigurationBuilder().SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddJsonFile("appsettings.json", true, true)
.Build();
var container = new ServiceCollection();
container.AddSingleton<IConfiguration>(configuration);
// A single configurator instance owns the kept-open SQLite in-memory connection for the whole run.
container.AddSingleton<SqliteInMemoryDatabaseConfigurator>();
container.AddSingleton<IDatabaseConfigurator>(services => services.GetRequiredService<SqliteInMemoryDatabaseConfigurator>());
container.AddTransient(_ => new DbContextOptionsBuilder<ProductDbContext>().Options);
container.AddScoped<ProductDbContext>();
container.AddEntityFrameworkSession<ProductDbContext>();
// Provide the repository, sessions, bulk operations and interceptors from the framework.
new GerstITS.System.Module().RegisterComponents(container);
new GerstITS.Data.Module().RegisterComponents(container);
new GerstITS.Data.EntityFramework.Module().RegisterComponents(container);
return container.BuildServiceProvider();
}
#endregion
}
@@ -0,0 +1,137 @@
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
}
@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"ProductDbContext": ""
}
}