diff --git a/Directory.Packages.props b/Directory.Packages.props
index 54bc829..c6b8683 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,42 +6,46 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
diff --git a/GerstITS.Examples.JobDashboard/Configurations/ReportGenerationJobConfiguration.cs b/GerstITS.Examples.JobDashboard/Configurations/ReportGenerationJobConfiguration.cs
new file mode 100644
index 0000000..e09409c
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Configurations/ReportGenerationJobConfiguration.cs
@@ -0,0 +1,16 @@
+using GerstITS.Job.Scheduling;
+using Microsoft.Extensions.Configuration;
+
+namespace GerstITS.Examples.JobDashboard.Configurations;
+
+public sealed class ReportGenerationJobConfiguration : JobSchedulingConfigurationBase
+{
+ #region Constructors
+
+ public ReportGenerationJobConfiguration(IConfiguration configuration)
+ : base(configuration)
+ {
+ }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/Data/InMemoryDatabaseConfigurator.cs b/GerstITS.Examples.JobDashboard/Data/InMemoryDatabaseConfigurator.cs
new file mode 100644
index 0000000..1609bde
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Data/InMemoryDatabaseConfigurator.cs
@@ -0,0 +1,44 @@
+using GerstITS.Data.EntityFramework;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+
+namespace GerstITS.Examples.JobDashboard.Data;
+
+///
+/// Configures to use the EF Core InMemory provider
+/// (UseInMemoryDatabase) so the example runs with zero external database dependencies.
+///
+/// All contexts share the same named in-memory database, so records written by one job execution are
+/// visible to the next. No migrations are involved - the InMemory provider creates the model on demand.
+///
+public sealed class InMemoryDatabaseConfigurator : DatabaseConfiguratorBase
+{
+ #region Constants
+
+ private const string DatabaseName = "GerstITS.Examples.JobRuns";
+
+ #endregion
+
+ #region Constructors
+
+ public InMemoryDatabaseConfigurator(IConfiguration configuration)
+ : base(configuration)
+ {
+ }
+
+ #endregion
+
+ #region DatabaseConfiguratorBase
+
+ public override void ConfigureModelCreating(ModelBuilder modelBuilder)
+ {
+ // JobRunEntity is mapped by convention; the InMemory provider needs no relational configuration.
+ }
+
+ protected override void ConfigureDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
+ {
+ optionsBuilder.UseInMemoryDatabase(DatabaseName);
+ }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/Data/JobRunDbContext.cs b/GerstITS.Examples.JobDashboard/Data/JobRunDbContext.cs
new file mode 100644
index 0000000..0e1de2f
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Data/JobRunDbContext.cs
@@ -0,0 +1,25 @@
+using GerstITS.Data.EntityFramework;
+using Microsoft.EntityFrameworkCore;
+
+namespace GerstITS.Examples.JobDashboard.Data;
+
+public sealed class JobRunDbContext : EntityFrameworkDbContextBase, IDbContext
+{
+ #region Properties
+
+ public DbSet JobRuns => Set();
+
+ protected override string ConnectionStringName => "JobRunDbContext";
+
+ #endregion
+
+ #region Constructors
+
+ public JobRunDbContext(IDatabaseConfigurator databaseConfigurator,
+ DbContextOptions dbContextOptions)
+ : base(databaseConfigurator, dbContextOptions)
+ {
+ }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/Data/JobRunEntity.cs b/GerstITS.Examples.JobDashboard/Data/JobRunEntity.cs
new file mode 100644
index 0000000..ae69f41
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Data/JobRunEntity.cs
@@ -0,0 +1,16 @@
+using GerstITS.Data;
+
+namespace GerstITS.Examples.JobDashboard.Data;
+
+public sealed class JobRunEntity : EntityBase
+{
+ #region Properties
+
+ public string JobName { get; set; }
+ public DateTimeOffset StartedAt { get; set; }
+ public long DurationMilliseconds { get; set; }
+ public bool Success { get; set; }
+ public string Error { get; set; }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/Data/NoDatabaseMigrationConfiguration.cs b/GerstITS.Examples.JobDashboard/Data/NoDatabaseMigrationConfiguration.cs
new file mode 100644
index 0000000..d3036e2
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Data/NoDatabaseMigrationConfiguration.cs
@@ -0,0 +1,17 @@
+using GerstITS.Data.EntityFramework;
+
+namespace GerstITS.Examples.JobDashboard.Data;
+
+///
+/// The EF Core module registers an auto-migration startup task that requires an
+/// . The InMemory provider has no migrations, so
+/// this implementation simply disables auto-migration.
+///
+public sealed class NoDatabaseMigrationConfiguration : IEntityFrameworkMigrationConfiguration
+{
+ #region IEntityFrameworkMigrationConfiguration
+
+ public bool AutoMigrate => false;
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/GerstITS.Examples.JobDashboard.csproj b/GerstITS.Examples.JobDashboard/GerstITS.Examples.JobDashboard.csproj
new file mode 100644
index 0000000..3b2ec2c
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/GerstITS.Examples.JobDashboard.csproj
@@ -0,0 +1,49 @@
+
+
+
+ Gerst ITS
+ Gerst ITS
+ © 2025 Gerst ITS
+ Gerst ITS Examples Job Dashboard (in-memory)
+ Example hosting the Job Dashboard and Quartz scheduling with an EF Core in-memory store and no external database.
+ 0.0.0.0
+ 0.0.0.0
+ 0.0.0.0
+ 0.0.0.0
+
+
+
+ true
+ true
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GerstITS.Examples.JobDashboard/Jobs/ReportGenerationJob.cs b/GerstITS.Examples.JobDashboard/Jobs/ReportGenerationJob.cs
new file mode 100644
index 0000000..10dc286
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Jobs/ReportGenerationJob.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics;
+using GerstITS.Data;
+using GerstITS.Examples.JobDashboard.Configurations;
+using GerstITS.Examples.JobDashboard.Data;
+using GerstITS.Job;
+using Quartz;
+
+namespace GerstITS.Examples.JobDashboard.Jobs;
+
+///
+/// A scheduled job that, on every run, persists a run record to the EF Core InMemory store through the
+/// framework's (wired via AddEntityFrameworkSession). Every third run fails so
+/// the Job Dashboard shows a mix of successful and failed executions.
+///
+[DisallowConcurrentExecution]
+public sealed class ReportGenerationJob : JobBase
+{
+ #region Fields
+
+ private static int _runCounter;
+
+ private readonly ReportGenerationJobConfiguration _configuration;
+ private readonly IRepository _repository;
+
+ #endregion
+
+ #region Constructors
+
+ public ReportGenerationJob(ReportGenerationJobConfiguration configuration,
+ IRepository repository)
+ {
+ _configuration = configuration;
+ _repository = repository;
+ }
+
+ #endregion
+
+ #region Methods
+
+ protected override void Execute()
+ {
+ var runNumber = Interlocked.Increment(ref _runCounter);
+ var startedAt = DateTimeOffset.UtcNow;
+ var stopwatch = Stopwatch.StartNew();
+
+ // Simulate some work.
+ Thread.Sleep(250);
+ stopwatch.Stop();
+
+ var success = runNumber % 3 != 0;
+ var error = success
+ ? null
+ : $"Report generation run #{runNumber} failed (simulated).";
+
+ PersistRun(startedAt, stopwatch.ElapsedMilliseconds, success, error);
+
+ if (!success)
+ throw new InvalidOperationException(error);
+ }
+
+ private void PersistRun(DateTimeOffset startedAt, long durationMilliseconds, bool success, string error)
+ {
+ _repository.Save(new JobRunEntity
+ {
+ JobName = _configuration.Name,
+ StartedAt = startedAt,
+ DurationMilliseconds = durationMilliseconds,
+ Success = success,
+ Error = error
+ });
+
+ var storedRuns = _repository.Query()
+ .Count();
+
+ Console.WriteLine($"---> [EF Core InMemory] persisted run of '{_configuration.Name}' (success: {success}); total runs stored: {storedRuns}");
+ }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/Module.cs b/GerstITS.Examples.JobDashboard/Module.cs
new file mode 100644
index 0000000..29b0bc9
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Module.cs
@@ -0,0 +1,44 @@
+using GerstITS.Data.EntityFramework;
+using GerstITS.Examples.JobDashboard.Configurations;
+using GerstITS.Examples.JobDashboard.Data;
+using GerstITS.Examples.JobDashboard.Jobs;
+using GerstITS.IoC;
+using GerstITS.Job.Scheduling;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace GerstITS.Examples.JobDashboard;
+
+public sealed class Module : IIoCModule
+{
+ #region IIoCModule
+
+ public void RegisterComponents(IServiceCollection container)
+ {
+ RegisterInMemoryStore(container);
+ RegisterJobs(container);
+ }
+
+ #endregion
+
+ #region Methods
+
+ private static void RegisterInMemoryStore(IServiceCollection container)
+ {
+ // The InMemory provider has no migrations, so disable the framework's auto-migration startup task.
+ container.AddSingleton();
+
+ // Wire the EF Core InMemory provider through the framework's session (unit of work). No real database.
+ container.AddScoped();
+ container.AddTransient(_ => new DbContextOptionsBuilder().Options);
+ container.AddScoped();
+ container.AddEntityFrameworkSession();
+ }
+
+ private static void RegisterJobs(IServiceCollection container)
+ {
+ container.RegisterJob(IgnoreConditions.EntityFramework, IgnoreConditions.Swagger);
+ }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/Program.cs b/GerstITS.Examples.JobDashboard/Program.cs
new file mode 100644
index 0000000..b6f4fe5
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Program.cs
@@ -0,0 +1,30 @@
+using GerstITS.Job.Dashboard.Web;
+using GerstITS.Web.Api;
+using GerstITS.Web.Api.Hosting;
+using Serilog;
+
+namespace GerstITS.Examples.JobDashboard;
+
+public class Program
+{
+ #region Methods
+
+ public static void Main(string[] args)
+ {
+ HostingStartup.Use(args)
+ .Host()
+ .UseSerilog((context, configuration) => configuration.ReadFrom.Configuration(context.Configuration))
+ .Services()
+ .Build()
+ .IfDevelopment(app => app.UseDeveloperExceptionPage())
+ .UseAuthentication()
+ .UseAuthorization()
+ .UseJobDashboard()
+ .UseRouting()
+ .UseEndpoints(endpoints => endpoints.MapControllers())
+ .UseSerilogRequestLogging()
+ .Run();
+ }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.JobDashboard/Properties/launchSettings.json b/GerstITS.Examples.JobDashboard/Properties/launchSettings.json
new file mode 100644
index 0000000..a5d32d1
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "profiles": {
+ "GerstITS.Examples.JobDashboard": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "jobs",
+ "applicationUrl": "http://localhost:5080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/GerstITS.Examples.JobDashboard/appsettings.json b/GerstITS.Examples.JobDashboard/appsettings.json
new file mode 100644
index 0000000..c6a4678
--- /dev/null
+++ b/GerstITS.Examples.JobDashboard/appsettings.json
@@ -0,0 +1,51 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*",
+ "Jobs": {
+ "Dashboard": {
+ "Title": "Gerst ITS Examples - Job Dashboard (in-memory)",
+ "DefaultTheme": "Dark",
+ "RequireAuthenticatedUser": true,
+ "Users": [
+ {
+ "Username": "admin",
+ "Password": "I33bgHWHF3VrbFhT"
+ }
+ ]
+ },
+ "ReportGenerationJob": {
+ "CronExpression": "0/15 * * * * ?",
+ "Name": "Report Generation"
+ },
+ "SayHelloWorldJob": {
+ "CronExpression": "0/30 * * * * ?",
+ "Name": "Say Hello World"
+ },
+ "SayHelloWorldWithDefaultNameUsageJob": {
+ "CronExpression": "0/30 * * * * ?"
+ }
+ },
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "===> {Timestamp:HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"
+ }
+ }
+ ]
+ }
+}
diff --git a/GerstITS.Examples.Transactions/Entities/ProductEntity.cs b/GerstITS.Examples.Transactions/Entities/ProductEntity.cs
new file mode 100644
index 0000000..1b77d61
--- /dev/null
+++ b/GerstITS.Examples.Transactions/Entities/ProductEntity.cs
@@ -0,0 +1,13 @@
+using GerstITS.Data;
+
+namespace GerstITS.Examples.Transactions.Entities;
+
+public sealed class ProductEntity : EntityBase
+{
+ #region Properties
+
+ public string Name { get; set; }
+ public decimal Price { get; set; }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.Transactions/GerstITS.Examples.Transactions.csproj b/GerstITS.Examples.Transactions/GerstITS.Examples.Transactions.csproj
new file mode 100644
index 0000000..c796908
--- /dev/null
+++ b/GerstITS.Examples.Transactions/GerstITS.Examples.Transactions.csproj
@@ -0,0 +1,61 @@
+
+
+
+ Gerst ITS
+ Gerst ITS
+ © 2025 Gerst ITS
+ Gerst ITS Examples transaction console application
+ Example demonstrating the IRepository transaction API (BeginTransaction / Commit / Rollback).
+ 0.0.0.0
+ 0.0.0.0
+ 0.0.0.0
+ 0.0.0.0
+
+
+
+ Exe
+
+
+
+ true
+ true
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GerstITS.Examples.Transactions/Migrations/SqliteInMemoryDatabaseConfigurator.cs b/GerstITS.Examples.Transactions/Migrations/SqliteInMemoryDatabaseConfigurator.cs
new file mode 100644
index 0000000..3ba4b07
--- /dev/null
+++ b/GerstITS.Examples.Transactions/Migrations/SqliteInMemoryDatabaseConfigurator.cs
@@ -0,0 +1,64 @@
+using GerstITS.Data.EntityFramework;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+
+namespace GerstITS.Examples.Transactions.Migrations;
+
+///
+/// Configures 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 (UseInMemoryDatabase) is deliberately NOT used here: it does
+/// not support transactions at all - Database.BeginTransaction() raises the
+/// TransactionIgnoredWarning - so it cannot demonstrate an atomic rollback. In production the
+/// same code runs unchanged against PostgreSQL or SQL Server.
+///
+/// A single is opened and kept open for the lifetime of the process so
+/// the in-memory database is shared across every instance (an in-memory
+/// SQLite database only lives as long as its connection is open).
+///
+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
+}
diff --git a/GerstITS.Examples.Transactions/ProductDbContext.cs b/GerstITS.Examples.Transactions/ProductDbContext.cs
new file mode 100644
index 0000000..ccbdd80
--- /dev/null
+++ b/GerstITS.Examples.Transactions/ProductDbContext.cs
@@ -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 Products => Set();
+
+ protected override string ConnectionStringName => "ProductDbContext";
+
+ #endregion
+
+ #region Constructors
+
+ public ProductDbContext(IDatabaseConfigurator databaseConfigurator,
+ DbContextOptions dbContextOptions)
+ : base(databaseConfigurator, dbContextOptions)
+ {
+ }
+
+ #endregion
+}
diff --git a/GerstITS.Examples.Transactions/Program.cs b/GerstITS.Examples.Transactions/Program.cs
new file mode 100644
index 0000000..a7ff06e
--- /dev/null
+++ b/GerstITS.Examples.Transactions/Program.cs
@@ -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(),
+ scope.ServiceProvider.GetRequiredService()).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(configuration);
+
+ // A single configurator instance owns the kept-open SQLite in-memory connection for the whole run.
+ container.AddSingleton();
+ container.AddSingleton(services => services.GetRequiredService());
+
+ container.AddTransient(_ => new DbContextOptionsBuilder().Options);
+ container.AddScoped();
+ container.AddEntityFrameworkSession();
+
+ // 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
+}
diff --git a/GerstITS.Examples.Transactions/TransactionRunner.cs b/GerstITS.Examples.Transactions/TransactionRunner.cs
new file mode 100644
index 0000000..1eede9c
--- /dev/null
+++ b/GerstITS.Examples.Transactions/TransactionRunner.cs
@@ -0,0 +1,137 @@
+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
+}
diff --git a/GerstITS.Examples.Transactions/appsettings.json b/GerstITS.Examples.Transactions/appsettings.json
new file mode 100644
index 0000000..9aa8e4b
--- /dev/null
+++ b/GerstITS.Examples.Transactions/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "ConnectionStrings": {
+ "ProductDbContext": ""
+ }
+}
diff --git a/GerstITS.Examples.sln b/GerstITS.Examples.sln
index 3514e18..e9d4ff4 100644
--- a/GerstITS.Examples.sln
+++ b/GerstITS.Examples.sln
@@ -23,6 +23,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GerstITS.Examples.Data", "G
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GerstITS.Examples.Tests", "GerstITS.Examples.Tests\GerstITS.Examples.Tests.csproj", "{1F71C429-A9B3-46C2-BDEE-8F8413765EB5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GerstITS.Examples.Transactions", "GerstITS.Examples.Transactions\GerstITS.Examples.Transactions.csproj", "{1093CDD2-562F-45F6-B259-CFCCA0A557E6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GerstITS.Examples.JobDashboard", "GerstITS.Examples.JobDashboard\GerstITS.Examples.JobDashboard.csproj", "{30485DD4-0E9A-4CB2-A394-D926570314D7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -117,6 +121,30 @@ Global
{1F71C429-A9B3-46C2-BDEE-8F8413765EB5}.Release|x64.Build.0 = Release|Any CPU
{1F71C429-A9B3-46C2-BDEE-8F8413765EB5}.Release|x86.ActiveCfg = Release|Any CPU
{1F71C429-A9B3-46C2-BDEE-8F8413765EB5}.Release|x86.Build.0 = Release|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Debug|x64.Build.0 = Debug|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Debug|x86.Build.0 = Debug|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Release|x64.ActiveCfg = Release|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Release|x64.Build.0 = Release|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Release|x86.ActiveCfg = Release|Any CPU
+ {1093CDD2-562F-45F6-B259-CFCCA0A557E6}.Release|x86.Build.0 = Release|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Debug|x64.Build.0 = Debug|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Debug|x86.Build.0 = Debug|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Release|x64.ActiveCfg = Release|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Release|x64.Build.0 = Release|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Release|x86.ActiveCfg = Release|Any CPU
+ {30485DD4-0E9A-4CB2-A394-D926570314D7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE