Update to latest version and show additional features
This commit is contained in:
+35
-31
@@ -6,42 +6,46 @@
|
||||
<ItemGroup>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AutoMapper" Version="16.1.1" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
||||
<PackageVersion Include="AutoMapper" Version="16.2.0" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
|
||||
<PackageVersion Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageVersion Include="GerstITS.Authentication.ApiKey" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Authentication.OpenId" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Common" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Data" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework.PostgreSql" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework.SqlServer" Version="2026.6.3" />
|
||||
<PackageVersion Include="GerstITS.IoC" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.IoC.DotNetCore" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Job" Version="2026.6.2" />
|
||||
<PackageVersion Include="GerstITS.Job.Scheduling" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Mail" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Mapping.AutoMapper" Version="2026.6.2" />
|
||||
<PackageVersion Include="GerstITS.Search" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.System" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.TestFramework" Version="2026.7.16" />
|
||||
<PackageVersion Include="GerstITS.Validation" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Web" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Web.Api" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Web.Api.Swagger" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Web.Rest" Version="2026.7.17" />
|
||||
<PackageVersion Include="GerstITS.Authentication.ApiKey" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Authentication.OpenId" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Common" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Data" Version="2026.7.59" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework" Version="2026.7.59" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework.PostgreSql" Version="2026.7.59" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework.SqlServer" Version="2026.7.59" />
|
||||
<PackageVersion Include="GerstITS.IoC" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.IoC.DotNetCore" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Job" Version="2026.7.60" />
|
||||
<PackageVersion Include="GerstITS.Job.Dashboard" Version="2026.7.60" />
|
||||
<PackageVersion Include="GerstITS.Job.Scheduling" Version="2026.7.60" />
|
||||
<PackageVersion Include="GerstITS.Mail" Version="2026.7.56" />
|
||||
<PackageVersion Include="GerstITS.Mapping.AutoMapper" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Search" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.System" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.TestFramework" Version="2026.7.49" />
|
||||
<PackageVersion Include="GerstITS.Validation" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Web" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Web.Api" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Web.Api.Swagger" Version="2026.7.54" />
|
||||
<PackageVersion Include="GerstITS.Web.Rest" Version="2026.7.54" />
|
||||
<PackageVersion Include="Hocon.Extensions.Configuration" Version="2.0.4" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||
<PackageVersion Include="Microsoft.OpenApi" Version="2.9.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||
<PackageVersion Include="Microsoft.OpenApi" Version="3.9.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
|
||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using GerstITS.Data.EntityFramework;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace GerstITS.Examples.JobDashboard.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Configures <see cref="JobRunDbContext" /> to use the EF Core InMemory provider
|
||||
/// (<c>UseInMemoryDatabase</c>) 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
@@ -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<JobRunEntity> JobRuns => Set<JobRunEntity>();
|
||||
|
||||
protected override string ConnectionStringName => "JobRunDbContext";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public JobRunDbContext(IDatabaseConfigurator databaseConfigurator,
|
||||
DbContextOptions<JobRunDbContext> dbContextOptions)
|
||||
: base(databaseConfigurator, dbContextOptions)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using GerstITS.Data;
|
||||
|
||||
namespace GerstITS.Examples.JobDashboard.Data;
|
||||
|
||||
public sealed class JobRunEntity : EntityBase<int>
|
||||
{
|
||||
#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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using GerstITS.Data.EntityFramework;
|
||||
|
||||
namespace GerstITS.Examples.JobDashboard.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The EF Core module registers an auto-migration startup task that requires an
|
||||
/// <see cref="IEntityFrameworkMigrationConfiguration" />. The InMemory provider has no migrations, so
|
||||
/// this implementation simply disables auto-migration.
|
||||
/// </summary>
|
||||
public sealed class NoDatabaseMigrationConfiguration : IEntityFrameworkMigrationConfiguration
|
||||
{
|
||||
#region IEntityFrameworkMigrationConfiguration
|
||||
|
||||
public bool AutoMigrate => false;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<Company>Gerst ITS</Company>
|
||||
<Authors>Gerst ITS</Authors>
|
||||
<Copyright>© 2025 Gerst ITS</Copyright>
|
||||
<Product>Gerst ITS Examples Job Dashboard (in-memory)</Product>
|
||||
<Description>Example hosting the Job Dashboard and Quartz scheduling with an EF Core in-memory store and no external database.</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 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>
|
||||
<PackageReference Include="GerstITS.Data.EntityFramework" />
|
||||
<PackageReference Include="GerstITS.Job.Dashboard" />
|
||||
<PackageReference Include="GerstITS.Job.Scheduling" />
|
||||
<PackageReference Include="GerstITS.System" />
|
||||
<PackageReference Include="GerstITS.Web.Api" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GerstITS.Examples.Jobs.SayHelloWorld\GerstITS.Examples.Jobs.SayHelloWorld.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectCapability Include="ConfigurableFileNesting" />
|
||||
<ProjectCapability Include="ConfigurableFileNestingFeatureEnabled" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A scheduled job that, on every run, persists a run record to the EF Core InMemory store through the
|
||||
/// framework's <see cref="IRepository" /> (wired via AddEntityFrameworkSession). Every third run fails so
|
||||
/// the Job Dashboard shows a mix of successful and failed executions.
|
||||
/// </summary>
|
||||
[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<JobRunEntity>()
|
||||
.Count();
|
||||
|
||||
Console.WriteLine($"---> [EF Core InMemory] persisted run of '{_configuration.Name}' (success: {success}); total runs stored: {storedRuns}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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<IServiceCollection>
|
||||
{
|
||||
#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<IEntityFrameworkMigrationConfiguration, NoDatabaseMigrationConfiguration>();
|
||||
|
||||
// Wire the EF Core InMemory provider through the framework's session (unit of work). No real database.
|
||||
container.AddScoped<IDatabaseConfigurator, InMemoryDatabaseConfigurator>();
|
||||
container.AddTransient(_ => new DbContextOptionsBuilder<JobRunDbContext>().Options);
|
||||
container.AddScoped<JobRunDbContext>();
|
||||
container.AddEntityFrameworkSession<JobRunDbContext>();
|
||||
}
|
||||
|
||||
private static void RegisterJobs(IServiceCollection container)
|
||||
{
|
||||
container.RegisterJob<ReportGenerationJob, ReportGenerationJobConfiguration>(IgnoreConditions.EntityFramework, IgnoreConditions.Swagger);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profiles": {
|
||||
"GerstITS.Examples.JobDashboard": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "jobs",
|
||||
"applicationUrl": "http://localhost:5080",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user