Skip to content

Initial draft - #20

Merged
viceroypenguin merged 14 commits into
masterfrom
initial-draft
Jul 27, 2026
Merged

Initial draft#20
viceroypenguin merged 14 commits into
masterfrom
initial-draft

Conversation

@viceroypenguin

@viceroypenguin viceroypenguin commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added a compile-time generated background job scheduler for enqueueing, delayed execution, retries, queues, concurrency limits, and recurring schedules.
    • Added an interactive monitoring dashboard with live job status, history charts, server health, retries, deletion, and recurring-job controls.
    • Added in-memory, single-server, and durable Entity Framework Core storage options.
    • Added request-context propagation, health checks, telemetry, NodaTime support, and Native AOT compatibility.
    • Added Aspire, basic, and Native AOT sample applications.
  • Documentation

    • Expanded setup, operations, monitoring API, context propagation, and architecture documentation.
  • Tests

    • Added broad functional, dashboard, storage, generator, analyzer, and integration coverage.

@viceroypenguin
viceroypenguin requested a review from Copilot July 27, 2026 19:49
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR introduces the Immediate.Jobs platform, including source-generated job scheduling, analyzers, in-memory and EF Core storage, context propagation, dashboard APIs and UI, NodaTime/testing integrations, samples, benchmarks, documentation, CI updates, and broad functional test coverage.

Immediate.Jobs platform

