Skip to main content

From v2 to v3

Version 3 changes how queue processing is started and adds distributed queue processors and named queues. There are also breaking changes to service lifetimes, public APIs, and database initialization. Note that v3 renames some tables and values. Read more in Database initialization in v3.

This guide covers the changes to review before upgrading. For code replacements, configuration examples, and a validation checklist, use the detailed upgrade guide. That guide is written as a skill for an LLM carrying out the upgrade.

Deploy the latest v2 first​

Before upgrading an existing installation to v3, upgrade to the latest Nexus v2 release, deploy it, and let it complete startup and database initialization in every environment being upgraded. This applies to installations already running v2 as well as those coming from v1. Updating package references without deploying and running that version is not enough.

Make sure every Nexus database and its registered queues have been initialized successfully by the latest v2. If different applications register different queues, run the relevant applications so their initialization also completes. Once this prerequisite is satisfied, stop the v2 instances before deploying v3.

V3's faster database initialization adopts the existing v2 schema and then records which initialization work has completed. The supported upgrade path assumes v2's migrations and compatibility work have already finished; do not skip the intermediate deployment from an older version. If the latest v2 is already deployed and initialization has completed, this prerequisite is already satisfied.

Direct upgrades from v1 are no longer supported. V3 no longer adds the old v1 queue priority columns or rewrites serialized v1 type and assembly names, so any remaining v1 persisted data must also be processed or migrated through v2.

.NET and package versions​

All Nexus packages now require .NET 10. Update the application's target framework, build environment, and hosting runtime before upgrading the Nexus packages together to v3.

Job and queue API names​

Schedule-neutral APIs now use Job instead of ScheduledJob. For example, AddScheduledJobs() becomes AddJobs(), [ScheduledJob] becomes [Job], and IScheduledJobMetaDataRepository becomes IJobMetaDataRepository.

Queue jobs use IQueueJob<T> and IBatchQueueJob<T> instead of IScheduledQueueJob<T> and IScheduledBatchQueueJob<T>. Ordinary scheduled jobs still implement IScheduledJob as regular jobs always have a schedule.

Obsolete APIs have been removed, if your build is failing on v3 because of this the easiest thing is to use the latest v2 and check and fix all build warnings on obsolete APIs.

Queue jobs can start when messages are ready​

Queue jobs without a schedule now start when messages become ready. EnqueueAsync() requests activation after messages are committed, so replace EnqueueAndStartProcessingAsync() with EnqueueAsync().

A queue job with a cron schedule remains scheduled-only. Existing schedules are stored in the database, so removing DefaultSchedule from the class does not clear an existing schedule. Clear it through the API or Admin UI when you want to switch that queue job to readiness-driven processing. Conversely, a queue job with an empty schedule that previously only ran manually can now start automatically.

Review calls to EnqueueAndStartProcessingAsync() on scheduled queues: enqueueing no longer brings their next run forward. If an additional run is needed, request it explicitly using the job APIs. See section about IJobStartRequester.

Database providers register persistent activation storage automatically. Redis activation storage is also available; in-memory activation storage is only suitable for a single instance setup. ActivationDelay can collect a burst before processing starts, while ProcessingDelay still controls when each message becomes eligible.

Queue jobs no longer report automatic progress by default to get better default queue performance. Set AutomaticProgressMode to QueueJobProgressMode.Percentage to retain the old behavior, or ProcessedCount to show a handled count without first counting the queue.

Re-enqueueing a leased message in a database or in-memory queue now puts it in AwaitingRelease. The current processor keeps its lease, and the latest enqueue takes effect when it finishes. Update custom status handling and monitoring to recognize this as a message still being processed. External pending storage keeps its provider-specific behavior. Previously AwaitingRelease wasn't needed as Nexus only ever allowed a single instance to process a queue at any time, which is no longer true in v3.

Starting jobs with IJobStartRequester​

Immediate parameterless RequestJobStartAsync() calls now send a fast, best-effort instance event. Only instances running the continuous job loop handle these requests. They may be coalesced, lost during downtime, or cause successive runs on different instances; one request does not guarantee exactly one run. If a job is running at the same time that IJobStartRequester requests it to start another run will start immediately after. The purpose of this is to process anything added at the end of the job run that the job didn't process.

Keep frequent immediate start calls on IJobStartRequester to benefit from the faster implementation: it avoids extra-run database writes and the associated cache invalidations across instances. Do not routinely replace these calls with scheduled extra runs. Check how the application recovers pending work after a missed signal; having no schedule does not by itself mean frequent calls should become database writes.

