Skip to main content

Nexus v2 to v3 upgrade skill

Use this skill when upgrading an application and its libraries from Nexus v2 to v3. It is intended for an LLM working in the consuming application's repository. The overview explains the main changes; this file supplies migration instructions and the details needed to preserve behavior.

The reference is Nexus 3.0.1. Check the actual target package's interfaces when upgrading to a later v3 version.

Working without the Nexus source repository​

GitHub access is optional. The migration rules and selected contract excerpts below are included so this skill can be used in a consuming repository without a Nexus checkout. Links to other pages and GitHub are supplementary references, not prerequisites.

When an application-specific integration needs more detail than this guide supplies:

  1. Identify the exact target package versions from the application's package references and restored obj/project.assets.json. Restore the packages through its configured NuGet feeds if needed. Do not assume the latest source on main matches those binaries.
  2. Inspect the target package's XML documentation, IDE metadata view, or assemblies. project.assets.json records the resolved versions, package folders, and assembly paths. dotnet nuget locals global-packages --list also locates the global package cache; build output may already contain the required DLLs.
  3. Decompile the exact v3 assemblies when necessary, using an available .NET decompiler or the IDE's decompilation support. Start with CommerceMind.Nexus.Abstractions.dll for interfaces and records, CommerceMind.Nexus.dll for core behavior and registrations, and the applicable CommerceMind.Nexus.EntityFramework.dll, database-provider, or broker assembly for integration behavior. Follow the relevant implementation and call sites, not just its public signature. See the inspection map at the end of this guide.
  4. Record the package version and whether a conclusion came from this guide, package metadata, decompiled implementation, or matching source. If neither source nor binaries are available, identify the exact unverified contract and continue independent work; do not invent a signature or claim the integration was validated.

Do not require the user to obtain GitHub access just to inspect APIs available in their installed packages. Decompilation is for understanding the dependency; update the consuming application rather than copying or modifying Nexus internals.

Outcome and boundaries​

Produce a working v3 application, an account of intentional behavior changes, and deployment instructions for its actual database and brokers. Preserve existing queue names, job names, message contracts, schedules, retention policies, and transaction boundaries except for changes requested by the user or agreed through the questions below.

Distributed processors, named queues, Redis activations, and local-time schedules are optional features. Do not adopt them merely to complete the upgrade. Keep existing queue jobs unless the application needs a different execution model.

Code changes and local validation do not imply permission to migrate a live database, drain a broker, or deploy. Prepare those actions for the environment's normal release process. A package downgrade alone is not a rollback after v3 database migrations or broker changes.

1. Inventory the application​

Read the repository's instructions and identify every host, shared registration library, producer, worker, API client, and separately hosted Admin UI that uses Nexus. Inspect project files, central package versions, SDK selection, container images, CI, deployment manifests, and configuration sources.

Useful searches, adapted to the repository:

rg -n 'CommerceMind\.Nexus|TargetFramework|TargetFrameworks|net8\.0|net9\.0' -g '*.csproj' -g '*.props' -g 'global.json' -g 'Dockerfile*'
rg -n 'AddNexus|AddScheduledJobs|ScheduledJob|EnqueueAndStartProcessing|IQueueProcessingRequester' -g '*.cs'
rg -n 'AddSingleton|BackgroundService|IHostedService|GetRequiredService|GetService|BuildServiceProvider' -g '*.cs'
rg -n 'AddEntityFramework|UseNexus|AddDbContext|SaveChanges|CommitAsync|\.Commit\(' -g '*.cs'
rg -n 'ExternalPending|AddNexus.*Queues|EnqueueFrom|AddAdminUI|UseNexusAdminUI|AddApi|TeamsPublisher|OnInstanceEventRecieved|RequireTypeHash' -g '*.cs'

Record these decisions before changing behavior:

AreaWhat to establish
PersistenceSQL Server, PostgreSQL, SQLite, in-memory fallback, and any custom storage implementations
Queue triggeringEach queue job's actual saved schedule, whether it was manually started, and callers that explicitly started processing
Job startsCall frequency, immediate wake-up signals versus persisted requests, and how pending work is recovered after downtime
Dependency injectionSingleton/root-provider callers of Nexus, and custom replacements registered with old lifetimes
EFConfigured context, scope ownership, save/commit paths, and enqueueing that bypassed the unit of work
BrokersExternal pending storage versus ingress into Nexus; direct producers, old resources, backlog, offsets, and provisioning
SerializationDefault STJ or Newtonsoft.Json, custom converters/writers, persisted polymorphic data, and the existing type-hash salt
ClientsCustom HTTP/TypeScript clients, raw SQL, monitoring queries, and separate UI hosting

If saved schedules or broker state cannot be inspected locally, list the required environment checks in the deployment instructions. Do not assume code defaults describe an existing installation.

Also establish the Nexus version actually deployed against each database and whether its startup/queue initialization completed. Package references in the current checkout are not evidence that the required v2 deployment has run.

Questions to resolve with the user​

Inspect the application first, then ask about applicable behavior choices that the repository and existing instructions do not answer. Present actual job names, saved schedules where available, and a recommendation. Group related questions and continue mechanical API/runtime changes while awaiting answers. Do not ask the user to decide straightforward renames, repeat decisions already made, or silently treat an unanswered recommendation as accepted.

When applicableWhat to ask and recommend
Queue jobs polling every five seconds or more oftenList every queue job whose effective schedule runs at intervals of five seconds or less. Recommend converting all of them to queue jobs without schedules, so readiness starts processing. Ask which, if any, need an exception because the schedule enforces business hours, batching, downstream rate limits, or another business rule.
Other scheduled or previously manual-only queue jobsAsk which should retain scheduled processing and which should run when messages are ready. Preserve slower schedules unless a change is agreed. Explicitly ask how a previously manual-only job should behave: an empty schedule now enables readiness-driven processing.
Choosing the processing modelRecommend retaining IQueueJob/IBatchQueueJob for the upgrade. Removing frequent polling does not require a distributed queue processor. Ask about adopting processors only if the user wants their different concurrency/execution model; first explain any affected job parameters, mutexes, progress, and run history.
Explicit job-start callsEstablish call frequency and whether each call is a wake-up signal or a request that must itself be persisted. Recommend keeping frequent immediate calls on IJobStartRequester: v3 avoids extra-run database writes and the associated cache invalidations across instances. Do not convert frequent callers to scheduling extra runs as part of the upgrade. Ask about durable extra runs only for specific persisted-start requirements, explaining their additional cost and checking how the application already recovers pending work.
EF callers whose transaction boundary is unclearAsk whether the message must commit with the entity changes or be published independently. Recommend preserving the existing business transaction; do not choose immediate publishing merely to make a compile error disappear.
Queue jobs with progress reportingExplain that automatic progress now defaults to None. Ask whether users need percentages, processed counts, or no automatic progress. Recommend explicitly retaining a relied-upon display; explain the initial count query needed for percentages.
Deployment history or broker cutover is unknownAsk whether the latest v2 was deployed and initialized against every target database/queue, and what evidence is available. For incompatible broker changes, ask whether to drain or transfer existing backlog and who handles the cutover. Prepare the plan without claiming these environment steps have happened.

For the frequent-polling question, show a table with job name, queue name, environment, code default, actual saved schedule, proposed change, and any known restrictions. For six-field cron expressions, * * * * * * runs every second and */5 * * * * * every five seconds: both qualify. Expand application schedule helpers and inspect effective saved schedules rather than matching expression text. Use the actual cron dialect and timezone when evaluating cadence. A restricted-hours schedule may poll every five seconds while active, but removing it also removes that restriction; make this consequence explicit.

An example question, populated with the jobs actually found:

These queue jobs poll every five seconds or faster: [job names and effective schedules]. I recommend removing the schedule from all of them so they process when messages are ready. This also means clearing their saved schedules in each environment. Should any retain a schedule because they enforce processing windows, batching, or downstream limits?

If saved schedules are inaccessible, identify candidates from code but mark their environment state unverified. Record accepted changes, exceptions, and unresolved choices. Without an answer, preserve the existing configured behavior where possible and leave the proposed behavior change pending; explicitly flag v3 defaults, such as an empty schedule becoming readiness-driven, that prevent automatic preservation.

2. Update the runtime and packages​

Required intermediate deployment​

For every existing installation, deploy and run the latest Nexus v2 release before deploying v3, even if the application is already on an earlier v2 release. The supported upgrade path requires this separate deployment so v2's database migrations and compatibility work complete before v3 adopts and caches the schema.

  1. Verify the version actually running in each environment. If the latest v2 has not already been deployed successfully, prepare that deployment first.
  2. Let startup migrations and queue initialization finish against every Nexus database being upgraded. When different hosts register different queue sets, run the relevant hosts against those databases so all queue initialization is covered. Resolve failed or incomplete initialization before proceeding.
  3. Record the deployed v2 version and evidence of successful initialization in the deployment plan. Updating package references, compiling v2, or testing against a different database does not satisfy this prerequisite for the target environment.

If the latest v2 is already deployed and fully initialized, no repeat deployment is needed. If deployment history cannot be verified, code preparation for v3 can continue, but report the prerequisite as unresolved and do not mark the v3 rollout ready. Fresh v3 installations with no existing Nexus data do not need an intermediate v2 deployment.

Runtime and package changes​

All Nexus v3 packages target .NET 10. Update consuming projects to .NET 10 or later, and align the SDK, runtime/ASP.NET hosting bundle, containers, and CI. Upgrade the CommerceMind.Nexus.* packages together, including abstractions, storage providers, EF, UI, API client, serializers, and integration packages actually in use. Check transitive references from shared libraries too.

V3 cannot upgrade v1 directly. An installation still on v1 must complete the same latest-v2 deployment first. V3 no longer adds v1 queue priority columns or rewrites old serialized type/assembly names. Inspect persisted queue messages, job parameters, function arguments, instance events, health-check results, and integration state when v1 data may remain. Successful schema initialization alone does not prove every old payload has been rewritten; process or migrate remaining v1 payloads through v2.

Remove NexusOptions, SkipV1Compatibility, and the AddNexus(Action<NexusOptions>) overload:

// v2
services.AddNexus(options => options.SkipV1Compatibility = true);

// v3
services.AddNexus();

The public JsonTypeName.UpdateMovedTypeNames helper was also removed. Do not recreate v1 compatibility inside the application's normal v3 startup.

3. Migrate job APIs without changing job identity​

Most schedule-neutral names lost the Scheduled prefix. Namespaces generally remain under CommerceMind.Nexus.Abstractions.Jobs, .Jobs.Attributes, and .Jobs.Queueing; registration remains under CommerceMind.Nexus.Jobs.Extensions.

V2 APIV3 API
AddScheduledJobs()AddJobs()
ScheduledJobsOptionsJobsOptions
ScheduledJobsOptions.ScheduledJobFilterJobsOptions.JobFilter
ScheduledJobsOptions.GeneratedScheduledJobsJobsOptions.GeneratedJobs
[ScheduledJob] / ScheduledJobAttribute[Job] / JobAttribute
IScheduledQueueJob<T> / <T, TParameters>IQueueJob<T> / <T, TParameters>
IScheduledBatchQueueJob and its generic variantsIBatchQueueJob and its generic variants
IScheduledQueueJobBase / <T>IQueueJobBase / <T>
IScheduledQueueJobExecutorIQueueJobExecutor
GeneratedScheduledQueueJob<T> / IGeneratedScheduledQueueJobGeneratedQueueJob<T> / IGeneratedQueueJob
GeneratedScheduledQueueJob<T>.ScheduledJobBaseTypeGeneratedQueueJob<T>.JobBaseType
IScheduledJobMetaDataRepositoryIJobMetaDataRepository
IScheduledJobExecutor / IScheduledJobExecutorServiceIJobExecutor / IJobExecutorService
IScheduledJobMiddlewareIJobMiddleware
IScheduledJobMetrics / IScheduledJobMetricsFactoryIJobMetrics / IJobMetricsFactory
ScheduledJobDescriptor / ScheduledJobDescriptorExtensionsJobDescriptor / JobDescriptorExtensions
ScheduledJobDescriptor.ScheduledJobTypeJobDescriptor.JobType
ScheduledJobMetaData, ScheduledJobCategory, ScheduledJobMutexJobMetaData, JobCategory, JobMutex
ScheduledJobCurrentRunDetails, ScheduledJobRunDetailsJobCurrentRunDetails, JobRunDetails
ScheduledJobHistoricalRunDetails, ScheduledJobExtraRunJobHistoricalRunDetails, JobExtraRun
ScheduledJobRunInitiator, ScheduledJobRunOrderJobRunInitiator, JobRunOrder
ScheduledJobProgress, ScheduledJobHealthCheckDataJobProgressDetails, JobHealthCheckData

Do not apply a global ScheduledJob to Job replacement. Ordinary cron jobs still use IScheduledJob, IScheduledJob<TParameters>, and IScheduledJobBase. NextScheduledJobRun also keeps its name. The common base for both ordinary scheduled jobs and queue jobs is now IJobBase; generic infrastructure intended for both should use it. Code that assumes every queue job is an IScheduledJobBase must be updated.

Preserve explicit attribute/generated names. If a job was named by its class, keep that class name or set [Job("existing-name")] before renaming it. Job names identify schedules, history, parameters, and administrative state. Queue names identify storage. When renaming application classes, update matching filenames according to repository conventions.

Removed members and repository changes​

  • JobAttribute and JobDescriptor no longer have the old Description property. Use ShortDescription for the code-defined summary. This does not remove editable JobMetaData.Description.
  • StopProcessingOnError was removed from queue-job interfaces. Set [Job(PauseOnError = true)] or the generated job's PauseOnError. Check saved settings that may override the code default.
  • IScheduledFilteredQueueJobBase, IScheduledFilteredQueueJob<...>, and IScheduledFilteredBatchQueueJob<...> were removed. Preserve the filter through generated queue routing (ShouldEnqueue) or an explicit GeneratedQueueJob<T>.MessageFilter, according to whether the filter selects the destination or messages to lease. The generated job filter is not derived from job parameters. Preserve each existing queue/job binding and verify its selected messages; do not silently replace filtered processing with an unfiltered job.
  • The unscoped ScheduledJobInstance property was removed. Use JobDescriptor.ScopedJobInstance, inspect its exception result, and dispose its returned scope after using the job. The old scoped member was named ScopedScheduledJobInstance.
  • Job metadata reads no longer load progress by default. Pass loadJobProgress: true to GetJobsMetaDataAsync() or the GetJobMetaDataAsync() overload when the caller needs it.
  • TrySetAsRunningAsync() takes JobRunStart, containing StartedAtUtc, InstanceId, Initiator, Parameters, and CorrelationId. JobCurrentRunDetails has an implicit conversion, but custom repository implementations must implement the new signature.
  • Historical-list queries take JobHistoricalRunQuery and return JobHistoricalRunResult with RunDetails and TotalCount. The result can still be deconstructed. Transfer all old filtering, ordering, and paging arguments into the query object.
  • Historical runs no longer expose a database Id. Use CorrelationId.
  • JobExtraRun.Id, extra-run deletion arguments, and HTTP extra-run IDs are long, not int.
  • Custom job repositories must implement both GetMessageFilterAsync() overloads; their default interface implementations were removed. Review the complete target interface for queue-job and nullable-schedule changes.

4. Preserve queue triggering and job-start guarantees​

Queue jobs​