Layer / File(s) Summary
Repository build and delivery infrastructure
.editorconfig, .github/**, Directory.*, Immediate.Jobs.slnx, coverage-settings.xml, dotnet-tools.json, global.json, .gitignore
Expands framework/package support, updates CI and release workflows, adds coverage and tooling configuration, reorganizes the solution, and updates repository rules.
Documentation and product specification
spec.md, docs/**, readme.md
Documents job definition, scheduling, storage, context propagation, monitoring, diagnostics, observability, testing, and delivery defaults.
Compile-time discovery and diagnostics
src/Common/**, src/Immediate.Jobs.Analyzers/**, tests/Immediate.Jobs.Tests/AnalyzerTests/**
Adds Roslyn discovery and validation utilities, IJOB001–IJOB014 diagnostics, analyzer implementation, and analyzer tests.
Source generator pipeline and emitted code
src/Immediate.Jobs.Generators/**, tests/Immediate.Jobs.Tests/GeneratorTests/**
Adds incremental job and registration generation, JSON metadata emission, Scriban templates, generated schedulers/invokers, and snapshot/incrementality tests.
Shared runtime contracts and scheduler
src/Immediate.Jobs.Shared/**, src/Immediate.Jobs/Immediate.Jobs.csproj
Adds job models, scheduling contracts, serializers, context envelopes, hosted execution, storage modes, in-memory and single-server storage, telemetry, health checks, and package composition.
EF Core persistence provider
src/Immediate.Jobs.EntityFrameworkCore/**, tests/Immediate.Jobs.FunctionalTests/Storage/**
Adds EF Core mappings, durable storage operations, recurring materialization, optimistic concurrency, recovery, and persistence tests.
NodaTime and testing packages
src/Immediate.Jobs.NodaTime/**, src/Immediate.Jobs.Testing/**
Adds NodaTime scheduling/serialization APIs and deterministic capture/harness testing utilities.
Dashboard API and client
src/Immediate.Jobs.Dashboard/**
Adds embedded SPA assets, monitoring routes, SSE state streaming, job and recurring actions, authorization behavior, and Svelte dashboard views.
Samples and benchmark suite
samples/**, benchmarks/**, BenchmarkDotNet.Artifacts/**
Adds Basic, NativeAot, and Aspire samples plus enqueue, startup, and dispatch comparisons with benchmark reports.
Functional and generator validation
tests/Immediate.Jobs.FunctionalTests/**, tests/Immediate.Jobs.Tests/**
Adds end-to-end coverage for generated execution, context propagation, queue limits, storage behavior, dashboard APIs, package integrations, and generator output.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: copilot

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GeneratedScheduler
  participant IJobStorage
  participant JobSchedulerService
  participant GeneratedInvoker
  participant HandlerPipeline
  Client->>GeneratedScheduler: Enqueue payload
  GeneratedScheduler->>IJobStorage: Persist JobRecord
  JobSchedulerService->>IJobStorage: Acquire due job
  JobSchedulerService->>GeneratedInvoker: Invoke job execution
  GeneratedInvoker->>HandlerPipeline: Restore context and dispatch handler
  HandlerPipeline-->>JobSchedulerService: Complete or fail execution
  JobSchedulerService->>IJobStorage: Update job state
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague and generic to describe the changes in this pull request. Replace it with a concise title that names the main change, such as adding job scheduling, dashboard, and storage support.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch initial-draft

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces the initial end-to-end “Immediate.Jobs” implementation: a source-generator-driven background job scheduler with runtime library (shared contracts + scheduler), optional integration packages (Dashboard, EF Core, NodaTime, Testing), plus samples, benchmarks, and CI/release automation updates.

Changes:

  • Add core runtime APIs (storage abstraction, schedulers, context propagation, telemetry, health checks) and a Roslyn generator/analyzer pair to emit typed schedulers/invokers/registrations.
  • Add optional packages: Dashboard (embedded SPA + monitoring API), EntityFrameworkCore storage provider, NodaTime serializer/overloads, and a deterministic testing harness + test doubles.
  • Add tests (unit + functional), samples (Basic, NativeAOT, Aspire), benchmarks, and update build/release workflows (Node dashboard build/test, coverage, package validation, OIDC NuGet publish).

Reviewed changes

Copilot reviewed 131 out of 133 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Immediate.Jobs.Tests/ModuleInitializer.cs Initializes Verify settings for snapshot-based generator tests.
tests/Immediate.Jobs.Tests/Immediate.Jobs.Tests.csproj Adds generator/analyzer test project with Verify + Roslyn references across TFMs.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net8.0#IJOB.ServiceCollectionExtensions.g.verified.cs Adds net8 snapshot for generated DI registrations with queues/tags.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net9.0#IJOB.ServiceCollectionExtensions.g.verified.cs Adds net9 snapshot for generated DI registrations with queues/tags.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net10.0#IJOB.ServiceCollectionExtensions.g.verified.cs Adds net10 snapshot for generated DI registrations with queues/tags.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net11.0#IJOB.ServiceCollectionExtensions.g.verified.cs Adds net11 snapshot for generated DI registrations with queues/tags.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PlainRequestGeneratesJobWithoutJobDetailsAssignment#IJOB.global..PlainRequestJob.g.verified.cs Adds snapshot for a payload job without job-details assignment behavior.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PayloadlessJobWithoutCronGeneratesDynamicRecurringScheduler#IJOB.global..TenantCleanupJob.g.verified.cs Adds snapshot for payloadless dynamic recurring scheduler generation.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.JobWithoutContextDoesNotEmitCaptureOrRestoreCode#IJOB.global..PlainJob.g.verified.cs Adds snapshot verifying no context capture/restore code when unused.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.InvokerDelegatesExecutionToImmediateHandlersPipeline#IJOB.global..WorkJob.g.verified.cs Adds snapshot verifying invoker/handler wiring.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ExplicitJobDetailsOnValueTypeUsesConstrainedByReferenceAssignment#IJOB.global..StructJob.g.verified.cs Adds snapshot covering job-details assignment on structs via constrained ref assignment.
tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.CronJobGeneratesPayloadlessRecurringScheduler#IJOB.global..CleanupSessionsJob.g.verified.cs Adds snapshot for cron-driven payloadless recurring trigger generation.
tests/Immediate.Jobs.FunctionalTests/Storage/QueueStorageTests.cs Adds functional tests for context persistence and queue/job capacity acquisition ordering.
tests/Immediate.Jobs.FunctionalTests/QueueSchedulerTests.cs Adds functional test verifying queue/job concurrency limits are applied before dispatch.
tests/Immediate.Jobs.FunctionalTests/Packages/TestingPackageTests.cs Adds functional coverage for the Testing package harness and capture scheduler.
tests/Immediate.Jobs.FunctionalTests/Packages/NodaTimeTests.cs Adds functional coverage for NodaTime serializer and scheduling overloads.
tests/Immediate.Jobs.FunctionalTests/Packages/DashboardPackageTests.cs Adds functional coverage for dashboard assets, routing, SSE, and job API behavior.
tests/Immediate.Jobs.FunctionalTests/Immediate.Jobs.FunctionalTests.csproj Adds functional test project referencing runtime + packages and analyzers/generator.
src/Immediate.Jobs/Immediate.Jobs.csproj Updates packaging to embed shared runtime + analyzers/generator payloads and NuGet metadata.
src/Immediate.Jobs.Testing/JobTestHarness.cs Adds deterministic harness for in-memory storage + fake clock without background threads.
src/Immediate.Jobs.Testing/JobTestAssertionException.cs Adds custom assertion exception type for harness diagnostics.
src/Immediate.Jobs.Testing/Immediate.Jobs.Testing.csproj Adds packable Testing package project configuration.
src/Immediate.Jobs.Testing/CaptureOnlyRecurringJobScheduler.cs Adds recurring scheduler test double that captures calls.
src/Immediate.Jobs.Testing/CaptureOnlyJobScheduler.cs Adds typed scheduler test double that captures enqueue/schedule calls.
src/Immediate.Jobs.Shared/ServiceCollectionExtensions.cs Adds runtime DI registration entrypoint + health check extension.
src/Immediate.Jobs.Shared/QueueAttributes.cs Adds queue definition + job-to-queue assignment attributes.
src/Immediate.Jobs.Shared/JobTelemetry.cs Adds Activity/Metrics instrumentation primitives (OpenTelemetry-friendly).
src/Immediate.Jobs.Shared/JobSchedulers.cs Adds scheduler interfaces + JobScheduler base + cron/time-zone helpers.
src/Immediate.Jobs.Shared/JobExecution.cs Adds execution boundary types and serializer abstraction + default STJ serializer.
src/Immediate.Jobs.Shared/JobContextPropagation.cs Adds durable context propagation abstraction + envelope helpers.
src/Immediate.Jobs.Shared/JobAttribute.cs Adds [Job] attribute contract and enums for overlap/backoff policies.
src/Immediate.Jobs.Shared/ImmediateJobsOptions.cs Adds options for storage topology and validation + builder wrapper.
src/Immediate.Jobs.Shared/ImmediateJobsHealthCheck.cs Adds health check combining storage connectivity + scheduler heartbeat.
src/Immediate.Jobs.Shared/Immediate.Jobs.Shared.csproj Adds Shared runtime project definition and dependencies.
src/Immediate.Jobs.Shared/IJobStorage.cs Defines durable storage contract + replica capability for single-server mode.
src/Immediate.Jobs.Shared/IJobRequest.cs Defines JobDetails and IJobRequest + NoPayload marker request.
src/Immediate.Jobs.NodaTime/NodaTimeJsonExtensions.cs Adds NodaTime serializer implementation + DI integration.
src/Immediate.Jobs.NodaTime/NodaTimeJobSchedulerExtensions.cs Adds NodaTime overloads for scheduling APIs.
src/Immediate.Jobs.NodaTime/Immediate.Jobs.NodaTime.csproj Adds packable NodaTime integration project definition.
src/Immediate.Jobs.Generators/Templates/ServiceCollectionExtensions.sbntxt Adds Scriban template for generated AddImmediateJobs registrations.
src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Transform.cs Adds model building for jobs/queues (discovery/validation/tagging).
src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Render.cs Adds rendering logic for jobs + registrations with Scriban templates.
src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Models.cs Adds generator model records for jobs/queues/assembly defaults.
src/Immediate.Jobs.Generators/ImmediateJobsGenerator.cs Adds incremental generator pipeline and output registration.
src/Immediate.Jobs.Generators/Immediate.Jobs.Generators.csproj Adds generator project targeting netstandard2.0 + net10.0 and Scriban wiring.
src/Immediate.Jobs.Generators/EquatableReadOnlyList.cs Adds stable value-equality wrapper for incremental generator models.
src/Immediate.Jobs.EntityFrameworkCore/ImmediateJobsModelBuilderExtensions.cs Adds EF Core entity model mappings for jobs/recurring/servers.
src/Immediate.Jobs.EntityFrameworkCore/Immediate.Jobs.EntityFrameworkCore.csproj Adds packable EF Core provider project definition.
src/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreServiceCollectionExtensions.cs Adds options extension to select EF Core storage provider.
src/Immediate.Jobs.Dashboard/ImmediateJobsDashboardOptions.cs Adds dashboard options including authorization policy wiring.
src/Immediate.Jobs.Dashboard/Immediate.Jobs.Dashboard.csproj Adds packable dashboard package project definition + embedded assets.
src/Immediate.Jobs.Dashboard/DevelopmentDashboardFilter.cs Adds dev-only endpoint filter fallback when no auth policy is configured.
src/Immediate.Jobs.Dashboard/DashboardState.cs Adds dashboard state DTO for SSE payloads.
src/Immediate.Jobs.Dashboard/DashboardJsonSerializerContext.cs Adds source-gen JSON context for dashboard API payload shapes.
src/Immediate.Jobs.Dashboard/DashboardClient/vite.config.js Adds Vite build config outputting deterministic asset names.
src/Immediate.Jobs.Dashboard/DashboardClient/tests/JobTable.test.js Adds SSR-based unit test for JobTable rendering behavior.
src/Immediate.Jobs.Dashboard/DashboardClient/tests/HistoryChart.test.js Adds SSR-based unit test for HistoryChart rendering and accessibility labels.
src/Immediate.Jobs.Dashboard/DashboardClient/src/MetricCard.svelte Adds animated metric card component with reduced-motion handling.
src/Immediate.Jobs.Dashboard/DashboardClient/src/main.js Adds dashboard SPA entrypoint mounting Svelte app.
src/Immediate.Jobs.Dashboard/DashboardClient/src/JobTable.svelte Adds job table + details panel UI with retry/delete actions.
src/Immediate.Jobs.Dashboard/DashboardClient/src/HistoryChart.svelte Adds inline SVG chart with accessible focus/hover tooltips.
src/Immediate.Jobs.Dashboard/DashboardClient/src/App.svelte Adds main dashboard SPA view/state management + SSE subscription.
src/Immediate.Jobs.Dashboard/DashboardClient/src/app.css Adds dashboard SPA styles.
src/Immediate.Jobs.Dashboard/DashboardClient/package.json Adds dashboard client build/test scripts and dependencies.
src/Immediate.Jobs.Dashboard/DashboardClient/index.html Adds dashboard client HTML entrypoint.
src/Immediate.Jobs.Dashboard/DashboardAssets.cs Adds embedded resource serving helper for SPA assets.
src/Immediate.Jobs.Dashboard/Assets/index.html Adds built SPA index embedded into the dashboard package.
src/Immediate.Jobs.Dashboard/Assets/app.css Adds built/minified SPA CSS embedded into the dashboard package.
src/Immediate.Jobs.Analyzers/Immediate.Jobs.Analyzers.csproj Adds analyzer project definition (Roslyn component).
src/Immediate.Jobs.Analyzers/DiagnosticDescriptors.cs Adds analyzer diagnostic descriptors for invalid job declarations.
src/Common/Utility.cs Adds shared helper extensions (e.g., WhereNotNull).
src/Common/PayloadValidation.cs Adds payload/context type validation for source-gen JSON compatibility + NodaTime detection.
src/Common/ITypeSymbolExtensions.cs Adds Roslyn symbol classification helpers for discovery/validation.
src/Common/CronValidator.cs Adds lightweight compile-time cron validation helper.
samples/NativeAot/Program.cs Adds NativeAOT sample demonstrating context capture/restore and generator output.
samples/NativeAot/NativeAot.csproj Adds NativeAOT sample project configuration.
samples/Basic/Program.cs Adds basic ASP.NET Core sample integrating scheduler + dashboard.
samples/Basic/Basic.csproj Adds basic sample project configuration.
samples/Aspire/ServiceDefaults/Immediate.Jobs.Aspire.ServiceDefaults.csproj Adds Aspire service-defaults helper project.
samples/Aspire/ServiceDefaults/Extensions.cs Adds OpenTelemetry + health endpoint wiring for Aspire sample.
samples/Aspire/readme.md Adds Aspire sample documentation and usage instructions.
samples/Aspire/AppHost/Properties/launchSettings.json Adds Aspire AppHost launch settings.
samples/Aspire/AppHost/Immediate.Jobs.Aspire.AppHost.csproj Adds Aspire AppHost project configuration.
samples/Aspire/AppHost/AppHost.cs Adds Aspire distributed app composition (Postgres + API).
samples/Aspire/Api/Properties/launchSettings.json Adds Aspire API launch settings.
samples/Aspire/Api/Program.cs Adds Aspire sample API using EF Core storage, dashboard, context extraction, recurring heartbeat.
samples/Aspire/Api/Immediate.Jobs.Aspire.Api.csproj Adds Aspire API project configuration.
license.txt Updates license header text and copyright year/casing.
Immediate.Jobs.slnx Expands solution manifest to include new projects/tools/config files.
global.json Adds test runner configuration for Microsoft.Testing.Platform.
dotnet-tools.json Adds dotnet tool manifest (dotnet-validate).
docs/monitoring-api.md Documents stable dashboard/monitoring API endpoints and semantics.
Directory.Build.props Sets global TargetFrameworks, CI warning behavior, polyfills, and MinVer settings.
coverage-settings.xml Adds code coverage configuration excluding analyzer/generator modules and Scriban sources.
benchmarks/Immediate.Jobs.Benchmarks/SchedulerBenchmarks.cs Adds benchmarks comparing enqueue/startup/dispatch against Hangfire and Quartz.
benchmarks/Immediate.Jobs.Benchmarks/Program.cs Adds BenchmarkDotNet entrypoint.
benchmarks/Immediate.Jobs.Benchmarks/Immediate.Jobs.Benchmarks.csproj Adds benchmarks project configuration.
BenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.StartupBenchmarks-report-github.md Adds benchmark result artifact (startup).
BenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.EnqueueBenchmarks-report-github.md Adds benchmark result artifact (enqueue).
BenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.DispatchBenchmarks-report-github.md Adds benchmark result artifact (dispatch).
.gitignore Updates ignore rules for build outputs, artifacts, and node_modules layout.
.github/workflows/release.yml Updates release workflow: multi-SDK setup, Node build, OIDC NuGet push, release creation.
.github/workflows/build.yml Updates build workflow: multi-SDK setup, Node test/build, tool restore, coverage, package validation, NativeAOT publish.
.github/FUNDING.yml Removes repository funding configuration.
.github/dependabot.yml Simplifies header and adds npm ecosystem updates for dashboard client.
.gitattributes Adjusts git attributes (currently only a whitespace rule for dashboard app.js).
.editorconfig Updates naming rules and analyzer severities; adds per-folder documentation enforcement.
Files not reviewed (1)
  • src/Immediate.Jobs.Dashboard/DashboardClient/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Immediate.Jobs/Immediate.Jobs.csproj
Comment thread src/Immediate.Jobs/Immediate.Jobs.csproj
Comment thread tests/Immediate.Jobs.Tests/ModuleInitializer.cs
Comment thread .gitattributes
@viceroypenguin
viceroypenguin merged commit eac5c10 into master Jul 27, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (26)
docs/context-propagation.md-344-348 (1)

344-348: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the failure-policy tests assert the decided behavior.

§8 requires restore failures to retry and capture failures to propagate to the caller, but §14.4 currently allows “run-without-context” and “succeeds without context” as alternatives. That permits implementations which violate the stated policy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/context-propagation.md` around lines 344 - 348, Update the “Restore
failure policy” and “Capture failure policy” requirements in §14.4 to assert the
§8 decisions exclusively: restore failures must trigger retry behavior,
including the expected retry count and eventual dead-letter outcome, while
CaptureAsync failures must propagate to the enqueue caller. Remove the
alternative “run-without-context” and “succeeds without context” outcomes.
docs/context-propagation.md-168-172 (1)

168-172: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make orphaned context slices optional during restore.

§8 says a missing extractor should be skipped, but this generated example calls GetRequiredService before reading the slice. A removed or misregistered extractor therefore throws and retries/dead-letters the job instead of running without that slice. Resolve the extractor optionally, while still treating RestoreAsync failures as execution failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/context-propagation.md` around lines 168 - 172, Update the context
restoration block around UsageContextExtractor so the extractor is resolved
optionally and a missing registration skips that slice without throwing.
Continue reading and restoring the slice when an extractor is available, and
allow RestoreAsync failures to propagate as execution failures.
.github/workflows/build.yml-5-7 (1)

5-7: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Align the push trigger with master

.github/workflows/build.yml only fires on pushes to main, but this repo’s branch is master. Pushes to master won’t run the post-merge build/test/packaging workflow.

Proposed alignment
     branches:
-      - 'main'
+      - 'master'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml around lines 5 - 7, Update the push trigger
branch under the workflow’s `push` configuration from `main` to `master`, so the
post-merge build, test, and packaging workflow runs for pushes to the
repository’s actual default branch.
docs/context-propagation.md-135-137 (1)

135-137: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Redact Context from public monitoring output. docs/monitoring-api.md says read endpoints return the public JobRecord JSON shape, so this envelope can be exposed through /api/jobs while carrying UserId and TenantId. Exclude it from the response contract or add explicit allowlisting/redaction and retention rules before treating it as public data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/context-propagation.md` around lines 135 - 137, Update the
context-propagation documentation around the envelope description to prevent
Context from being treated as public monitoring data: either exclude the
envelope from the public JobRecord response contract or define explicit
allowlisting, redaction, and retention rules before exposing it through
monitoring endpoints. Keep the storage representation separate from the
documented public response shape.
.github/workflows/build.yml-18-18 (1)

18-18: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Disable persisted checkout credentials. actions/checkout leaves the token in .git/config by default, and the later npm/.NET steps run repository-controlled code. Set persist-credentials: false here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml at line 18, Update the actions/checkout step to
set persist-credentials to false, ensuring the checkout token is not retained in
.git/config before subsequent npm/.NET steps execute.

Source: Linters/SAST tools

.github/workflows/release.yml-3-7 (1)

3-7: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restrict publishing to release tags.

'**' publishes packages and creates a release for every tag, while Directory.Build.props configures MinVer to recognize v-prefixed tags. A non-release tag can therefore publish an unintended build. Filter to release tags (for example, v*) or enforce equivalent SemVer validation.

Proposed fix
     tags:
-      - '**'
+      - 'v*'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 3 - 7, Update the push trigger in
the workflow’s on configuration to match only MinVer release tags, such as
v-prefixed tags, instead of every tag. Preserve workflow_dispatch and ensure
non-release tags cannot trigger publishing.
.github/workflows/release.yml-18-18 (1)

18-18: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pin the workflow actions and disable checkout credential persistence. All uses: entries here are mutable tags; pin them to full commit SHAs, and set persist-credentials: false on actions/checkout so later build steps can’t reuse the repository write token from git config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 18, Update the workflow’s
actions/checkout step to use an immutable full commit SHA instead of the mutable
v7 tag, and set persist-credentials to false for that checkout. Review every
other uses entry in the workflow and replace mutable tags with their
corresponding full commit SHAs.

Source: Linters/SAST tools

src/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreJobStorage.cs-133-165 (1)

133-165: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

N+1 DbContext/round-trip per acquired job in the acquisition hot path.

AcquireCandidatesAsync opens a brand-new DbContext and issues a separate SaveChangesAsync for every single candidate, sequentially. Both AcquireDueJobsAsync (line 83-89) and AcquireJobsAsync (line 130) route through this method, so acquiring a batch of N due jobs costs N sequential context creations + round trips instead of one. This will scale poorly as batch size/throughput grows and increases exposure to write contention (e.g. SQLite "database is locked" under the functional tests' concurrent-node scenario).

♻️ Proposed fix: batch the save and use `DbUpdateConcurrencyException.Entries` to retry only conflicting rows
-		var acquired = new List<JobRecord>(candidates.Count);
-		foreach (var candidate in candidates)
-		{
-			await using var context = await contextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
-			var entity = Copy(candidate);
-			_ = context.Attach(entity);
-			entity.State = JobState.Active;
-			entity.WorkerId = workerId;
-			entity.LeaseExpiresAt = now + lease;
-			entity.Attempt++;
-			entity.CompletedAt = null;
-			entity.ConcurrencyStamp = Guid.NewGuid();
-			try
-			{
-				_ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
-				acquired.Add(ToRecord(entity));
-			}
-			catch (DbUpdateConcurrencyException)
-			{
-				// Another scheduler claimed or changed this candidate first.
-			}
-		}
-
-		return acquired;
+		await using var context = await contextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+		var tracked = new List<ImmediateJobEntity>(candidates.Count);
+		foreach (var candidate in candidates)
+		{
+			var entity = Copy(candidate);
+			_ = context.Attach(entity);
+			entity.State = JobState.Active;
+			entity.WorkerId = workerId;
+			entity.LeaseExpiresAt = now + lease;
+			entity.Attempt++;
+			entity.CompletedAt = null;
+			entity.ConcurrencyStamp = Guid.NewGuid();
+			tracked.Add(entity);
+		}
+
+		while (tracked.Count > 0)
+		{
+			try
+			{
+				_ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+				break;
+			}
+			catch (DbUpdateConcurrencyException ex)
+			{
+				// Another scheduler claimed or changed some candidates first; drop just those and retry the rest.
+				foreach (var entry in ex.Entries)
+				{
+					_ = tracked.Remove((ImmediateJobEntity)entry.Entity);
+					entry.State = EntityState.Detached;
+				}
+			}
+		}
+
+		return [.. tracked.Select(ToRecord)];

Also applies to: 83-89, 130-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreJobStorage.cs`
around lines 133 - 165, Refactor AcquireCandidatesAsync to create one DbContext
for the candidate batch, attach and update all copied entities, then perform a
single SaveChangesAsync. If DbUpdateConcurrencyException occurs, use its Entries
to identify only conflicting candidates, detach or revert those entries, and
retry saving the non-conflicting acquisitions so successful jobs are returned
while conflicts are skipped; preserve cancellation handling and existing
acquisition field updates.
src/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreJobStorage.cs-453-466 (1)

453-466: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Unbounded in-memory load for purge; inconsistent with the bulk-delete pattern already used in this file.

PurgeAsync loads every matching job entity into memory (no Take/batching) before RemoveRange + SaveChangesAsync. For a background-job store this table can accumulate a large volume of completed/failed rows over time, so this can load unbounded data into memory and issue one DELETE per row. RemoveObsoleteCodeDefinedRecurringAsync (line 232-244) already demonstrates the correct set-based pattern with ExecuteDeleteAsync for this codebase.

♻️ Proposed fix: use a set-based delete
-		var jobs = await context.Set<ImmediateJobEntity>()
-			.Where(job => (job.State == JobState.Succeeded && job.CompletedAt < succeededBefore)
-				|| ((job.State == JobState.Failed || job.State == JobState.Cancelled) && job.CompletedAt < failedBefore))
-			.ToListAsync(cancellationToken)
-			.ConfigureAwait(false);
-		context.RemoveRange(jobs);
-		_ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+		_ = await context.Set<ImmediateJobEntity>()
+			.Where(job => (job.State == JobState.Succeeded && job.CompletedAt < succeededBefore)
+				|| ((job.State == JobState.Failed || job.State == JobState.Cancelled) && job.CompletedAt < failedBefore))
+			.ExecuteDeleteAsync(cancellationToken)
+			.ConfigureAwait(false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreJobStorage.cs`
around lines 453 - 466, Update PurgeAsync to delete matching ImmediateJobEntity
records directly with the existing set-based ExecuteDeleteAsync pattern,
preserving the succeededBefore and failedBefore filtering and cancellation token
usage. Remove the ToListAsync, RemoveRange, and SaveChangesAsync flow so the
purge does not load entities or issue per-row deletes.
src/Immediate.Jobs.NodaTime/NodaTimeJsonExtensions.cs-55-60 (1)

55-60: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Constructor mutates the caller-supplied JsonSerializerOptions.

options.UseNodaTime(...) configures converters on the passed instance instead of copying it first. That can throw if the options are already read-only, or silently affect other code sharing the same options object. Create a copy before applying NodaTime configuration.

🐛 Proposed fix
 public NodaTimeJobSerializer(JsonSerializerOptions options, IDateTimeZoneProvider? timeZoneProvider = null)
 {
 	ArgumentNullException.ThrowIfNull(options);
-	Options = options.UseNodaTime(timeZoneProvider);
+	Options = new JsonSerializerOptions(options).UseNodaTime(timeZoneProvider);
 	_serializer = new(Options);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.NodaTime/NodaTimeJsonExtensions.cs` around lines 55 - 60,
Update the NodaTimeJobSerializer constructor to copy the caller-supplied
JsonSerializerOptions before invoking UseNodaTime, then assign the configured
copy to Options and initialize _serializer from it. Preserve null validation and
avoid mutating or rejecting the original options instance.
src/Immediate.Jobs.Dashboard/DashboardClient/tests/HistoryChart.test.js-9-13 (1)

9-13: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the Svelte runtime replacement quote-agnostic. The generated server module uses a quoted svelte/internal/server import, so replacing only 'svelte/internal/server' can leave the bare import in place and break the data: URL load in both tests.

Suggested change
-const moduleSource = compiled.js.code.replace("'svelte/internal/server'", serverRuntime);
+const moduleSource = compiled.js.code.replace(
+  /(["'])svelte\/internal\/server\1/,
+  serverRuntime
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Dashboard/DashboardClient/tests/HistoryChart.test.js`
around lines 9 - 13, Make the Svelte runtime replacement in both
HistoryChart.test.js lines 9-13 and JobTable.test.js lines 9-13 quote-agnostic
by matching either single- or double-quoted svelte/internal/server imports
before constructing the data URL. Preserve the existing import resolution and
module loading flow.
src/Immediate.Jobs.Dashboard/ImmediateJobsDashboardEndpointRouteBuilderExtensions.cs-226-262 (1)

226-262: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

SSE loop has no resilience against transient storage failures.

StreamEventsAsync only swallows OperationCanceledException; any other exception thrown by storage.GetMonitoringSnapshotAsync/QueryJobsAsync (e.g. a transient DB error in the EF Core provider) propagates unhandled and tears down the stream. Since the stream already advertises retry: 3000, every connected client will immediately reconnect and retry the same failing call, with no backoff — during a storage outage this can turn into a tight reconnect storm hammering the backend across all connected dashboards.

🔧 Proposed fix: catch and log per-iteration failures instead of letting them tear down the stream
             while (!cancellationToken.IsCancellationRequested)
             {
-                var snapshot = await storage.GetMonitoringSnapshotAsync(cancellationToken).ConfigureAwait(false);
-                var jobs = await storage.QueryJobsAsync(new() { Take = 100 }, cancellationToken).ConfigureAwait(false);
-                var state = new DashboardState(snapshot, [.. jobs]);
-                var json = JsonSerializer.Serialize(state, DashboardJsonSerializerContext.Default.DashboardState);
-                await context.Response.WriteAsync(
-                    "id: " + snapshot.CapturedAt.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture) + "\n",
-                    cancellationToken
-                ).ConfigureAwait(false);
-                await context.Response.WriteAsync("event: state\ndata: " + json + "\n\n", cancellationToken)
-                    .ConfigureAwait(false);
-                await context.Response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
+                try
+                {
+                    var snapshot = await storage.GetMonitoringSnapshotAsync(cancellationToken).ConfigureAwait(false);
+                    var jobs = await storage.QueryJobsAsync(new() { Take = 100 }, cancellationToken).ConfigureAwait(false);
+                    var state = new DashboardState(snapshot, [.. jobs]);
+                    var json = JsonSerializer.Serialize(state, DashboardJsonSerializerContext.Default.DashboardState);
+                    await context.Response.WriteAsync(
+                        "id: " + snapshot.CapturedAt.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture) + "\n",
+                        cancellationToken
+                    ).ConfigureAwait(false);
+                    await context.Response.WriteAsync("event: state\ndata: " + json + "\n\n", cancellationToken)
+                        .ConfigureAwait(false);
+                    await context.Response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
+                }
+                catch (Exception) when (!cancellationToken.IsCancellationRequested)
+                {
+                    // TODO: log the transient failure; keep the connection open instead of
+                    // forcing every client to immediately reconnect against a failing backend.
+                }
                 await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Immediate.Jobs.Dashboard/ImmediateJobsDashboardEndpointRouteBuilderExtensions.cs`
around lines 226 - 262, Update StreamEventsAsync so transient exceptions from
per-iteration storage or serialization work are caught and logged within the
loop instead of terminating the SSE request. Preserve OperationCanceledException
handling for client cancellation, and add a delay/backoff before retrying failed
iterations to avoid immediate reconnect or storage retry storms.
src/Immediate.Jobs.Dashboard/DashboardClient/src/HistoryChart.svelte-63-63 (1)

63-63: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Focusable buttons inside role="img" are hidden from assistive tech.

role="img" makes the whole SVG subtree presentational, so the <title> elements and the foreignObject buttons are pruned from the accessibility tree. Keyboard users still tab into 30 focusable controls per chart that screen readers cannot name, and the per-point aria-labels never reach AT.

Either drop the per-point buttons and keep a single labelled role="img" graphic (with a text summary elsewhere), or switch the root to role="group" with an aria-label so the interactive descendants remain exposed.

♿ Minimal fix: expose the group instead of an image
-  <svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`Recent ${label.toLowerCase()} history`}>
+  <svg viewBox={`0 0 ${width} ${height}`} role="group" aria-label={`Recent ${label.toLowerCase()} history`}>

Also applies to: 76-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Dashboard/DashboardClient/src/HistoryChart.svelte` at line
63, Update the SVG root in HistoryChart to use role="group" instead of
role="img", preserving its aria-label so the foreignObject buttons and per-point
aria-labels remain exposed to assistive technology.
src/Immediate.Jobs.Dashboard/DashboardClient/src/App.svelte-7-20 (1)

7-20: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the derived values explicit in legacy mode. App.svelte is still a legacy component, so the top-level let state is reactive; the real risk is that visibleJobs() and count(item) hide their dependencies from Svelte’s legacy analysis. Search/filter changes and snapshot updates may not refresh those bindings reliably. Use $: (or inline the expressions) so the jobs list and metric cards stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Dashboard/DashboardClient/src/App.svelte` around lines 7 -
20, Update the legacy-reactive derived values in App.svelte, especially
visibleJobs() and count(item), to use explicit $: declarations or inline
expressions that reference search, state, queue, jobs, and snapshot. Ensure
search/filter changes and snapshot updates reliably refresh the jobs list and
metric cards without changing their existing filtering or counting behavior.
tests/Immediate.Jobs.Tests/ModuleInitializer.cs-10-14 (1)

10-14: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the StringComparison-first overload here. VerifierSettings.ScrubLinesContaining("cs", comparison: StringComparison.Ordinal, "GeneratedCodeAttribute"); doesn’t match either available overload; if both strings should be scrubbed case-sensitively, this should be VerifierSettings.ScrubLinesContaining(StringComparison.Ordinal, "cs", "GeneratedCodeAttribute");.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Immediate.Jobs.Tests/ModuleInitializer.cs` around lines 10 - 14, The
ScrubLinesContaining call in ModuleInitializer uses an invalid argument order;
update it to the StringComparison-first overload, preserving ordinal matching
and both scrubbed strings.
src/Common/CronValidator.cs-38-54 (1)

38-54: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align CronValidator with Cronos grammar. CronValidator only accepts numeric 5/6-field expressions, so analyzer/generated-code validation rejects valid Cronos forms like @daily, JAN, MON-FRI, L, and reversed ranges that the scheduler parses. Delegate validation to Cronos or expand this parser to match its grammar.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Common/CronValidator.cs` around lines 38 - 54, Update CronValidator’s
validation flow, especially ValidateField and its caller, to use Cronos parsing
as the source of truth or fully support Cronos grammar. Ensure valid aliases,
month/day names, L syntax, reversed ranges, and supported 5/6-field expressions
are accepted consistently with the scheduler, while still rejecting invalid
expressions.
src/Immediate.Jobs.Generators/Templates/ServiceCollectionExtensions.sbntxt-14-14 (1)

14-14: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard the registrations template against older LangVersion. src/Immediate.Jobs.Generators/Templates/ServiceCollectionExtensions.sbntxt:28 emits unconditional list-pattern and collection-expression syntax, so a project pinned to an older LangVersion can fail to compile even though line 14 already branches for params support. Use version-agnostic syntax here, e.g. tags.Length == 0 || Intersects(tags, new[] { {{ group.key }} }).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Generators/Templates/ServiceCollectionExtensions.sbntxt`
at line 14, Update the registrations template’s tag-matching logic near the
version-conditional params declaration to avoid unconditional list-pattern and
collection-expression syntax. Replace those constructs with syntax supported by
older LangVersion values, while preserving the empty-tags and Intersects
matching behavior.
src/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cs-31-37 (1)

31-37: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use a netstandard2.0-compatible null check here ArgumentNullException.ThrowIfNull is not available on the netstandard2.0 target, so this analyzer project will fail to compile there. Use a null check that works across both target frameworks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cs` around lines 31 - 37,
Replace ArgumentNullException.ThrowIfNull in Initialize with an explicit
netstandard2.0-compatible null check that throws ArgumentNullException for a
null context, while preserving the existing analysis configuration and
registration flow.
src/Immediate.Jobs.Shared/JobExecution.cs-71-90 (1)

71-90: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Cache JsonTypeInfo<TPayload> instead of rebuilding it on every Serialize/Deserialize call.

Every generated call site passes a factory like options => new PayloadJsonContext(options).Payload. Because Deserialize/Serialize invoke this factory (and clone Options via new(Options)) on every call, each job enqueue and each job invocation reconstructs a brand-new JsonSerializerContext and rebuilds the full JsonTypeInfo metadata graph for TPayload, defeating the whole point of source-generated JSON metadata (which is meant to be built once and reused, as the existing PayloadJsonContext.Default singleton pattern already demonstrates). This is a hot-path allocation/CPU cost paid on every job dispatch.

♻️ Proposed fix: cache the computed JsonTypeInfo per payload type
+using System.Collections.Concurrent;
+
 public sealed class SystemTextJsonJobSerializer(JsonSerializerOptions options) : IJobSerializer
 {
+	private readonly ConcurrentDictionary<Type, object> _typeInfoCache = new();
+
 	// ...

 	public string Serialize<TPayload>(
 		TPayload payload,
 		Func<JsonSerializerOptions, JsonTypeInfo<TPayload>> payloadTypeInfoFactory
 	)
 	{
 		ArgumentNullException.ThrowIfNull(payloadTypeInfoFactory);
-		return JsonSerializer.Serialize(payload, payloadTypeInfoFactory(new(Options)));
+		var typeInfo = (JsonTypeInfo<TPayload>)_typeInfoCache.GetOrAdd(
+			typeof(TPayload),
+			_ => payloadTypeInfoFactory(new(Options)));
+		return JsonSerializer.Serialize(payload, typeInfo);
 	}

 	public TPayload Deserialize<TPayload>(
 		string payload,
 		Func<JsonSerializerOptions, JsonTypeInfo<TPayload>> payloadTypeInfoFactory
 	)
 	{
 		ArgumentNullException.ThrowIfNull(payloadTypeInfoFactory);
-		return JsonSerializer.Deserialize(payload, payloadTypeInfoFactory(new(Options)))
-			?? throw new JsonException($"The payload for {typeof(TPayload).FullName} was null.");
+		var typeInfo = (JsonTypeInfo<TPayload>)_typeInfoCache.GetOrAdd(
+			typeof(TPayload),
+			_ => payloadTypeInfoFactory(new(Options)));
+		return JsonSerializer.Deserialize(payload, typeInfo)
+			?? throw new JsonException($"The payload for {typeof(TPayload).FullName} was null.");
 	}
 }

Note: caching by typeof(TPayload) assumes one payload type maps to a single context/options combination in practice (true for the generated call sites shown); if Options can be swapped at runtime (e.g. via IOptionsMonitor), the cache key would need to also account for that.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/JobExecution.cs` around lines 71 - 90, Cache the
JsonTypeInfo<TPayload> produced by payloadTypeInfoFactory in the Serialize and
Deserialize flow, keyed by typeof(TPayload), so each payload type’s metadata is
created once and reused instead of rebuilding a context from new(Options) per
call. Preserve the existing null checks, serialization behavior, and
null-deserialization exception.
src/Immediate.Jobs.Shared/SingleServerJobStorage.cs-169-180 (1)

169-180: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Partial materialization orphans a job in durable storage.

The durable write happens first and is not undone. If durable succeeds but the primary returns false (schedule drift or duplicate RecurringKey), this method reports false while durable storage holds a Pending record and an advanced NextRunAt. Because AcquireDueJobsAsync reads only the primary, that record is never executed and never purged until it ages out. Materialize against the primary first and only replicate on success, or reconcile the durable record when the primary rejects it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/SingleServerJobStorage.cs` around lines 169 - 180,
Update MaterializeRecurringAsync to materialize against _primary before
DurableStorage, and only replicate to durable storage after the primary
succeeds. Preserve the existing false return when the primary rejects the
recurring job, and ensure durable storage is not left with a pending record or
advanced schedule in that case.
src/Immediate.Jobs.Shared/InMemoryJobStorage.cs-296-305 (1)

296-305: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

_recurringKeys grows without bound.

Every materialized occurrence adds a key that is never removed — not by PurgeAsync, DeleteAsync, or schedule removal. Since SingleServerJobStorage uses InMemoryJobStorage as its authoritative primary in production single-server mode, a frequent cron (e.g. per-second) leaks one string per occurrence for the lifetime of the process.

🐛 Suggested cleanup on purge/delete
 	public ValueTask DeleteAsync(string jobId, CancellationToken cancellationToken = default)
 	{
 		cancellationToken.ThrowIfCancellationRequested();
 		lock (_gate)
 		{
 			if (_jobs.TryGetValue(jobId, out var job) && job.State is JobState.Active or JobState.Pending or JobState.Scheduled)
 				throw new InvalidOperationException("Only terminal jobs can be deleted.");
 
+			if (job?.RecurringKey is { } recurringKey)
+				_ = _recurringKeys.Remove(recurringKey);
 			_ = _jobs.Remove(jobId);
 		}

and in PurgeAsync, remove the RecurringKey of each purged record before removing the job.

Also applies to: 376-394

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/InMemoryJobStorage.cs` around lines 296 - 305,
Update InMemoryJobStorage lifecycle handling so every removed job also removes
its RecurringKey from _recurringKeys. Apply this in the purge logic around
PurgeAsync, deletion logic around DeleteAsync, and schedule-removal paths,
removing the key before removing the corresponding job record while preserving
existing behavior for null keys.
src/Immediate.Jobs.Shared/JobSchedulerService.cs-487-495 (1)

487-495: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

OverlapPolicy.Skip can miss an active run.

Search is a substring match over JobName (InMemoryJobStorage.QueryJobsAsync, Line 336), and Take = 1 returns only the newest match. If any other active job name contains definition.Name as a substring, that record wins the single slot, the exact-name check fails, and the overlapping occurrence is materialized anyway. Query by job name explicitly rather than relying on Search + Take = 1.

🐛 Suggested direction
-				var active = await _storage.QueryJobsAsync(new() { State = JobState.Active, Search = definition.Name, Take = 1 }, cancellationToken)
-					.ConfigureAwait(false);
-				if (active.Any(x => string.Equals(x.JobName, definition.Name, StringComparison.Ordinal)))
+				var active = await _storage.QueryJobsAsync(
+					new() { State = JobState.Active, JobName = definition.Name, Take = 1 },
+					cancellationToken
+				).ConfigureAwait(false);
+				if (active.Count > 0)
 				{
 					record = record with { State = JobState.Cancelled, CompletedAt = now };
 				}

This requires adding an exact JobName filter to JobQuery and to each provider's QueryJobsAsync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/JobSchedulerService.cs` around lines 487 - 495,
Update the OverlapPolicy.Skip check in the job scheduling flow to query active
jobs by an exact JobName filter rather than Search plus Take = 1. Add the
exact-name criterion to JobQuery and implement it consistently in every
QueryJobsAsync provider, then use that filter when querying definition.Name so
any active exact-name run is detected.
src/Immediate.Jobs.Shared/SingleServerJobStorage.cs-283-305 (1)

283-305: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Recovery does not restore recurring-occurrence dedupe keys.

Jobs and schedules are rebuilt, but the primary's _recurringKeys set (InMemoryJobStorage.MaterializeRecurringAsync, Line 299) starts empty, so an occurrence already materialized before the restart can be materialized a second time — the durable EnqueueAsync then fails on the duplicate id, or a duplicate run is created. Consider seeding the set from the recovered records' RecurringKey values.

🛠️ Suggested direction
 					foreach (var job in jobs)
 						await recoveredPrimary.EnqueueAsync(job, cancellationToken).ConfigureAwait(false);

Have InMemoryJobStorage.EnqueueAsync register a non-null job.RecurringKey in _recurringKeys so recovery and normal enqueue share one dedupe path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/SingleServerJobStorage.cs` around lines 283 - 305,
Update InMemoryJobStorage.EnqueueAsync to register every non-null
job.RecurringKey in _recurringKeys, ensuring recovered jobs and normally
enqueued jobs share the same recurring-occurrence deduplication path; keep null
keys unregistered and preserve existing enqueue behavior.
src/Immediate.Jobs.Shared/JobSchedulerService.cs-308-324 (1)

308-324: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A lease-renewal failure in finally masks the job outcome.

Only OperationCanceledException is swallowed at Line 316. An in-flight RenewLeaseAsync that lands after the job reached a terminal state throws InvalidOperationException from GetOwnedActive (InMemoryJobStorage, Line 415); that exception escapes the finally, replaces the successful/failed outcome, and skips Release(record) in DrainAsync's caller path. Swallow non-fatal renewal failures and log them instead.

🛡️ Proposed handling
 			try
 			{
 				await leaseTask.ConfigureAwait(false);
 			}
 			catch (OperationCanceledException)
 			{
 			}
+#pragma warning disable CA1031 // Lease renewal failures must not replace the job outcome.
+			catch (Exception leaseException)
+#pragma warning restore CA1031
+			{
+				LeaseRenewalFailed(_logger, leaseException, record.Id);
+			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/JobSchedulerService.cs` around lines 308 - 324,
Update the leaseTask await handling in the JobSchedulerService cleanup finally
block to catch non-fatal renewal exceptions, including
InvalidOperationException, and log them instead of allowing them to replace the
job outcome. Preserve cancellation handling and ensure cleanup continues through
_state.DecrementActive(), JobTelemetry.ExecutionFinished(), and Release(record).
src/Immediate.Jobs.Shared/InMemoryJobStorage.cs-3-3 (1)

3-3: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the async keyword from these methods or add a real await. #pragma warning disable IDE0391 doesn’t cover CS1998, and CS1998 is treated as an error here, so these methods fail the build under TreatWarningsAsErrors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/InMemoryJobStorage.cs` at line 3, Remove the async
keyword from the synchronous storage methods in the in-memory job storage
implementation, or add a genuine await where asynchronous behavior is required,
so CS1998 no longer occurs. Remove the ineffective IDE0391 pragma unless it
remains necessary for other methods, and preserve each method’s existing
behavior and return contract.
src/Immediate.Jobs.Shared/JobSchedulerService.cs-505-515 (1)

505-515: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Backoff computation can overflow to a negative delay.

Math.Min(TimeSpan.MaxValue.Ticks, ...) compares in double; (double)long.MaxValue rounds above long.MaxValue, so the subsequent (long)ticks cast is an unchecked out-of-range conversion yielding long.MinValue. With a larger BackoffBase (e.g. 1 hour) and a high attempt count the product exceeds the range and the retry is scheduled in the past.

🐛 Proposed clamp
 		var exponent = Math.Min(30, Math.Max(0, attempt - 1));
-		var ticks = Math.Min(TimeSpan.MaxValue.Ticks, definition.BackoffBase.Ticks * Math.Pow(2, exponent));
-		if (definition.Backoff == BackoffStrategy.ExponentialJitter)
-			ticks *= 0.5 + Random.Shared.NextDouble();
-		return TimeSpan.FromTicks((long)ticks);
+		var ticks = definition.BackoffBase.Ticks * Math.Pow(2, exponent);
+		if (definition.Backoff == BackoffStrategy.ExponentialJitter)
+			ticks *= 0.5 + Random.Shared.NextDouble();
+		return ticks >= TimeSpan.MaxValue.Ticks
+			? TimeSpan.MaxValue
+			: TimeSpan.FromTicks((long)ticks);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/JobSchedulerService.cs` around lines 505 - 515,
Update GetRetryDelay so exponential backoff is clamped safely before converting
to long ticks; avoid relying on Math.Min with double against
TimeSpan.MaxValue.Ticks, which can permit an out-of-range cast to produce a
negative delay. Preserve fixed backoff and jitter behavior while ensuring the
final value never exceeds the valid TimeSpan tick range.
🟡 Minor comments (9)
.gitattributes-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve the root Git attributes

Root .gitattributes now only contains the app.js whitespace rule, so the repo-wide text normalization and diff/merge settings are gone. Keep the global attributes and add this file-specific override only if a repository-wide migration is intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitattributes at line 1, Restore the existing repository-wide Git
attributes in the root .gitattributes file, including its text normalization and
diff/merge settings, then retain the app.js-specific whitespace=-blank-at-eol
override. Do not replace the global attributes unless a repository-wide
migration is explicitly intended.
.gitignore-5-7 (1)

5-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep generic user files ignored.

Removing the prior *.user rule causes Visual Studio user-specific project settings to appear as untracked files and makes accidental commits more likely. Restore *.user, then explicitly unignore any intentional shared file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 5 - 7, Restore the generic *.user ignore rule in
.gitignore to cover Visual Studio user-specific files, while explicitly
unignoring any intentional shared file that must remain tracked.
src/Immediate.Jobs.Testing/JobTestHarness.cs-96-107 (1)

96-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use JobQuery.Id for GetJobAsync

QueryJobsAsync already supports exact Id filtering, so the fixed Take = 1000 scan can miss a real job once storage grows past that window. Switch this lookup to new() { Id = jobId, Take = 1 } instead of paging and filtering in memory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Testing/JobTestHarness.cs` around lines 96 - 107, Update
GetJobAsync to query Storage.QueryJobsAsync with JobQuery configured as Id =
jobId and Take = 1, then use the returned result to preserve the existing
not-found JobTestAssertionException. Remove the fixed Take = 1000 scan and
in-memory ID filtering; leave QueryJobsAsync unchanged.
samples/Aspire/Api/Program.cs-50-51 (1)

50-51: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not redirect production requests to an unmapped Scalar route.

/scalar exists only when app.Environment.IsDevelopment() is true, so the root route becomes a broken redirect in other environments. Map this redirect conditionally or redirect to an always-mapped route such as /jobs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/Aspire/Api/Program.cs` around lines 50 - 51, Update the root route
mapping around the app.MapGet("/") call so it does not redirect to /scalar when
Scalar is unavailable outside development. Make the redirect conditional on
app.Environment.IsDevelopment(), or use an always-mapped destination such as
/jobs while preserving the existing root behavior.
src/Immediate.Jobs.Dashboard/Assets/app.css-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use 100% for the final keyframe selector.

to violates the configured keyframe-selector-notation rule.

Proposed fix
-...60%{opacity:1;transform:translateY(-1px) scale(1.005)}to{transform:none}}...
+...60%{opacity:1;transform:translateY(-1px) scale(1.005)}100%{transform:none}}...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Dashboard/Assets/app.css` at line 1, Update the final
selector in the `@keyframes` metric-update animation from the shorthand to
selector notation required by the configured keyframe-selector-notation rule,
using 100% while preserving the existing declarations.

Source: Linters/SAST tools

src/Immediate.Jobs.Shared/JobModels.cs-228-235 (1)

228-235: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Capture only W3C trace-parent values. Activity.Current?.Id can still be hierarchical under non-W3C ambient contexts, so TraceParent may persist a value that won’t round-trip through ActivityContext.TryParse. Guard on activity?.IdFormat == ActivityIdFormat.W3C before storing Id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Shared/JobModels.cs` around lines 228 - 235, Update
TraceContextCapture.Current to store the activity Id only when Activity.Current
uses ActivityIdFormat.W3C; otherwise return a null Parent while preserving
TraceStateString capture.
src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Transform.cs-119-133 (1)

119-133: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Tags = null yields a null array constant and will throw when enumerating Values.

[Handler(Tags = null)] produces Kind == TypedConstantKind.Array with IsNull == true and a default Values, which the Select then walks. Add an IsNull check.

🛡️ Proposed guard
-		if (tags is not { Kind: TypedConstantKind.Array })
+		if (tags is not { Kind: TypedConstantKind.Array, IsNull: false })
 			return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Transform.cs` around
lines 119 - 133, Update GetHandlerTags to check tags.IsNull after confirming the
named argument is an array, returning null for a null array before accessing
tags.Value.Values; preserve the existing formatting and ordering for non-null
tag arrays.
src/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cs-46-56 (1)

46-56: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

entries.First(...) can throw if the group contains only symbol-equal entries.

If JobDiscovery.FindJobs ever yields the same symbol more than once (e.g. duplicate attribute application paths), group.Count() > 1 is true while no candidate differs, and First throws InvalidOperationException, which surfaces as an analyzer crash (AD0001). Prefer FirstOrDefault plus a skip. Same pattern at Line 203 for queues.

🛡️ Proposed guard
-				var other = entries.First(candidate => !SymbolEqualityComparer.Default.Equals(candidate, job));
+				var other = entries.FirstOrDefault(candidate => !SymbolEqualityComparer.Default.Equals(candidate, job));
+				if (other is null)
+					continue;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cs` around lines 46 - 56,
Update the duplicate-job diagnostic loop around JobDiscovery.FindJobs to use
FirstOrDefault when selecting the differing symbol, and skip reporting when no
such symbol exists; apply the same guard to the corresponding duplicate-queue
logic around line 203. Preserve diagnostics for groups containing genuinely
distinct symbols while preventing analyzer exceptions for symbol-equal
duplicates.
src/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cs-140-142 (1)

140-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Time-zone problems are reported under the cron descriptor.

InvalidCron (IJOB001) with the message "time zone must not be empty" is misleading — suppressing IJOB001 to silence a cron problem would also hide time-zone errors and vice versa. Use InvalidConfiguration (or a dedicated descriptor) instead.

🐛 Proposed fix
 			if (timeZone is not null && !JobDiscovery.IsValidTimeZone(timeZone))
-				context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidCron, location, timeZone, "time zone must not be empty"));
+				context.ReportDiagnostic(Diagnostic.Create(
+					DiagnosticDescriptors.InvalidConfiguration, location, job.Name, "TimeZone must not be empty"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cs` around lines 140 -
142, The invalid time-zone validation in the analyzer currently reports the cron
diagnostic descriptor, conflating unrelated configuration errors. Update the
ReportDiagnostic call in the timeZone validation branch to use
InvalidConfiguration (or the appropriate dedicated time-zone descriptor), while
preserving the existing validation condition and message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4042278e-b493-4ac5-a787-dc47d2c3b720

📥 Commits

Reviewing files that changed from the base of the PR and between e8a73c2 and a73d5f4.

⛔ Files ignored due to path filters (1)
  • src/Immediate.Jobs.Dashboard/DashboardClient/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (132)
  • .editorconfig
  • .gitattributes
  • .github/FUNDING.yml
  • .github/dependabot.yml
  • .github/workflows/build.yml
  • .github/workflows/release.yml
  • .gitignore
  • BenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.DispatchBenchmarks-report-github.md
  • BenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.EnqueueBenchmarks-report-github.md
  • BenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.StartupBenchmarks-report-github.md
  • Directory.Build.props
  • Directory.Packages.props
  • Immediate.Jobs.slnx
  • benchmarks/Immediate.Jobs.Benchmarks/Immediate.Jobs.Benchmarks.csproj
  • benchmarks/Immediate.Jobs.Benchmarks/Program.cs
  • benchmarks/Immediate.Jobs.Benchmarks/SchedulerBenchmarks.cs
  • coverage-settings.xml
  • docs/context-propagation.md
  • docs/monitoring-api.md
  • dotnet-tools.json
  • global.json
  • license.txt
  • readme.md
  • samples/Aspire/Api/Immediate.Jobs.Aspire.Api.csproj
  • samples/Aspire/Api/Program.cs
  • samples/Aspire/Api/Properties/launchSettings.json
  • samples/Aspire/AppHost/AppHost.cs
  • samples/Aspire/AppHost/Immediate.Jobs.Aspire.AppHost.csproj
  • samples/Aspire/AppHost/Properties/launchSettings.json
  • samples/Aspire/ServiceDefaults/Extensions.cs
  • samples/Aspire/ServiceDefaults/Immediate.Jobs.Aspire.ServiceDefaults.csproj
  • samples/Aspire/readme.md
  • samples/Basic/Basic.csproj
  • samples/Basic/Program.cs
  • samples/NativeAot/NativeAot.csproj
  • samples/NativeAot/Program.cs
  • spec.md
  • src/Common/CronValidator.cs
  • src/Common/ITypeSymbolExtensions.cs
  • src/Common/JobDiscovery.cs
  • src/Common/PayloadValidation.cs
  • src/Common/Utility.cs
  • src/Immediate.Jobs.Analyzers/DiagnosticDescriptors.cs
  • src/Immediate.Jobs.Analyzers/Immediate.Jobs.Analyzers.csproj
  • src/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cs
  • src/Immediate.Jobs.Dashboard/Assets/app.css
  • src/Immediate.Jobs.Dashboard/Assets/app.js
  • src/Immediate.Jobs.Dashboard/Assets/index.html
  • src/Immediate.Jobs.Dashboard/DashboardAssets.cs
  • src/Immediate.Jobs.Dashboard/DashboardClient/index.html
  • src/Immediate.Jobs.Dashboard/DashboardClient/package.json
  • src/Immediate.Jobs.Dashboard/DashboardClient/src/App.svelte
  • src/Immediate.Jobs.Dashboard/DashboardClient/src/HistoryChart.svelte
  • src/Immediate.Jobs.Dashboard/DashboardClient/src/JobTable.svelte
  • src/Immediate.Jobs.Dashboard/DashboardClient/src/MetricCard.svelte
  • src/Immediate.Jobs.Dashboard/DashboardClient/src/app.css
  • src/Immediate.Jobs.Dashboard/DashboardClient/src/main.js
  • src/Immediate.Jobs.Dashboard/DashboardClient/tests/HistoryChart.test.js
  • src/Immediate.Jobs.Dashboard/DashboardClient/tests/JobTable.test.js
  • src/Immediate.Jobs.Dashboard/DashboardClient/vite.config.js
  • src/Immediate.Jobs.Dashboard/DashboardJsonSerializerContext.cs
  • src/Immediate.Jobs.Dashboard/DashboardState.cs
  • src/Immediate.Jobs.Dashboard/DevelopmentDashboardFilter.cs
  • src/Immediate.Jobs.Dashboard/Immediate.Jobs.Dashboard.csproj
  • src/Immediate.Jobs.Dashboard/ImmediateJobsDashboardEndpointRouteBuilderExtensions.cs
  • src/Immediate.Jobs.Dashboard/ImmediateJobsDashboardOptions.cs
  • src/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreJobStorage.cs
  • src/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreServiceCollectionExtensions.cs
  • src/Immediate.Jobs.EntityFrameworkCore/Immediate.Jobs.EntityFrameworkCore.csproj
  • src/Immediate.Jobs.EntityFrameworkCore/ImmediateJobsModelBuilderExtensions.cs
  • src/Immediate.Jobs.Generators/EquatableReadOnlyList.cs
  • src/Immediate.Jobs.Generators/Immediate.Jobs.Generators.csproj
  • src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Models.cs
  • src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Render.cs
  • src/Immediate.Jobs.Generators/ImmediateJobsGenerator.Transform.cs
  • src/Immediate.Jobs.Generators/ImmediateJobsGenerator.cs
  • src/Immediate.Jobs.Generators/JsonMetadataEmitter.cs
  • src/Immediate.Jobs.Generators/Templates/Job.sbntxt
  • src/Immediate.Jobs.Generators/Templates/ServiceCollectionExtensions.sbntxt
  • src/Immediate.Jobs.NodaTime/Immediate.Jobs.NodaTime.csproj
  • src/Immediate.Jobs.NodaTime/NodaTimeJobSchedulerExtensions.cs
  • src/Immediate.Jobs.NodaTime/NodaTimeJsonExtensions.cs
  • src/Immediate.Jobs.Shared/IJobRequest.cs
  • src/Immediate.Jobs.Shared/IJobStorage.cs
  • src/Immediate.Jobs.Shared/Immediate.Jobs.Shared.csproj
  • src/Immediate.Jobs.Shared/ImmediateJobsHealthCheck.cs
  • src/Immediate.Jobs.Shared/ImmediateJobsOptions.cs
  • src/Immediate.Jobs.Shared/InMemoryJobStorage.cs
  • src/Immediate.Jobs.Shared/JobAttribute.cs
  • src/Immediate.Jobs.Shared/JobContextPropagation.cs
  • src/Immediate.Jobs.Shared/JobExecution.cs
  • src/Immediate.Jobs.Shared/JobModels.cs
  • src/Immediate.Jobs.Shared/JobSchedulerService.cs
  • src/Immediate.Jobs.Shared/JobSchedulers.cs
  • src/Immediate.Jobs.Shared/JobTelemetry.cs
  • src/Immediate.Jobs.Shared/QueueAttributes.cs
  • src/Immediate.Jobs.Shared/ServiceCollectionExtensions.cs
  • src/Immediate.Jobs.Shared/SingleServerJobStorage.cs
  • src/Immediate.Jobs.Testing/CaptureOnlyJobScheduler.cs
  • src/Immediate.Jobs.Testing/CaptureOnlyRecurringJobScheduler.cs
  • src/Immediate.Jobs.Testing/Immediate.Jobs.Testing.csproj
  • src/Immediate.Jobs.Testing/JobTestAssertionException.cs
  • src/Immediate.Jobs.Testing/JobTestHarness.cs
  • src/Immediate.Jobs/Immediate.Jobs.csproj
  • tests/Immediate.Jobs.FunctionalTests/ContextPropagationTests.cs
  • tests/Immediate.Jobs.FunctionalTests/GeneratedJobTests.cs
  • tests/Immediate.Jobs.FunctionalTests/Immediate.Jobs.FunctionalTests.csproj
  • tests/Immediate.Jobs.FunctionalTests/Packages/DashboardPackageTests.cs
  • tests/Immediate.Jobs.FunctionalTests/Packages/NodaTimeTests.cs
  • tests/Immediate.Jobs.FunctionalTests/Packages/TestingPackageTests.cs
  • tests/Immediate.Jobs.FunctionalTests/QueueSchedulerTests.cs
  • tests/Immediate.Jobs.FunctionalTests/Storage/EntityFrameworkCoreJobStorageTests.cs
  • tests/Immediate.Jobs.FunctionalTests/Storage/QueueStorageTests.cs
  • tests/Immediate.Jobs.FunctionalTests/Storage/SingleServerJobStorageTests.cs
  • tests/Immediate.Jobs.Tests/AnalyzerTests/ImmediateJobsAnalyzerTests.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/GeneratorTestHelper.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/ImmediateJobsGeneratorTests.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ContextExtractorsGenerateOrderedCaptureRestoreMetadataAndScopedRegistrations#IJOB.global..ContextualJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.CronJobGeneratesPayloadlessRecurringScheduler#IJOB.global..CleanupSessionsJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ExplicitJobDetailsOnValueTypeUsesConstrainedByReferenceAssignment#IJOB.global..StructJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.InvokerDelegatesExecutionToImmediateHandlersPipeline#IJOB.global..WorkJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.JobWithoutContextDoesNotEmitCaptureOrRestoreCode#IJOB.global..PlainJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.NodaTimeContextUsesConfiguredGeneratedMetadata#IJOB.global..ClockJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PayloadJobGeneratesTypedSchedulerDirectInvokerAndRegistrations#IJOB.global..Example.SendEmailJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PayloadlessJobWithoutCronGeneratesDynamicRecurringScheduler#IJOB.global..TenantCleanupJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PlainRequestGeneratesJobWithoutJobDetailsAssignment#IJOB.global..PlainRequestJob.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net10.0#IJOB.ServiceCollectionExtensions.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net11.0#IJOB.ServiceCollectionExtensions.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net8.0#IJOB.ServiceCollectionExtensions.g.verified.cs
  • tests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net9.0#IJOB.ServiceCollectionExtensions.g.verified.cs
  • tests/Immediate.Jobs.Tests/Immediate.Jobs.Tests.csproj
  • tests/Immediate.Jobs.Tests/ModuleInitializer.cs
💤 Files with no reviewable changes (1)
  • .github/FUNDING.yml

Comment on lines +49 to +64
private static JsonObjectRenderModel CreateObjectModel(INamedTypeSymbol type)
{
var members = GetMembers(type);
var constructor = GetConstructor(type, members);
var constructorParameters = constructor is null
#if NETSTANDARD2_0
? ImmutableArray<IParameterSymbol>.Empty
#else
? []
#endif
: constructor.Parameters;
return new()
{
HasParameterlessCreator = constructor is null || constructor.Parameters.Length == 0,
ConstructorParameters = constructorParameters
.Select((parameter, index) => new JsonConstructorParameterRenderModel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Analyzer validation and generator constructor-binding logic have drifted apart, risking generated code that fails to compile. PayloadValidation.Visit's ctor/member matching is looser than JsonMetadataEmitter's actual member/constructor selection, and JsonMetadataEmitter.CreateObjectModel compounds this by assuming a parameterless constructor exists whenever its own stricter matching finds none.

  • src/Immediate.Jobs.Generators/JsonMetadataEmitter.cs#L49-L64: don't infer HasParameterlessCreator = true from constructor is null; explicitly check for an accessible public parameterless constructor.
  • src/Common/PayloadValidation.cs#L48-L70: restrict the ctor-binding and member-traversal checks to the same filtered property/field set JsonMetadataEmitter.GetMembers uses (exclude indexers, methods, events, nested types, static/write-only members) so a type that passes analyzer validation is guaranteed to be constructible by the generator.
📍 Affects 2 files
  • src/Immediate.Jobs.Generators/JsonMetadataEmitter.cs#L49-L64 (this comment)
  • src/Common/PayloadValidation.cs#L48-L70
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Immediate.Jobs.Generators/JsonMetadataEmitter.cs` around lines 49 - 64,
In src/Immediate.Jobs.Generators/JsonMetadataEmitter.cs lines 49-64, update
CreateObjectModel so HasParameterlessCreator is true only when an accessible
public parameterless constructor exists, not merely when GetConstructor returns
null. In src/Common/PayloadValidation.cs lines 48-70, align
PayloadValidation.Visit constructor binding and member traversal with
JsonMetadataEmitter.GetMembers by excluding indexers, methods, events, nested
types, static members, and write-only properties.

This was referenced Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants