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,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
}
+44
View File
@@ -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
}
+30
View File
@@ -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}"
}
}
]
}
}