An IQueueJob/IBatchQueueJob has no default schedule. A job with no saved schedule is readiness-driven: committed pending messages request activation, and activation starts processing. A job with a cron schedule is scheduled-only. Manual/extra runs remain available.

Replace every EnqueueAndStartProcessingAsync(...) with the equivalent EnqueueAsync(...), preserving context and destination. The old methods, IQueueProcessingRequester, and its implementation were removed. Advanced integrations use IQueueReadinessNotifier for queue readiness; do not replace normal enqueueing with a manual job-start event.

Choose the intended behavior for each existing queue job:

  • To preserve scheduled-only processing, retain its saved cron schedule. Enqueueing does not bring that run forward.
  • To start on readiness, omit DefaultSchedule for new jobs and clear the persisted schedule on existing jobs through the Admin UI/API or IJobMetaDataRepository.UpdateJobScheduleAsync(jobName, null). Changing the class default alone does not update an existing database row.
  • A previously manual-only queue job with an empty schedule now qualifies for readiness-driven processing. If it must remain manual, explicitly design that behavior using disabling/enabling or a suitable schedule and manual runs; do not assume an empty schedule still means manual-only.
  • If an old enqueue-and-start call intentionally woke a scheduled queue job before its next cron tick, enqueue and then use IJobStartRequester to request that run. Keep frequent wake-up calls on the fast requester path. Use a durable extra run only when the start request itself specifically needs persistence, rather than treating every enqueue as a reason to write an extra run.

Convert frequent polling to readiness​

Recommend removing the schedule from every queue job scheduled to run every five seconds or more often, subject to the user's decisions above. This means a polling interval of at most five seconds, including exactly five seconds. It applies to queue-processing jobs, not ordinary scheduled jobs that perform periodic work without a queue.

For each agreed conversion:

  1. Keep its IQueueJob/IBatchQueueJob implementation, explicit job and queue names, message contracts, filters, and processing logic. Remove its DefaultSchedule override, or set the generated queue job's Schedule to null. Check shared base classes and registration helpers for inherited schedule defaults too.
  2. Prepare an explicit list of the existing saved schedules to clear in each environment. Removing the code default alone does not change existing metadata. Use the Admin UI/API or the scoped IJobMetaDataRepository.UpdateJobScheduleAsync(jobName, null) during the coordinated v3 cutover, after the required latest-v2 deployment and v3 initialization. Clear only the jobs chosen for conversion. A null schedule enables readiness; it does not disable the job.
  3. Preserve disabled states and pause-on-error settings. Verify the configured activation provider is shared by producers and workers, and that enqueueing requests readiness after commit. Do not retain a polling timer or add a manual start on every enqueue to emulate the old schedule.
  4. Review MaxNumberOfMessagesPerRun, batching, and concurrency if the old run interval limited throughput. A job without a schedule can start again as work becomes ready; a per-run message limit does not impose the old time-based rate limit. If a business-hours window or downstream limit is required, agree how to retain it before removing the schedule.
  5. Validate committed enqueueing, delayed messages, retries, an already populated queue at cutover, and idle behavior. Confirm the saved schedule is empty and new processing does not wait for a cron tick. Report both the code change and the per-environment metadata change, including any deferred execution.

Do not automatically replace a five-second schedule with a five-second ActivationDelay. Activation delay is an optional fixed collection window with different semantics; choose it only when the user wants that behavior.

Relevant v3 contract excerpts​

These are selected members from the v3 contracts, included for offline reference. They omit other members and are not replacement interfaces to add to the application. Types are under CommerceMind.Nexus.Abstractions.Jobs, .Jobs.Queueing, and .Queueing.

// IQueueJobBase : IJobBase
string? DefaultSchedule => null;
QueueJobProgressMode AutomaticProgressMode => QueueJobProgressMode.None;
int NumberOfProcessingThreads => 1;
TimeSpan MessageLeaseTime => TimeSpan.FromMinutes(30);
int MaxNumberOfMessagesPerRun => int.MaxValue;

// IJobMetaDataRepository
Task<List<JobMetaData>> GetJobsMetaDataAsync(
bool bypassCache = false, bool loadJobProgress = false);
Task UpdateJobScheduleAsync(string jobName, string? cronSchedule);
Task ScheduleExtraRunAsync(
string jobName, TimeSpan startIn, JobRunInitiator initiator,
object? parameters = null, string? instanceId = null);
Task ScheduleExtraRunAsync(
string jobName, DateTime startAt, JobRunInitiator initiator,
object? parameters = null, string? instanceId = null);

For example, inside an existing application scope, this reads saved metadata without the repository cache and clears one selected queue job's schedule:

var jobs = await jobMetaDataRepository.GetJobsMetaDataAsync(bypassCache: true);
// Inspect jobs' Name and Schedule; use the agreed existing job name below.
await jobMetaDataRepository.UpdateJobScheduleAsync("existing-queue-job-name", null);

Prepare this as a deliberate cutover operation, not an unconditional startup loop that overwrites later user edits. The implementation normalizes null to an empty stored schedule; queue-job activation checks for an empty/whitespace schedule. Public metadata therefore need not return null after clearing it.

Activation and progress​

Queue readiness and IJobStartRequester are different mechanisms. Queue activation retains coalesced readiness state; a job-start event does not.

SQL Server, PostgreSQL, and SQLite providers register database activation storage automatically. AddRedisQueueActivations(...) selects Redis instead and is independent of AddRedisExternalPendingStorage(...). AddMemoryQueueActivations() selects process-local activation state for a single-instance deployment and does not survive a restart. Use a consistent shared activation provider and namespace on producers and workers. Only one explicitly selected activation provider is supported.

Activation is requested after the queue operation commits; do not assume an atomic enqueue-plus-activation transaction merely because both stores use SQL. A crash between commit and activation is recovered by reconciliation. JobsOptions.QueueActivationReconciliationInterval defaults to ten minutes, and QueueActivationFailureDelay defaults to five seconds. Reconciliation is a correctness fallback, not a substitute for correct producer readiness.

ActivationDelay is a fixed burst-collection window, not a sliding debounce. Additional enqueues do not extend the pending deadline. ProcessingDelay/ProcessAtUtc determines when each message is eligible. Configure activation delay on the queue attribute/named or generated definition, or through queue metadata/API.

Automatic queue-job progress defaults to QueueJobProgressMode.None. Set this property to retain percentages:

public QueueJobProgressMode AutomaticProgressMode => QueueJobProgressMode.Percentage;

ProcessedCount avoids the initial pending-count query. All automatic modes aggregate updates per lease/batch; injected IJobProgress still works. External queue counts may be approximate and may require broker-management permissions.

Explicit job starts​

Keep frequent immediate job-start calls on IJobStartRequester. The v3 implementation deliberately makes these calls faster by broadcasting an instance event instead of writing an extra run to the database. This also avoids invalidating the extra-run cache on every instance when an extra run is scheduled and completed. Replacing frequent calls with ScheduleExtraRunAsync() would give up that improvement; do not make that replacement as a routine upgrade step.

RequestJobStartAsync(jobName) and the parameterless generic overload use this fast path. Calls whose actual parameters and start time are both null also take it, even if a parameter-taking overload was used. The event is a best-effort wake-up signal: requests can be lost during downtime, coalesced, or cause successive runs on different instances. A request received while the job is running can trigger another run afterward, so it can pick up work that arrived near the end of the current run. The job mutex still prevents concurrent runs of the same job; one request does not guarantee exactly one run.

Ask about the caller's purpose and frequency before changing its execution model. Distinguish a signal to process work already stored elsewhere from a start request that is itself the only record of required work. Check the application's existing recovery path for missed signals, such as a later request or a periodic job run. A job having no schedule is a reason to inspect recovery, not an automatic reason to replace its frequent start calls with database writes. If a frequent caller needs stronger recovery, discuss recovery of its persisted work while retaining fast wake-up signals rather than automatically persisting an extra run per call.