Use IJobMetaDataRepository.ScheduleExtraRunAsync() for specific start requests that must themselves be persisted, or applications using RunAllJobsPendingSinceLastCallAsync(), which cannot handle the events. Calls with a non-null parameter object or an explicit delay/start time still schedule extra runs.

Applications can now send immediate requests without registering the job locally or calling AddJobs(), provided they configure the shared instance-event broker. Use the correct persisted job name, especially for generated jobs or jobs bound to several queues.

Distributed processors and named queues are optional​

Your existing scheduled queue jobs can work just like before. Distributed queue processors are an alternative when independent messages should be processed concurrently across application instances. They do not provide job runs, schedules, parameters, or mutexes. Each concrete queue can have one queue job or one distributed processor; in-memory queues do not support distributed processors.

Named queues let several queues share one message type. If you adopt them, choose the destination explicitly when enqueueing and preserve existing names where data and metadata should be retained. The new Admin UI can override processing settings per queue, so also review any saved overrides when changing code defaults. This is a good option when you need a dynamic set of queues for the same thing. Perhaps one queue per order type but you don't know at compile time how many order types you have.

Generated queues need explicit routing​

Existing generated queues also use the new named queue infrastructure. Nexus no longer creates runtime message or job subclasses, so generated message contracts and job implementations must be concrete.

Choose the queue name when enqueueing to a generated queue. For the previous ShouldEnqueue routing behavior, configure a shared RoutingQueueName and enqueue to that virtual entry point. Type-only enqueueing no longer broadcasts to generated destinations, and direct enqueueing to a concrete destination bypasses ShouldEnqueue. Keep the existing concrete queue and generated job names to retain their data and settings. See generated queues.

Most Nexus services are now scoped​

Operational Nexus services, including enqueuers, repositories, functions, serializers, and integration services, are now scoped. Code running in a request or Nexus execution scope can inject them normally. Singleton application services and BackgroundService implementations must create a scope for each independent operation instead of injecting scoped Nexus dependencies or resolving them from the root provider.

Choose application lifetimes based on their own requirements; they do not all need to become scoped. Keep the scope alive until its asynchronous work finishes, and do not cache a Nexus service beyond that scope. Process-wide services such as the instance-event broker remain singleton.

Entity Framework queue integration is scope-bound​

Nexus now uses the configured DbContext from the same scope as the enqueuer. Register it as scoped with AddDbContext, avoid DbContext pooling, and configure it with UseNexus(serviceProvider).

All local enqueue interfaces now respect that unit of work. Database messages commit with entity changes, and in-memory messages become visible after commit. Use asynchronous saves and transaction commits; pending messages are discarded if the context is disposed without saving. Review code that previously relied on an enqueue overload persisting immediately.

For independent enqueue operations that should persist without saving a context, use ImmediateQueueOperationScope. Nexus's API, MCP, and broker ingress already do this. Creating a new DI scope cannot join the caller's existing transaction. Remote API enqueueing and external pending storage are sent immediately and cannot be rolled back with EF; use a database outbox queue if they must follow the entity transaction. See Entity Framework integration.

External pending storage​

External pending storage now has a separate durable readiness path. RabbitMQ uses a fan-out exchange with processing and readiness queues, Azure Service Bus uses a topic with two subscriptions, and Kafka uses separate processing and readiness consumer groups. Redis keys now include a shared cluster hash tag and a readiness stream.

Review provisioning, broker permissions, and any producers writing directly to these resources. Drain or explicitly migrate messages in old resources before switching; database migrations do not move broker data. Redis's old keys and Azure Service Bus's old queue entities are not read by the new topology. RabbitMQ producers must publish to the new exchange instead of directly to the processing queue. See external pending storage.

This is separate from integrations that consume an existing external queue into a Nexus queue. Those keep their configured source, but custom Kafka/RabbitMQ transformers and deserializers may need API changes described in the detailed guide.

The Admin UI is registered separately from the API​

AddApi() no longer registers the Admin UI. Reference CommerceMind.Nexus.UI, call AddAdminUI(), and keep UseNexusAdminUI(). Set NexusAdminUIOptions.ApiBaseUrl explicitly; when hosted together, it should match NexusApiOptions.BasePath. Environment name, notices, and other UI options are configured on NexusAdminUIOptions instead of through AddApi().

The new Admin UI is the default. UseExperimentalNewVersion has been replaced by UseLegacyVersion, which temporarily enables the legacy UI. A separately hosted UI now defaults to the nexus path instead of /. Review custom paths and reverse-proxy configuration. See Admin UI configuration.

