From e44f2e7380404689b938b2a5734126c198b2349d Mon Sep 17 00:00:00 2001 From: Daniel Gerst Date: Wed, 29 Jul 2026 13:25:23 +0200 Subject: [PATCH] Update to latest version and show additional features --- Directory.Packages.props | 66 +++++---- .../ReportGenerationJobConfiguration.cs | 16 ++ .../Data/InMemoryDatabaseConfigurator.cs | 44 ++++++ .../Data/JobRunDbContext.cs | 25 ++++ .../Data/JobRunEntity.cs | 16 ++ .../Data/NoDatabaseMigrationConfiguration.cs | 17 +++ .../GerstITS.Examples.JobDashboard.csproj | 49 +++++++ .../Jobs/ReportGenerationJob.cs | 79 ++++++++++ GerstITS.Examples.JobDashboard/Module.cs | 44 ++++++ GerstITS.Examples.JobDashboard/Program.cs | 30 ++++ .../Properties/launchSettings.json | 14 ++ .../appsettings.json | 51 +++++++ .../Entities/ProductEntity.cs | 13 ++ .../GerstITS.Examples.Transactions.csproj | 61 ++++++++ .../SqliteInMemoryDatabaseConfigurator.cs | 64 ++++++++ .../ProductDbContext.cs | 26 ++++ GerstITS.Examples.Transactions/Program.cs | 50 +++++++ .../TransactionRunner.cs | 137 ++++++++++++++++++ .../appsettings.json | 5 + GerstITS.Examples.sln | 28 ++++ 20 files changed, 804 insertions(+), 31 deletions(-) create mode 100644 GerstITS.Examples.JobDashboard/Configurations/ReportGenerationJobConfiguration.cs create mode 100644 GerstITS.Examples.JobDashboard/Data/InMemoryDatabaseConfigurator.cs create mode 100644 GerstITS.Examples.JobDashboard/Data/JobRunDbContext.cs create mode 100644 GerstITS.Examples.JobDashboard/Data/JobRunEntity.cs create mode 100644 GerstITS.Examples.JobDashboard/Data/NoDatabaseMigrationConfiguration.cs create mode 100644 GerstITS.Examples.JobDashboard/GerstITS.Examples.JobDashboard.csproj create mode 100644 GerstITS.Examples.JobDashboard/Jobs/ReportGenerationJob.cs create mode 100644 GerstITS.Examples.JobDashboard/Module.cs create mode 100644 GerstITS.Examples.JobDashboard/Program.cs create mode 100644 GerstITS.Examples.JobDashboard/Properties/launchSettings.json create mode 100644 GerstITS.Examples.JobDashboard/appsettings.json create mode 100644 GerstITS.Examples.Transactions/Entities/ProductEntity.cs create mode 100644 GerstITS.Examples.Transactions/GerstITS.Examples.Transactions.csproj create mode 100644 GerstITS.Examples.Transactions/Migrations/SqliteInMemoryDatabaseConfigurator.cs create mode 100644 GerstITS.Examples.Transactions/ProductDbContext.cs create mode 100644 GerstITS.Examples.Transactions/Program.cs create mode 100644 GerstITS.Examples.Transactions/TransactionRunner.cs create mode 100644 GerstITS.Examples.Transactions/appsettings.json 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