Only continuous job loops handle the event. Use IJobMetaDataRepository.ScheduleExtraRunAsync() when a particular start request must be persisted, or when the host relies on RunAllJobsPendingSinceLastCallAsync() and therefore cannot handle these events. Explain the persistence/performance tradeoff and limit the change to those specific callers:

await jobMetaDataRepository.ScheduleExtraRunAsync(
"existing-job-name", TimeSpan.Zero, JobRunInitiator.Manual);

Requests with a non-null parameter object or an explicit delay/start time still persist extra runs and require AddJobs() plus job-database access. Preserve these semantics where needed. Do not add dummy parameters, TimeSpan.Zero, or an explicit current timestamp to frequent immediate calls merely to force persistence; that bypasses the fast path. Do not rely on passing null to make a request durable.

An event-only producer can use IJobStartRequester without AddJobs() or a local job registration when it configures the shared instance-event broker. It must use the correct target name. Generic requests fall back to [Job("name")] or the class name; multiple local bindings require an explicit name. Generated/per-queue names may differ from the class name.

Schedule timezone​

JobsOptions.ScheduleTimeZone is a TimeZoneInfo and defaults to UTC. Preserve UTC during a behavior-preserving upgrade. To intentionally use local wall-clock schedules:

nexus.AddJobs(options =>
{
options.ScheduleTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm");
});

This affects all cron schedules, including ones saved through the UI/API. Configure the same effective timezone on every processing instance. TimeZoneInfo.Local depends on the server configuration; named IDs must resolve on the target host. Timestamps and extra-run dates remain UTC. Verify daylight-saving boundaries using the actual cron expressions; local daily schedules follow Cronos's gap/overlap handling. Do not convert stored UTC timestamps to local time.

5. Repair dependency injection lifetimes​

Operational Nexus services are generally scoped in v3: enqueuers/readers/updaters, repositories, functions, job execution, serializers, health-check implementations, and integration operations. Process-wide state such as INexusInstance, IInstanceEventBroker, IGracefulShutdownService, and IDateTime remains singleton. Inspect actual registrations for custom overrides rather than assuming every Nexus type has changed.

Find singleton constructors, hosted services, cached delegates, static holders, and root-provider resolutions that capture scoped Nexus services, including indirect captures through transient dependencies. Either move an operation into an appropriate existing scope or create a scope for the independent operation:

public class QueuePublisher(IServiceScopeFactory scopeFactory)
{
public async Task PublishAsync(MyMessage message)
{
await using var scope = scopeFactory.CreateAsyncScope();
var enqueuer = scope.ServiceProvider.GetRequiredService<IEnqueuer<MyMessage>>();
await enqueuer.EnqueueAsync(message);
}
}

This example assumes enqueueing should persist immediately without EF interception. With EF enabled, apply the unit-of-work rules below. Keep scopes alive until asynchronous work finishes; do not return a scoped service or fire-and-forget its work. Controllers already have a request scope. Nexus job/function execution supplies a scope too.

Do not make every application service scoped automatically. Keep singleton state where required and create scopes around its independent operations. Do not suppress DI validation to hide a lifetime error. Validate the application's service graph with scope validation enabled in a local/test host.

Nexus's scoped hosted-service wrapper retains a scope for the hosted worker's lifetime; it does not make every scoped dependency a per-message dependency. Queue-job processing threads can still share the same job instance. Preserve thread safety and never run concurrent EF operations on one context. Distributed processors instead create a fresh scope and processor instance for each leased chunk.

6. Preserve Entity Framework transaction boundaries​

Apply this section only when the application uses CommerceMind.Nexus.EntityFramework.

The integration no longer finds contexts through global diagnostic-event tracking. It resolves the one configured context type from the enqueuer's DI scope. Register the integration once, register that context as scoped, avoid AddDbContextPool, and use the scoped provider:

services.AddNexus().AddEntityFramework<MyDbContext>();

services.AddDbContext<MyDbContext>((serviceProvider, options) =>
{
options.UseSqlServer(connectionString);
options.UseNexus(serviceProvider);
});

Database enqueue operations through IEnqueuer, IEnqueuer<T>, IDatabaseEnqueuer<T>, and IObjectEnqueuer now join the same scoped unit of work. In-memory queue additions happen after commit. Inspect all enqueue sites, including non-generic/batch/virtual/generated paths, for code that previously relied on a bypass.

SituationV3 behavior / required action
Application updates entities and enqueuesResolve context and enqueuer in the same scope; call SaveChangesAsync()
Explicit EF transactionSave and commit asynchronously; readiness is requested after commit
Context disposed without save/commitPending database/in-memory enqueue operations are discarded
Save failsPending messages remain with pending entity changes for a later successful save
Save succeeds, transaction later rolls backQueue operations from that transaction are discarded and are not replayed by a later save
Synchronous save/commit with pending operationsRejected; change to async throughout the call chain
Remote API or external-pending enqueueSent immediately; cannot roll back with EF, even for external messages temporarily stored in SQL
Queue updater/other Nexus operationsNot automatically enrolled by enqueue interception; use an explicit supported transaction when needed

A singleton's new scope cannot join an arbitrary caller's context. Move atomic entity-plus-enqueue work into a scoped application operation that owns both. Do not use an immediate-operation scope to work around that requirement.

For independent infrastructure publishing that must persist before returning without saving EF, use:

await using var scope = scopeFactory.CreateAsyncScope();
using var immediate = new ImmediateQueueOperationScope();
var enqueuer = scope.ServiceProvider.GetRequiredService<IEnqueuer<MyMessage>>();
await enqueuer.EnqueueAsync(message);

ImmediateQueueOperationScope is in CommerceMind.Nexus.Abstractions.Queueing. Nexus already uses it for API/MCP enqueue endpoints, broker ingress, and hybrid-storage transitions. Use it deliberately for standalone operations; normal application writes should join EF.

When remote/external delivery must follow an entity transaction, enqueue into a database queue as an outbox and forward after commit. Make forwarding safe to retry; an external publish and local acknowledgement are not one atomic transaction.

7. Migrate queue operations and custom implementations​

Batch request records and reads​

Tuple-based queue operations were replaced with records in CommerceMind.Nexus.Abstractions.Queueing:

OperationRequest type
Enqueue with contextEnqueueRequest<TMessage>(Message, Context)
Delete by ID/versionQueueItemDeleteRequest(Id, Version)
Mark processedProcessedQueueItemUpdate<TMessage>(Item, ProcessingLog, ProcessAgainAtUtc, NewPriority, UpdatedMessage)
Mark failedFailedQueueItemUpdate<TMessage>(Item, Error, RetryAfter, StatusWhenMaxRetried, UpdatedMessage)
Set statusQueueItemStatusUpdate<TMessage>(Item, Status, ProcessingLog, UpdatedMessage)
Release leaseQueueItemRelease<TMessage>(Item, NewPriority, UpdatedMessage)

For example:

await enqueuer.EnqueueAsync(messages.Select(message =>
new EnqueueRequest<MyMessage>(message, new EnqueueContext
{
ProcessAfter = TimeSpan.FromMinutes(5)
})));

Single-message enqueue methods now accept EnqueueContext?, typically defaulting to null. IObjectEnqueuer batch requests use EnqueueRequest<IQueueMessage>. Custom enqueuers must implement the new request-based methods and named-destination methods required by their interface.

The non-generic IQueueReader.ReadAsync() now puts filter before sort/paging arguments and returns QueueReadResult (QueueItems, TotalCount, still deconstructable). Revisit positional calls. Typed reader overloads still return lists; do not rewrite those as though they return QueueReadResult.