Custom HTTP clients also need updating:

  • GET /api/jobs returns an object containing jobs and categories; the separate categories endpoint has been removed.
  • GET /api/queues omits counts by default. Request selected queues with queueNamesWithCounts, or all counts with includeCountsForAll=true.
  • Historical runs use correlationId instead of a database id, and extra-run IDs are now 64-bit integers.
  • Queue creation payloads no longer accept startProcessing; queue activation follows the configured processing mode.

Regenerate clients against the v3 API, including its nullable queue-job schedule and new message status.

JSON and other integrations​

System.Text.Json polymorphic JSON now requires a valid type hash by default. Most applications using the default serializers need no change. Check older persisted data and custom writers that emit $type without $typehash; if needed, temporarily set JsonOptions.RequireTypeHash = false while migrating that data. Keep the existing type-hash salt.

Custom instance-event subscriptions now use asynchronous Subscribe() handlers and must dispose the returned subscription. The old OnInstanceEventRecieved event has been removed.

The retired Teams webhook publisher has been removed. Replace it with the Teams bot or outgoing bridge integration.

Database initialization in v3​

After the latest-v2 deployment and initialization has completed, stop all v2 instances before starting v3 against the same database. V3 automatically renames job tables, so a rolling deployment with v2 and v3 sharing the database is not supported. Update custom SQL, monitoring, and database permissions that reference the old table names.

The first v3 startup completes migrations, adopts existing queue schemas, and rebuilds queue lease indexes. Large queues need additional time, disk space, and database I/O; SQLite index creation also blocks writers. Allow initialization to finish before bringing the application into service.

Later starts skip migration-history and schema checks when the stored initialization state is unchanged. New queues and changed filterable properties are still initialized, including backfilling filters from existing messages. Keep those messages readable by the updated types and coordinate instances when making incompatible schema changes.

Nexus no longer automatically repairs manually changed tables. Leave Nexus-owned schema and reserved Nexus.Startup.* key-value entries intact. SQLite does not coordinate concurrent initializers; initialize it from one process. SQL Server and PostgreSQL coordinate v3 initialization through database locks.

Also review QueueSystemOptions.DisableHistory: it now overrides history settings saved in the database.

Rollback to v2​

PostgreSQL and SQL Server rollback scripts are available to reverse the built-in v3.0 database changes. Nexus automatically captures a v2 checkpoint before queue initialization and regular migrations, including saved schedules and the original serialized job categories. If you want to return to v2, stop all Nexus instances and run the selected rollback script to restore the v2 schema and captured settings.

v3 has been thoroughly tested and is stable. Being able to rollback to v2 is just a service we provide in case you want to try out upgrading in a lower environment to test the new features out, and decide if you're ready to migrate to v3 or wait.

From v1 to v2

Version 2 contains a number of breaking changes from version 1. The core logic and concepts all remain the same in Nexus and this release is mostly about code and project reorganization, removing unnecessary dependencies and updating to .NET 8.

New NuGet package structure​

Instead of having the different sub systems such as CommerceMind.Nexus.Jobs in their own NuGet packages they have been consolidated into the new CommerceMind.Nexus package and the abstraction packages have been consolidated into CommerceMind.Nexus.Abstractions.

This reorganization means that many interfaces have been moved to new namespaces. Eg IScheduledJob which now lives in CommerceMind.Nexus.Abstractions.Jobs instead of CommerceMind.Nexus.Jobs.Abstractions. The interface names are still the same, so it should be easy to either search-and-replace the namespaces or let Visual Studio/Rider/etc guide you to the new names.

Another reorganization change in this is that the implementations for the different databases are no longer included in the main packages. Instead they have been extracted into CommerceMind.Nexus.Postgres, CommerceMind.Nexus.SqlServer, and CommerceMind.Nexus.Sqlite. Allowing you to only include the database(s) you want.

New fluent service registration​

In v1 there was a number of extension methods on IServiceCollection that you had to call in a specific order. In v2 this has changed to a fluent registration on IServiceCollection where the order of which method you call is no longer important.

This is how the v2 registration looks like:

builder.Services.
.AddNexus()
.AddSingleServerInstanceEvents()
.AddFunctions()
.AddScheduledJobs()
.AddQueues()
.AddSqliteConnection()
// Don't forget this one!
.Build()
;

Note that you can still split up your service registration as long as the call to .Build() is done last, as that will register additional services based on your configuration. You can for example do this:

builder.Services.AddNexus().AddSingleServerInstanceEvents();
builder.Services.AddNexus().AddFunctions();
builder.Services.AddNexus().AddScheduledJobs();
builder.Services.AddNexus().AddQueues();
builder.Services.AddNexus().AddSqliteConnection();
builder.Services.AddNexus().Build();

