Initial draft - #20
Conversation
📝 WalkthroughWalkthroughChangesThe 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
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested reviewers: 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
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winMake 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 liftMake orphaned context slices optional during restore.
§8 says a missing extractor should be skipped, but this generated example calls
GetRequiredServicebefore 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 treatingRestoreAsyncfailures 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 winAlign the push trigger with
master
.github/workflows/build.ymlonly fires on pushes tomain, but this repo’s branch ismaster. Pushes tomasterwon’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 liftRedact
Contextfrom public monitoring output.docs/monitoring-api.mdsays read endpoints return the publicJobRecordJSON shape, so this envelope can be exposed through/api/jobswhile carryingUserIdandTenantId. 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 winDisable persisted checkout credentials.
actions/checkoutleaves the token in.git/configby default, and the later npm/.NET steps run repository-controlled code. Setpersist-credentials: falsehere.🤖 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 winRestrict publishing to release tags.
'**'publishes packages and creates a release for every tag, whileDirectory.Build.propsconfigures MinVer to recognizev-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 winPin the workflow actions and disable checkout credential persistence. All
uses:entries here are mutable tags; pin them to full commit SHAs, and setpersist-credentials: falseonactions/checkoutso 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 liftN+1 DbContext/round-trip per acquired job in the acquisition hot path.
AcquireCandidatesAsyncopens a brand-newDbContextand issues a separateSaveChangesAsyncfor every single candidate, sequentially. BothAcquireDueJobsAsync(line 83-89) andAcquireJobsAsync(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 winUnbounded in-memory load for purge; inconsistent with the bulk-delete pattern already used in this file.
PurgeAsyncloads every matching job entity into memory (noTake/batching) beforeRemoveRange+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 withExecuteDeleteAsyncfor 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 winConstructor 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 winMake the Svelte runtime replacement quote-agnostic. The generated server module uses a quoted
svelte/internal/serverimport, so replacing only'svelte/internal/server'can leave the bare import in place and break thedata: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 winSSE loop has no resilience against transient storage failures.
StreamEventsAsynconly swallowsOperationCanceledException; any other exception thrown bystorage.GetMonitoringSnapshotAsync/QueryJobsAsync(e.g. a transient DB error in the EF Core provider) propagates unhandled and tears down the stream. Since the stream already advertisesretry: 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 winFocusable buttons inside
role="img"are hidden from assistive tech.
role="img"makes the whole SVG subtree presentational, so the<title>elements and theforeignObjectbuttons are pruned from the accessibility tree. Keyboard users still tab into 30 focusable controls per chart that screen readers cannot name, and the per-pointaria-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 torole="group"with anaria-labelso 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 winMake the derived values explicit in legacy mode.
App.svelteis still a legacy component, so the top-levelletstate is reactive; the real risk is thatvisibleJobs()andcount(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 winUse 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 beVerifierSettings.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 winAlign
CronValidatorwith Cronos grammar.CronValidatoronly 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 winGuard the registrations template against older LangVersion.
src/Immediate.Jobs.Generators/Templates/ServiceCollectionExtensions.sbntxt:28emits unconditional list-pattern and collection-expression syntax, so a project pinned to an olderLangVersioncan fail to compile even though line 14 already branches forparamssupport. 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 winUse a netstandard2.0-compatible null check here
ArgumentNullException.ThrowIfNullis not available on thenetstandard2.0target, 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 winCache
JsonTypeInfo<TPayload>instead of rebuilding it on every Serialize/Deserialize call.Every generated call site passes a factory like
options => new PayloadJsonContext(options).Payload. BecauseDeserialize/Serializeinvoke this factory (and cloneOptionsvianew(Options)) on every call, each job enqueue and each job invocation reconstructs a brand-newJsonSerializerContextand rebuilds the fullJsonTypeInfometadata graph forTPayload, defeating the whole point of source-generated JSON metadata (which is meant to be built once and reused, as the existingPayloadJsonContext.Defaultsingleton 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); ifOptionscan be swapped at runtime (e.g. viaIOptionsMonitor), 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 liftPartial 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 duplicateRecurringKey), this method reportsfalsewhile durable storage holds aPendingrecord and an advancedNextRunAt. BecauseAcquireDueJobsAsyncreads 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
_recurringKeysgrows without bound.Every materialized occurrence adds a key that is never removed — not by
PurgeAsync,DeleteAsync, or schedule removal. SinceSingleServerJobStorageusesInMemoryJobStorageas 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 theRecurringKeyof 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.Skipcan miss an active run.
Searchis a substring match overJobName(InMemoryJobStorage.QueryJobsAsync, Line 336), andTake = 1returns only the newest match. If any other active job name containsdefinition.Nameas 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 onSearch+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
JobNamefilter toJobQueryand to each provider'sQueryJobsAsync.🤖 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 winRecovery does not restore recurring-occurrence dedupe keys.
Jobs and schedules are rebuilt, but the primary's
_recurringKeysset (InMemoryJobStorage.MaterializeRecurringAsync, Line 299) starts empty, so an occurrence already materialized before the restart can be materialized a second time — the durableEnqueueAsyncthen fails on the duplicate id, or a duplicate run is created. Consider seeding the set from the recovered records'RecurringKeyvalues.🛠️ Suggested direction
foreach (var job in jobs) await recoveredPrimary.EnqueueAsync(job, cancellationToken).ConfigureAwait(false);Have
InMemoryJobStorage.EnqueueAsyncregister a non-nulljob.RecurringKeyin_recurringKeysso 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 winA lease-renewal failure in
finallymasks the job outcome.Only
OperationCanceledExceptionis swallowed at Line 316. An in-flightRenewLeaseAsyncthat lands after the job reached a terminal state throwsInvalidOperationExceptionfromGetOwnedActive(InMemoryJobStorage, Line 415); that exception escapes thefinally, replaces the successful/failed outcome, and skipsRelease(record)inDrainAsync'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 winRemove the
asynckeyword from these methods or add a real await.#pragma warning disable IDE0391doesn’t cover CS1998, and CS1998 is treated as an error here, so these methods fail the build underTreatWarningsAsErrors.🤖 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 winBackoff computation can overflow to a negative delay.
Math.Min(TimeSpan.MaxValue.Ticks, ...)compares indouble;(double)long.MaxValuerounds abovelong.MaxValue, so the subsequent(long)tickscast is an unchecked out-of-range conversion yieldinglong.MinValue. With a largerBackoffBase(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 winPreserve the root Git attributes
Root
.gitattributesnow only contains theapp.jswhitespace 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 winKeep generic user files ignored.
Removing the prior
*.userrule 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 winUse
JobQuery.IdforGetJobAsync
QueryJobsAsyncalready supports exactIdfiltering, so the fixedTake = 1000scan can miss a real job once storage grows past that window. Switch this lookup tonew() { 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 winDo not redirect production requests to an unmapped Scalar route.
/scalarexists only whenapp.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 winUse
100%for the final keyframe selector.
toviolates the configuredkeyframe-selector-notationrule.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 winCapture only W3C trace-parent values.
Activity.Current?.Idcan still be hierarchical under non-W3C ambient contexts, soTraceParentmay persist a value that won’t round-trip throughActivityContext.TryParse. Guard onactivity?.IdFormat == ActivityIdFormat.W3Cbefore storingId.🤖 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 = nullyields a null array constant and will throw when enumeratingValues.
[Handler(Tags = null)]producesKind == TypedConstantKind.ArraywithIsNull == trueand a defaultValues, which theSelectthen walks. Add anIsNullcheck.🛡️ 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.FindJobsever yields the same symbol more than once (e.g. duplicate attribute application paths),group.Count() > 1is true while no candidate differs, andFirstthrowsInvalidOperationException, which surfaces as an analyzer crash (AD0001). PreferFirstOrDefaultplus 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 winTime-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. UseInvalidConfiguration(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
⛔ Files ignored due to path filters (1)
src/Immediate.Jobs.Dashboard/DashboardClient/package-lock.jsonis 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.gitignoreBenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.DispatchBenchmarks-report-github.mdBenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.EnqueueBenchmarks-report-github.mdBenchmarkDotNet.Artifacts/results/Immediate.Jobs.Benchmarks.StartupBenchmarks-report-github.mdDirectory.Build.propsDirectory.Packages.propsImmediate.Jobs.slnxbenchmarks/Immediate.Jobs.Benchmarks/Immediate.Jobs.Benchmarks.csprojbenchmarks/Immediate.Jobs.Benchmarks/Program.csbenchmarks/Immediate.Jobs.Benchmarks/SchedulerBenchmarks.cscoverage-settings.xmldocs/context-propagation.mddocs/monitoring-api.mddotnet-tools.jsonglobal.jsonlicense.txtreadme.mdsamples/Aspire/Api/Immediate.Jobs.Aspire.Api.csprojsamples/Aspire/Api/Program.cssamples/Aspire/Api/Properties/launchSettings.jsonsamples/Aspire/AppHost/AppHost.cssamples/Aspire/AppHost/Immediate.Jobs.Aspire.AppHost.csprojsamples/Aspire/AppHost/Properties/launchSettings.jsonsamples/Aspire/ServiceDefaults/Extensions.cssamples/Aspire/ServiceDefaults/Immediate.Jobs.Aspire.ServiceDefaults.csprojsamples/Aspire/readme.mdsamples/Basic/Basic.csprojsamples/Basic/Program.cssamples/NativeAot/NativeAot.csprojsamples/NativeAot/Program.csspec.mdsrc/Common/CronValidator.cssrc/Common/ITypeSymbolExtensions.cssrc/Common/JobDiscovery.cssrc/Common/PayloadValidation.cssrc/Common/Utility.cssrc/Immediate.Jobs.Analyzers/DiagnosticDescriptors.cssrc/Immediate.Jobs.Analyzers/Immediate.Jobs.Analyzers.csprojsrc/Immediate.Jobs.Analyzers/ImmediateJobsAnalyzer.cssrc/Immediate.Jobs.Dashboard/Assets/app.csssrc/Immediate.Jobs.Dashboard/Assets/app.jssrc/Immediate.Jobs.Dashboard/Assets/index.htmlsrc/Immediate.Jobs.Dashboard/DashboardAssets.cssrc/Immediate.Jobs.Dashboard/DashboardClient/index.htmlsrc/Immediate.Jobs.Dashboard/DashboardClient/package.jsonsrc/Immediate.Jobs.Dashboard/DashboardClient/src/App.sveltesrc/Immediate.Jobs.Dashboard/DashboardClient/src/HistoryChart.sveltesrc/Immediate.Jobs.Dashboard/DashboardClient/src/JobTable.sveltesrc/Immediate.Jobs.Dashboard/DashboardClient/src/MetricCard.sveltesrc/Immediate.Jobs.Dashboard/DashboardClient/src/app.csssrc/Immediate.Jobs.Dashboard/DashboardClient/src/main.jssrc/Immediate.Jobs.Dashboard/DashboardClient/tests/HistoryChart.test.jssrc/Immediate.Jobs.Dashboard/DashboardClient/tests/JobTable.test.jssrc/Immediate.Jobs.Dashboard/DashboardClient/vite.config.jssrc/Immediate.Jobs.Dashboard/DashboardJsonSerializerContext.cssrc/Immediate.Jobs.Dashboard/DashboardState.cssrc/Immediate.Jobs.Dashboard/DevelopmentDashboardFilter.cssrc/Immediate.Jobs.Dashboard/Immediate.Jobs.Dashboard.csprojsrc/Immediate.Jobs.Dashboard/ImmediateJobsDashboardEndpointRouteBuilderExtensions.cssrc/Immediate.Jobs.Dashboard/ImmediateJobsDashboardOptions.cssrc/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreJobStorage.cssrc/Immediate.Jobs.EntityFrameworkCore/EntityFrameworkCoreServiceCollectionExtensions.cssrc/Immediate.Jobs.EntityFrameworkCore/Immediate.Jobs.EntityFrameworkCore.csprojsrc/Immediate.Jobs.EntityFrameworkCore/ImmediateJobsModelBuilderExtensions.cssrc/Immediate.Jobs.Generators/EquatableReadOnlyList.cssrc/Immediate.Jobs.Generators/Immediate.Jobs.Generators.csprojsrc/Immediate.Jobs.Generators/ImmediateJobsGenerator.Models.cssrc/Immediate.Jobs.Generators/ImmediateJobsGenerator.Render.cssrc/Immediate.Jobs.Generators/ImmediateJobsGenerator.Transform.cssrc/Immediate.Jobs.Generators/ImmediateJobsGenerator.cssrc/Immediate.Jobs.Generators/JsonMetadataEmitter.cssrc/Immediate.Jobs.Generators/Templates/Job.sbntxtsrc/Immediate.Jobs.Generators/Templates/ServiceCollectionExtensions.sbntxtsrc/Immediate.Jobs.NodaTime/Immediate.Jobs.NodaTime.csprojsrc/Immediate.Jobs.NodaTime/NodaTimeJobSchedulerExtensions.cssrc/Immediate.Jobs.NodaTime/NodaTimeJsonExtensions.cssrc/Immediate.Jobs.Shared/IJobRequest.cssrc/Immediate.Jobs.Shared/IJobStorage.cssrc/Immediate.Jobs.Shared/Immediate.Jobs.Shared.csprojsrc/Immediate.Jobs.Shared/ImmediateJobsHealthCheck.cssrc/Immediate.Jobs.Shared/ImmediateJobsOptions.cssrc/Immediate.Jobs.Shared/InMemoryJobStorage.cssrc/Immediate.Jobs.Shared/JobAttribute.cssrc/Immediate.Jobs.Shared/JobContextPropagation.cssrc/Immediate.Jobs.Shared/JobExecution.cssrc/Immediate.Jobs.Shared/JobModels.cssrc/Immediate.Jobs.Shared/JobSchedulerService.cssrc/Immediate.Jobs.Shared/JobSchedulers.cssrc/Immediate.Jobs.Shared/JobTelemetry.cssrc/Immediate.Jobs.Shared/QueueAttributes.cssrc/Immediate.Jobs.Shared/ServiceCollectionExtensions.cssrc/Immediate.Jobs.Shared/SingleServerJobStorage.cssrc/Immediate.Jobs.Testing/CaptureOnlyJobScheduler.cssrc/Immediate.Jobs.Testing/CaptureOnlyRecurringJobScheduler.cssrc/Immediate.Jobs.Testing/Immediate.Jobs.Testing.csprojsrc/Immediate.Jobs.Testing/JobTestAssertionException.cssrc/Immediate.Jobs.Testing/JobTestHarness.cssrc/Immediate.Jobs/Immediate.Jobs.csprojtests/Immediate.Jobs.FunctionalTests/ContextPropagationTests.cstests/Immediate.Jobs.FunctionalTests/GeneratedJobTests.cstests/Immediate.Jobs.FunctionalTests/Immediate.Jobs.FunctionalTests.csprojtests/Immediate.Jobs.FunctionalTests/Packages/DashboardPackageTests.cstests/Immediate.Jobs.FunctionalTests/Packages/NodaTimeTests.cstests/Immediate.Jobs.FunctionalTests/Packages/TestingPackageTests.cstests/Immediate.Jobs.FunctionalTests/QueueSchedulerTests.cstests/Immediate.Jobs.FunctionalTests/Storage/EntityFrameworkCoreJobStorageTests.cstests/Immediate.Jobs.FunctionalTests/Storage/QueueStorageTests.cstests/Immediate.Jobs.FunctionalTests/Storage/SingleServerJobStorageTests.cstests/Immediate.Jobs.Tests/AnalyzerTests/ImmediateJobsAnalyzerTests.cstests/Immediate.Jobs.Tests/GeneratorTests/GeneratorTestHelper.cstests/Immediate.Jobs.Tests/GeneratorTests/ImmediateJobsGeneratorTests.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ContextExtractorsGenerateOrderedCaptureRestoreMetadataAndScopedRegistrations#IJOB.global..ContextualJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.CronJobGeneratesPayloadlessRecurringScheduler#IJOB.global..CleanupSessionsJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ExplicitJobDetailsOnValueTypeUsesConstrainedByReferenceAssignment#IJOB.global..StructJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.InvokerDelegatesExecutionToImmediateHandlersPipeline#IJOB.global..WorkJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.JobWithoutContextDoesNotEmitCaptureOrRestoreCode#IJOB.global..PlainJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.NodaTimeContextUsesConfiguredGeneratedMetadata#IJOB.global..ClockJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PayloadJobGeneratesTypedSchedulerDirectInvokerAndRegistrations#IJOB.global..Example.SendEmailJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PayloadlessJobWithoutCronGeneratesDynamicRecurringScheduler#IJOB.global..TenantCleanupJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.PlainRequestGeneratesJobWithoutJobDetailsAssignment#IJOB.global..PlainRequestJob.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net10.0#IJOB.ServiceCollectionExtensions.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net11.0#IJOB.ServiceCollectionExtensions.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net8.0#IJOB.ServiceCollectionExtensions.g.verified.cstests/Immediate.Jobs.Tests/GeneratorTests/Snapshots/ImmediateJobsGeneratorTests.ServiceCollectionExtensionsUsesQueuesAndTaggedRegistrations_framework=net9.0#IJOB.ServiceCollectionExtensions.g.verified.cstests/Immediate.Jobs.Tests/Immediate.Jobs.Tests.csprojtests/Immediate.Jobs.Tests/ModuleInitializer.cs
💤 Files with no reviewable changes (1)
- .github/FUNDING.yml
| 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 |
There was a problem hiding this comment.
🗄️ 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 inferHasParameterlessCreator = truefromconstructor 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 setJsonMetadataEmitter.GetMembersuses (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.
Summary by CodeRabbit
New Features
Documentation
Tests