Options and storage members​

  • QueueAttribute.KeepProcessedMessagesFor and QueueSystemOptions.KeepProcessedMessagesFor become ProcessedMessagesRetention. Update named attribute arguments such as [Queue(ProcessedMessagesRetention = "10")] too; the attribute value remains a duration string.
  • QueueDescriptor.KeepProcessedMessagesFor was removed; use ProcessedMessagesRetention.
  • Obsolete InMemory properties on attributes, generated queues, descriptors, and IGeneratedQueue were removed. Use StorageMode = QueueStorageMode.InMemory.
  • IQueueReader.QueueDisplayName was removed; use the queue descriptor's DisplayName.
  • IQueueStorage.QueueDisplayName, KeepProcessedMessagesFor, MaxRetries, and HistoryRetention were removed. Read descriptor defaults from Descriptor or current settings from IQueueMetaDataRepository.
  • Table names moved to IDatabaseQueueStorage: custom database implementations must supply TableName, HistoryTableName, and StatisticsTableName.
  • Statistics reader/writer methods now take IDatabaseQueueStorage.
  • QueueItem.MessageObject, MessageObjectOrThrow, and PreviousMessageObject are IQueueMessage, not object.
  • Custom IQueueItemUpdater implementations must supply the previously defaulted ID/version delete methods, typed release methods, and new batch methods.
  • Custom queue metadata repositories need SetActivationDelayAsync(). Implement saved processing settings too if runtime overrides are supported; the interface's default setter throws.
  • QueueSystemOptions.DisableHistory now wins over saved metadata. Review deployments that set it while expecting history enabled through the Admin UI; history tables/data may be removed during initialization.

AwaitingRelease and custom queue processing​

For database and in-memory messages with identity, enqueueing a new version while leased now sets AwaitingRelease and retains the original lease. The new version cannot be leased concurrently. The latest enqueue determines the deferred payload, priority, processing date, and StatusAfterRelease; subsequent enqueues replace those deferred values.

When the original processor completes or releases, apply the latest enqueue's destination. Its payload/priority/rescheduling/failure updates must not overwrite the newer enqueue. Successful processing still records the processed version as the previous message, even with zero processed-message retention. Expired leases follow normal abandonment and RetryAbandonedMessages rules. External pending storage retains provider-specific behavior and does not gain this protection.

Update custom status switches, count displays, dashboards, raw SQL, serializers, and custom storage/updater implementations. Treat AwaitingRelease as an active lease, not ordinary pending work. QueueItem.StatusAfterRelease exposes its deferred destination. Do not force it to Pending merely to make an old status switch compile.

Custom committers and external providers​

IQueueOperationCommitter.AddOperationAsync() now accepts a QueueOperation record. Its ExecuteAsync performs database work; AfterCommitAsync must run for every committed operation; GetReadyQueueName is evaluated after that callback succeeds. Coalesce readiness per queue only within the committed unit of work. Rolled-back operations must not publish readiness. In-memory callbacks must finish before consumers are woken.

Custom external-pending providers should supply an IExternalQueueReadinessBridge that durably observes readiness without leasing/settling processing messages, and acknowledges notifications only after activation succeeds. IExternalPendingStorage.MarkProcessedForIdempotencyBatchAsync() has a default per-message fallback; override it for native batching if useful. Review queue-aware IQueueMetricsFactory overloads if custom metrics must distinguish named queues or distributed processors.

8. Update virtual queues and broker ingress​

Both sync and async virtual transformers must accept EnqueueContext? and return TransformResult<TDestinationMessage>. The obsolete one-argument methods were removed:

public TransformResult<ExportMessage> Transform(SourceMessage message, EnqueueContext? context)
{
return new(new ExportMessage { Id = message.Id }, context);
}

Returning a result with no replacement context inherits the incoming context. A null transformed message/empty destinations filters it out. Explicit queue destinations and TransformResult<T>.ToMany(...) are available when needed. Fan-out to several destinations does not create an atomic cross-provider transaction, and partial delivery can repeat on retry. Custom implementations of the source-only transformer interface must implement TransformDestinationsAsync().

Kafka/RabbitMQ ingress registrations now distinguish the wire type from the Nexus payload type. Removed overloads accepted Func<string, TMessage> or a runtime Type. Use the typed overload, with the Nexus type first:

kafkaQueues.EnqueueFrom<MyNexusMessage, MyWireMessage>(
"source-topic", message => new MyNexusMessage { Id = message.Id });

rabbitQueues.EnqueueFrom<MyNexusMessage, MyWireMessage>(
sourceQueue, message => new MyNexusMessage { Id = message.Id });

If raw text is required, explicitly choose the wire type/deserializer appropriate to the payload; do not assume the old string lambda still receives raw JSON. IKafkaQueueMessageDeserializer.Deserialize() and IRabbitMqQueueMessageDeserializer.Deserialize() return object?. Custom Azure deserializers must implement DeserializeArray(); the old default implementation is gone.

Ingress registrations can take nexusQueueName to choose a concrete or virtual destination. Keep existing source subscriptions/queues unless the upgrade explicitly requires changing them. This ingress API is separate from Nexus external pending storage.

9. Plan external pending storage cutover​

Apply this section for QueueStorageMode.ExternalPending. Database migration does not move broker messages or create readiness for old resources. Inventory direct producers and backlog before cutover. Prefer draining through v2 when practical; otherwise prepare an explicit data-transfer plan preserving message identity, payload, delays, priority, and provider state. Do not delete old resources until the backlog and any required idempotency state have been accounted for.

ProviderV3 processing/readiness contractUpgrade action
RedisReady set {KeyPrefix}{queue-name}, delayed/ID/idempotency keys in the same hash slot, plus a readiness stream. Literal braces surround the logical queue name. With defaults: nexus:queue:{orders} and nexus:queue:{orders}:readiness.Old unbraced keys are different resources. Drain or migrate ready/delayed messages and needed identity/idempotency state. Direct writers must atomically add ready messages and readiness records.
RabbitMQDurable fan-out exchange {ExchangeNamePrefix}{queue-name} bound to processing and readiness queues. Defaults for orders: exchange nexus:external-pending:orders, processing queue nexus:orders, readiness queue nexus:orders:readiness.Publish to the exchange. Existing messages in the processing queue have no new readiness copy; drain or deliberately wake/reconcile them. Update direct writers and permissions for declarations/bindings.
KafkaSame topic, independent processing and readiness consumer groups. Default topic: nexus-orders.Configure distinct group IDs and retain the intended processing offsets. Readiness commits must never advance processing offsets. Grant access to both groups.
Azure Service BusTopic {QueueNamePrefix}{queue-name} with processing and readiness subscriptions. Defaults: topic nexus-orders, subscriptions processing and readiness.Old queue entities are not consumed by v3. Drain or migrate them and switch writers to the topic. Provision both subscriptions before sending. Nexus needs Manage permission if it creates topology.

Redis's literal cluster hash tag must match on all related keys. Do not assume RENAME works across Redis Cluster slots, and do not migrate only the ready set while ignoring delayed/ID/idempotency structures. The provider's idempotency sorted-set suffix is :idempotent; its full key also gains the braces in v3. Inspect provider code when transferring those records.

Broker bodies contain the Nexus external-pending envelope, not merely the application payload:

{
"MessageJson": "{\"OrderId\":\"123\"}",
"MessageId": "123",
"Priority": 0
}

MessageJson is serialized payload JSON inside the envelope string. See external pending storage for the direct Redis enqueue script and provider contracts. Nexus IEnqueuer already writes the v3 topology correctly. Redis direct producers needing full ID replacement/idempotency should use it rather than a minimal sorted-set script.

Provider bridges acknowledge their notification only after durable activation; they do not settle the processing copy. Check the NexusExternalQueueReadiness_{ProviderId} health check. Reconciliation can recover missed notification signals for the current processing resource, but cannot discover a message left in an old Redis key or Service Bus queue entity.