Each of these lines could be in different projects as well, as long as the call to Build() happens last. All calls to builder.Services.AddNexus() will return the same Nexus registration object for that specific service collection. It's also safe to call any method on the Nexus registration object multiple times.

Nexus no longer uses Newtonsoft.Json by default​

Instead System.Text.Json is used by default and the Newtonsoft.Json serializers have been extracted into a separate package called CommerceMind.NewtonsoftJson which you can use by calling builder.Services.AddNexus().AddNewtonsoftJson().

You probably don't need Newtonsoft.Json, this is only if you've modified how the JSON in Nexus is serialized with custom converters.

A notable change here is that the JSON formatting rules for ASP.NET will no longer be applied for queue messages in the API. Instead the JSON formatting rules for Nexus is applied, which means that you might get a response looking like this:

{
"id": 4823771,
"status": "Processed",
"retryCount": 0,
"message": {
"Id": null,
"SomeProperty": null,
"ExampleEnum": "Value1"
}
}

Where the outer object have gotten kebabCase properties but the message object has PascalCase properties. You can control both of these (see docs about JSON) formatting options.

Nexus now requires >= .NET 8​

v1 supported .NET 6 but v2 now reqires at least .NET 8. The only thing you need to do is to upgrade your .NET version in your application if you haven't already done so.

Deprecated methods and properties have been removed​

Methods and properties that have been deprecated in v1 have been removed in v2. If you upgrade to v1.23.0 and fix all build warnings about obsolete methods and properties you'll automatically be prepared for the removals of them in v2.

Default job history retention is now 30 days​

Previously the default was to keep job history in the database forever. To avoid having Nexus growing your database indefinitely the default has been changed to 30 days. You can still configure this just like before, but since you can now have different retention per job the property is now called DefaultHistoricalRunsRetention:

builder.Services.AddNexus().AddScheduledJobs(options =>
{
options.DefaultHistoricalRunsRetention = TimeSpan.FromDays(10);
});

You set it per job with the [ScheduledJob] attribute like this:

[ScheduledJob("MyJob", HistoricalRunsRetention = "30.00:00")]
public class MyJob : IScheduledJob
{
...
}

The IEnqueuer and IEnqueuer interfaces have been simplified​

Previously IEnqueuer<T> inherited from IEnqueuer which it no longer does. In v1 every message type was registered as a IEnqueuer which means that you could get all enqueuers by requesting IEnumerable<IEnqueuer> like this:

public class DynamicEnqueuer(IEnumerable<IEnqueuer> enqueuers)
{
public Task EnqueueAsync(IQueueMessage message)
{
var enqueuer = enqueuers.Single(e => e.MessageType == message.GetType());
return enqueuer.EnqueueAsync(message);
}
}

In v2 there is only a single non-generic IEnqueuer registered which does the above for you. Meaning that you can pass a IQueueMessage message to it and it will figure out which concrete IEnqueuer<T> to pass it on to.

Both the IEnqueuer<T> and IEnqueuer interfaces have been simplified to take a message and an EnqueueContext instead of having multiple different overloads for when you want to specify a custom status, a date for when to process it, etc.

So when you did this in v1:

await enqueuer.EnqueueAsync(message, DateTime.UtcNow.AddHours(1));

To enqueue a message to be processed in one hour you now do this:

await enqueuer.EnqueueAsync(message, new EnqueueContext { ProcessAfter = TimeSpan.FromHours(1) });

No longer using Serilog​

Nexus previously used Serilog to capture logs happening inside job runs. With v2 it instead has a ILoggerProvider which is registered by the scheduled job system.

In v2 you can use any logging framework you want as long as it writes to external logging providers. Note that this is turned off by default in Serilog and that you need to enable it, eg like this:

builder.Host.UseSerilog((context, services, configuration) =>
{
configuration.ReadFrom.Configuration(context.Configuration);

// This one is important
}, writeToProviders: true);

If you've used the jobs logging in v2 you also need to remove the Nexus Serilog sink from your configuration:

"Serilog": {
"Using": ["CommerceMind.Nexus.Jobs"],
"WriteTo": [{ "Name": "NexusJobLogs" }]
}

Changes to the IJobLogProvider interface​

For consistencys sake the IJobLogProvider interface has been renamed to IJobLogReader since there's also a IJobLogWriter interface.

And instead of just getting a date to fetch logs newer than Nexus now passes in a date range. You also get the log levels passed as LogLevel enums rather than strings.

Also note that JobLogEvent.Level has changed data type from string to the LogLevel enum.