10. Consider optional processors, named queues, and registration changes​

Do not convert existing jobs automatically. When requested, distributed IQueueProcessor<T>/IBatchQueueProcessor<T> implementations live in CommerceMind.Nexus.Abstractions.Queueing.Processing and register through AddQueueProcessors() after AddQueues() (and after AddJobs() when both are used).

Distributed processors run ConcurrencyPerInstance workers on every instance. They use atomic leases, are at-least-once, and provide no ordering across workers/instances. Set lease time above the maximum expected processing time of the whole chunk. Single-message processors default to LeaseSize = 1; batch processors default to lease size 50, capped by batch size. Each leased chunk has a new scope/processor instance invoked sequentially. This is different from a multithreaded queue job sharing its job instance.

Processors have queue-level pause/status controls, not job runs, schedules, parameters, mutexes, or job progress. In-memory queues are unsupported. A concrete queue cannot have both a queue job and distributed processor. Account for total concurrency across queues and hosts before opting in.

Named queues (NamedQueue<T> in QueueSystemOptions.NamedQueues) explicitly share one payload type. Adding a named definition suppresses automatic ordinary concrete discovery for that type. Enqueue by name even if only one named queue exists; unqualified typed DI cannot select a named destination. Use IQueueResolver.GetEnqueuer<T>(name), GetReader<T>(name), or GetUpdater<T>(name) for a bound accessor. IApiEnqueuer<T> also supports named destinations without local storage registration.

One ordinary processing implementation binds to every matching concrete queue. Named job bindings default to {job-name}_{queue-name}; existing single ordinary queue jobs retain their names. Use explicit generated job definitions to preserve previous per-queue job names when changing registration. Single-message handlers can inspect QueueProcessingContext.QueueName; batches expose QueueName and Context.

Existing generated queues require migration​

This is required for applications already using generated queues, even if they do not adopt any additional named queues. GeneratedQueue<T> now inherits the named queue model. Nexus no longer emits derived message or job types. Make abstract message contracts and generated job implementations concrete; open generic job implementations can still be closed over the original message type. Remove dependencies on generated runtime type names or casts to emitted subclasses. Preserve ordinary serialized payload compatibility and verify old messages remain readable. If several old filtered jobs shared one physical queue, v3's one-processing-job-per-queue rule requires an explicit redesign; do not discard or redistribute its existing backlog implicitly.

Keep each generated queue's existing Name and each generated job's explicit Name and QueueName. Rename ScheduledJobBaseType to JobBaseType and GeneratedScheduledJobs to GeneratedJobs. Set a shared RoutingQueueName on generated definitions whose ShouldEnqueue predicates previously selected destinations. Send messages to that routing name:

new GeneratedQueue<LanguageMessage>
{
Name = "language_sv",
DisplayName = "Swedish",
RoutingQueueName = "languages",
ShouldEnqueue = message => message.Language == "sv"
};

// Evaluates ShouldEnqueue for destinations sharing this entry point:
await enqueuer.EnqueueAsync("languages", message);

// Goes directly to this queue, without evaluating the routing predicate:
await enqueuer.EnqueueAsync("language_sv", message);

Type-only enqueueing no longer broadcasts to generated queues or evaluates ShouldEnqueue. Audit producers, including API/broker ingress and virtual transformers. Preserve the distinction between a routing predicate evaluated at enqueue time and a generated job's MessageFilter evaluated when leasing. See generated queues for a full registration example.

Keep names stable and unique ignoring case. Concrete storage names use letters, digits, and underscores; virtual names can also contain hyphens/periods. Nexus validates backing-table collisions, including provider truncation. Message identity/history/idempotency are independent in each queue, while validators and priority calculators remain type-based.

Repeated AddQueues()/AddJobs() calls share options and collect contributions. Append with Add/AddRange; assigning a new list replaces previous libraries' entries. Scalars use the last value. Each scan captures its filter at that call; duplicate discoveries do not duplicate registration. Build() registers all queues before jobs/processors and rejects duplicate names and conflicting bindings. Call it after all contributions. Generated job definitions still require their queue declarations before that AddJobs() call. AddQueues() alone does not enable processing.

Runtime processing overrides are saved per queue. For jobs they include threads, lease time, and batch size; distributed processors also expose worker count and lease size. Null override fields use code defaults; posting {} to /api/queues/{queueName}/processing-settings resets overrides. Active jobs keep their current snapshot until the next run. Distributed pools finish current work then rebuild; existing lease expiries do not change. Read defaults, overrides, effective, and pendingChanges when diagnosing why code changes have not taken effect. The legacy UI does not expose these controls.

Queue maintenance is independent of the job loop​

AddQueues() now registers its own maintenance worker. Readiness maintenance promotes due sleeping/retrying messages and recovers expired leases; cleanup deletes retained messages/history/statistics in bounded batches. Reducing JobsOptions.JobLoopDelay no longer controls this work. Custom hosts that invoke maintenance themselves can still call OrganizeMessagesAsync(force: true); the compatibility entry point uses the split implementation.

QueueSystemOptions defaults are MaintenanceFallbackInterval = 10s, CleanupInterval = 1m, CleanupBatchSize = 1000, and MaxCleanupBatchesPerRun = 10. The process wakes for known retry/lease deadlines; the fallback scan catches work/deadlines from other instances or restored after restart. Larger fallback intervals reduce idle database work but can delay that discovery. Cleanup limits apply per retention store per queue, so a large backlog may need several passes. Review timing assumptions in maintenance monitoring rather than changing these defaults automatically.

11. Separate Admin UI registration and update clients​

CommerceMind.Nexus.Api no longer brings in or registers CommerceMind.Nexus.UI. Add the UI package only to hosts that serve it. Move options to AddAdminUI() and use CommerceMind.Nexus.UI.Extensions for registration/middleware:

services.AddNexus()
.AddApi(options => options.BasePath = "/nexus/")
.AddAdminUI(options =>
{
options.ApiBaseUrl = "/nexus/";
options.EnvironmentName = environmentName;
options.InfoNotice = "Test environment";
})
.Build();

// After building the ASP.NET application:
app.UseNexusAdminUI();

ApiBaseUrl is required. When colocated, match NexusApiOptions.BasePath. NexusApiOptions.EnvironmentName and .AdminUI were removed. NexusAdminUIOptions, NexusAdminUIMenuItems, and IAdminUIFileProvider moved to CommerceMind.Nexus.UI.

UseExperimentalNewVersion was replaced by UseLegacyVersion, default false. Remove a true experimental flag to keep using the new UI; set UseLegacyVersion = true only when the legacy UI is intentionally needed temporarily. The default standalone UI path is now nexus; explicitly configure another Path if required. API base URL and UI path are separate settings. Check redirects, proxy path bases, links, authentication, and cross-origin access for separate hosting.

In the final v3 implementation, GET /api/enabled-features is served by the API and describes API-host capabilities, even for a remote UI. Menu customization comes from NexusAdminUIOptions.MenuItemsLoader on the UI host. Do not move feature discovery to the UI host based on earlier prerelease guidance or depend on removed menu fields in the feature response.

Regenerate custom clients from the actual v3 host's OpenAPI or update them explicitly. Routes below are relative to the API base path:

ContractV3 change
GET /api/jobs{ "jobs": [...], "categories": [...] } instead of an array
GET /api/jobs/categoriesRemoved; use categories in the jobs-list response
GET /api/queuesNo counts by default. Use queueNamesWithCounts or includeCountsForAll=true; defaultWithoutCounts was removed
Historical job runsNo database id; key by correlationId
Extra runs / nextRun.extraRunId64-bit IDs; remove 32-bit casts/validators
Queue create payloadsRemoved startProcessing; activation follows the processing mode. Single-item payloads support partialMessage
Job/queue responsesNullable queue-job schedules, activation state/delay, processor data, AwaitingRelease, and statusAfterRelease

Preserve 64-bit IDs in client languages/storage that cannot represent every long exactly. Treat omitted counts as not loaded, not zero. Review exhaustive enum/status handling and code assuming every job has a next scheduled run.

12. Serialization, events, functions, and Teams​

JsonOptions.RequireTypeHash now defaults to true for System.Text.Json polymorphic deserialization. JSON containing $type must carry a valid $typehash. The default writer already writes hashes, so do not change normal payloads just because the default changed. This is not a requirement to put $type on every ordinary queue payload.

The built-in STJ and Newtonsoft.Json health-result readers recognize v2's ScheduledJobHealthCheckData in cached results and historical statistics. They preserve stored payloads and validate STJ hashes against the original type name and salt; newly saved results use JobHealthCheckData. Custom health-result readers must retain equivalent compatibility. Rollback preserves original v2 rows and archives incompatible v3 results before refreshing cached checks.

Exercise representative persisted polymorphic messages, parameters, arguments, health results, and custom integration state with the selected serializer. Pay attention to historical v1/Newtonsoft-compatible data and custom writers. If hashes are absent, migrate/drain that data first, or temporarily configure:

services.AddNexus().ConfigureJsonOptions(options =>
{
options.RequireTypeHash = false;
});

Keep WriteTypeHash enabled for new STJ data and retain the existing TypeHashSalt. Custom writers must put $typehash before $type, and both before ordinary object properties, as the Nexus converter reads metadata in that order. A mismatched/invalid hash is not repaired by manufacturing hashes in stored JSON. Track the temporary compatibility setting and enable validation once old data is gone. Do not switch between STJ and Newtonsoft.Json as an incidental part of this upgrade; RequireTypeHash describes the STJ converter's behavior.

IInstanceEventBroker.OnInstanceEventRecieved was removed. Replace event handlers with asynchronous subscriptions, keep their handle, and dispose it with the subscriber:

private readonly IInstanceEventSubscription _subscription;

// In the constructor:
_subscription = instanceEventBroker.Subscribe(HandleInstanceEventAsync);

// HandleInstanceEventAsync returns Task.
public void Dispose() => _subscription.Dispose();

DbConnectionScope moved from CommerceMind.Nexus.Core.Database to CommerceMind.Nexus.Abstractions.Core.Database. Preserve its transaction/suppression semantics while changing the using directive.

NexusFunctionServiceMethod.Service was removed, and its constructor no longer takes IServiceProvider. Custom function invocation code must resolve ServiceType from the active scope. Do not retain a resolved application service globally.

The old Teams webhook API (AddTeamsPublisher(), NexusTeamsOptions) was removed. Direct bot mode uses AddTeamsBotPublisher()/NexusTeamsBotOptions and UseNexusTeamsBot(); outgoing-only bridge mode is another option when the application is not publicly reachable. Keep this migration separate from generic HTTP webhooks. See Teams integration for credentials, endpoint setup, linking, and bridge configuration; old webhook credentials are not bot credentials.

13. Prepare database initialization and deployment​

First verify the required latest-v2 deployment has completed initialization against the target database. Then stop all v2 processes that share it before v3 initialization, including API/producer hosts that initialize Nexus. A mixed v2/v3 rolling deployment is unsupported: v2 uses old job tables and does not participate in v3's startup locks. Coordinate producer pause/drain and broker changes before cutover.

Automatic schema migration​

Nexus migrates job tables while retaining their data. Update application-owned SQL, views, grants, reporting, monitoring, and maintenance scripts referencing the old names:

SQL Server v2 → v3PostgreSQL / SQLite v2 → v3
NexusScheduledJobs → NexusJobsnexus_scheduled_jobs → nexus_jobs
NexusScheduledJobRuns → NexusJobRunsnexus_scheduled_job_runs → nexus_job_runs
NexusScheduledJobProgress → NexusJobProgressnexus_scheduled_job_progress → nexus_job_progress
NexusScheduledJobMutexes → NexusJobMutexesnexus_scheduled_job_mutexes → nexus_job_mutexes
NexusScheduledJobLogs → NexusJobLogsnexus_scheduled_job_logs → nexus_job_logs
NexusScheduledJobExtraRuns → NexusJobExtraRunsnexus_scheduled_job_extra_runs → nexus_job_extra_runs

Do not manually recreate tables or discard the migration ledger. Renamed migrations recognize previous names through PreviousMigrationNameAttribute. New migrations add queue activation, activation delay, processing settings, and job-category storage as needed.

Lease indexes are rebuilt to match priority DESC, id ASC. PostgreSQL uses CREATE INDEX CONCURRENTLY; SQL Server builds a replacement nonclustered index while retaining the legacy index, then swaps names in a short transaction. These still need extra space, I/O, and on SQL Server transaction-log capacity. They do not promise zero blocking. Interrupted builds retain the old index until the replacement is valid and retry/resume on startup. SQLite's transactional rebuild blocks other writers; plan a maintenance window for large queues.

Cached initialization and filter backfills​

Reserved Nexus.Startup.* key-value records store the schema revision, completed migration manifests, and per-queue schema definitions. On unchanged starts Nexus skips regular migration-history queries and queue table/index/column inspection. Package version changes alone do not invalidate these records.

The first v3 startup reads the existing ledger rather than blindly replaying migrations. Each unstamped v2 queue is adopted separately, including queues first registered by another application later. Compatible filter columns/list tables retain their values. Missing/incompatible filter definitions are rebuilt from persisted payloads. New queues initialize with the current schema even when regular migrations have already completed.

Changes to filterable property definitions, history/statistics settings, and message-ID uniqueness are checked independently of the migration manifest. Keep old payloads deserializable when filter backfills require reading them. Pending backfill plans are persisted before schema edits so initialization can resume after interruption. Coordinate old/new application instances around incompatible schema changes.

Nexus does not inspect or repair unchanged tables after manual edits. Do not delete/edit startup markers as a routine repair strategy. Diagnose schema drift and prepare an explicit supported migration. Application-defined regular migrations participate in the manifest; user-defined idempotent migrations still run on every startup and should remain safe to repeat.

SQL Server/PostgreSQL serialize v3 initialization with session-owned locks on dedicated connections opened through the configured connection manager. Custom connection openers remain responsible for authentication and connection initialization. Allow pool capacity for the lock connection alongside migration connections. Failed lock acquisition/release discards the session from its pool. SQLite does not coordinate concurrent initializers; use one process. Completion state is published after successful initialization. A recent foreign v2 migration claim blocks startup rather than being stolen; verify the old owner has stopped before any force-migration recovery. Interrupted v3 claims can be retried after acquiring the lock. Regular migrations must still tolerate interrupted execution/retry.

If maintaining a custom Nexus database provider, inspect the v3 provider contracts and migrations: update both fresh-schema definitions and regular migrations, retain old migration identities, and support the initialization state/locking protocol. Fixed-schema changes belong in regular migrations plus the new-table baseline, not repeated per-queue compatibility work.

Deployment sequence​

  1. Deploy the latest v2 to each existing installation and wait for successful migrations and initialization of all relevant queues. If already completed, verify and record that evidence. Do not skip this deployment when moving from an older v2 release.
  2. Build and validate the upgraded code. Validate migration in a test environment with initialized v2 data and isolated broker resources.
  3. Record existing job names, schedules, disabled states, parameters, pending extra runs, queue backlog, and relevant broker offsets/resources. Confirm runtime and migration permissions/capacity.
  4. Pause producers as needed, drain or transfer incompatible broker resources, and stop every v2 instance. PostgreSQL/SQL Server capture their rollback checkpoint internally on the first v3 migration startup; there is no separate capture script to run.
  5. Provision the v3 broker topology and coordinate direct-producer changes. Initialize the database with the v3 application; verify automatic checkpoint capture on PostgreSQL/SQL Server and wait for successful migration/backfill completion before routing normal traffic. Use one initializer for SQLite.
  6. Verify preserved metadata, apply the agreed saved-schedule changes (including frequent-polling conversions) and processing overrides, then start v3 workers and resume producers. If the cutover tool requires a running host, keep normal processing paused while applying changes. Confirm readiness health and actual processing for each provider.
  7. Monitor errors, backlog, retries, lease recovery, EF persistence, and resource load. Retain old broker resources while they are still needed for backlog processing or a planned return to v2.

If returning to v2, use the rollback instructions to reverse the database changes and account for data written since cutover and any broker changes before starting the v2 application.

Optional rollback to v2​

Use this section if the user wants to test returning to v2 or include rollback in their deployment plan.

The PostgreSQL script and SQL Server script only perform rollback of the built-in v3 database changes, consuming the checkpoint Nexus creates internally. Read the rollback instructions and the actual selected script before planning its execution. These downloads do not require GitHub access. If unavailable in the LLM's environment, obtain the matching file from the user rather than inventing an inverse from table names alone.

  • Nexus automatically captures the initialized v2 baseline under the startup lock, before idempotent queue initialization and regular migrations. Record the database, explicit schema, checkpoint table, and application artifacts for the rollback. A capture error stops the upgrade before those schema changes proceed.
  • The checkpoint stores original category JSON, job/queue settings, queue schema/index definitions, and migration history. It allows rollback to restore type hashes/custom serializer output and schedules removed for readiness-driven processing without guessing.
  • Fresh v3 installations have no v2 checkpoint. An interrupted upgrade reuses its original checkpoint; a later upgrade after rollback archives the used checkpoint and captures a fresh baseline. Existing v3 databases without a checkpoint cannot recover their original settings through automatic capture. If a checkpoint is missing, establish the original settings and prepare an installation-specific rollback rather than inventing checkpoint contents.
  • Before an actual rollback, stop all producers/workers, resolve active leases with v3, and review broker topology/backlog recovery and persisted payload compatibility. Prepare the v2 code/configuration as well as the SQL.
  • The rollback transaction reverses the six job-table renames, lease columns/indexes, activation/processing-settings storage, category key/value, startup markers, and the listed v3 migrations. It restores captured configuration and maps readiness-run initiators to v2's Schedule, while preserving current messages, run history, extra runs, next-run parameters, and runtime execution details. It archives incompatible health-check results/statistics and v3-only control events in the checkpoint before removing them from active tables; v2 refreshes the cleared health-check caches.
  • Do not bypass a refusal for unknown migrations, changed queue/filter schemas/indexes or identities, conflicting tables, or unresolved leases. New v3 processors/named queues, application migrations, and broker changes need their own reversal steps. The rollback scripts preserve current messages and history; they do not recreate deleted data or undo external side effects.
  • Test the actual v2 application startup and representative work after rollback. Report whether checkpoint capture, SQL execution, broker recovery, and v2 restart were prepared, tested, or actually completed.

14. Validate behavior, not only compilation​

Build every affected host/library and update relevant tests according to repository instructions. A docs-only migration plan is not a validated application upgrade. Use isolated stores/brokers for checks that mutate data.

Select applicable checks from this list:

  • The latest v2 has actually been deployed and has completed database/queue initialization in every target environment before the v3 rollout.
  • If rollback is part of the requested work, validate v2 settings/category restoration, v2 application startup, persisted payloads, and broker recovery using the automatically captured PostgreSQL/SQL Server checkpoint.
  • DI validation catches no root-provider or singleton captures of scoped Nexus services. Custom replacements can resolve their scoped dependencies.
  • Existing job/queue names, disabled/pause states, parameters, history, and extra runs survive migration. Saved schedules are preserved except for the explicitly agreed changes; every accepted frequent-polling conversion has both its code default removed and its saved schedule cleared in the target environment.
  • A readiness-driven job starts after committed enqueueing and after a delayed/retrying message becomes eligible. A scheduled-only job waits for its schedule unless explicitly started.
  • Frequent immediate job-start callers retain the fast requester path without added per-call extra runs. For wake-up signals, validate processing of persisted work and recovery from missed/coalesced signals. Starts specifically requiring persistence use durable extra runs and survive worker downtime; hosts using RunAllJobsPendingSinceLastCallAsync() do not rely on events. Event-only producers reach the correct remote job name.
  • With EF, entity-plus-database-message writes commit together; rollback and unsaved-context disposal expose neither message nor readiness. Failed saves can retry. Test generic/non-generic/batch paths actually used by the application.
  • Standalone publishing persists before returning. External/API delivery is not assumed to roll back with EF.
  • Representative persisted polymorphic data deserializes with the intended hash policy and unchanged salt.
  • Re-enqueue during a lease produces AwaitingRelease; a second worker cannot lease it until release; the latest payload/date/priority/status wins. Check previous-message behavior if the application relies on it.
  • Broker bridges report healthy and readiness acknowledgement never consumes the processing copy. Direct producers use current topology. Old Redis/Service Bus backlogs have been drained or transferred deliberately.
  • Custom queue batch records, positional reads, filters, retries, and optimistic versions preserve their meaning.
  • API clients handle the jobs wrapper, missing counts, correlation IDs, 64-bit extra-run IDs, nullable schedules, and new status. UI/API paths and authentication work in their real hosting arrangement.
  • A first startup upgrades representative queues, a second unchanged startup succeeds, and adding/changing a filterable property backfills correctly without losing readable payloads. Interrupted initialization can resume if custom migrations are involved.
  • Optional distributed processors obey intended total concurrency, pause/resume, and retry behavior; optional named queues route explicitly and keep independent identity/history. Saved processing overrides are visible and intentional.

Finish with a concise report of changed projects, behavior choices, checks performed, checks requiring an environment, temporary compatibility settings, and the concrete deployment/rollback steps. Include a per-job list of accepted schedule removals, retained schedules and their reasons, and unresolved choices. Distinguish prepared metadata changes from ones actually applied. State the target package versions and how any application-specific contracts were verified. Do not report a broker migration or live deployment as completed when only its code/configuration was prepared.

Source references​

When a custom integration exposes an edge case, inspect the exact target version rather than extrapolating from the rename table. Use matching source if accessible, otherwise inspect/decompile the installed assemblies as described above. Useful entry points:

QuestionAssembly and types to inspect
Queue-job defaults, saved schedules, durable startsCommerceMind.Nexus.Abstractions.dll: IQueueJobBase, IJobMetaDataRepository, JobMetaData; CommerceMind.Nexus.dll: JobMetaDataRepositoryBase, JobExecutorService, and JobStartRequester. Check callers and default interface member implementations too.
Registration lifetimes and generated jobsCommerceMind.Nexus.dll: NexusServiceCollectionRegistrationExtensions; CommerceMind.Nexus.Abstractions.dll: JobDescriptor and GeneratedQueueJob<T>; registration extensions in the selected provider package.
Activation and recoveryCommerceMind.Nexus.dll: QueueActivationReconciler, JobExecutorService, DatabaseQueueActivationStoreBase; the selected provider's activation-store implementation.
EF transaction interception versus immediate operationsCommerceMind.Nexus.EntityFramework.dll: EntityFrameworkTransactionInterceptor; CommerceMind.Nexus.dll: ImmediateQueueOperationCommitter; CommerceMind.Nexus.Abstractions.dll: ImmediateQueueOperationScope. Follow operation commit and after-commit notification paths.
Custom queue implementations, batches, and optionsCommerceMind.Nexus.Abstractions.dll: IEnqueuer, QueueItemUpdateOperations, and QueueSystemOptions, plus the actual implementation in the storage/broker package.

The following links are optional. GitHub links point to main, which may differ from the installed package; use a matching release revision when available: