-
Notifications
You must be signed in to change notification settings - Fork 16
Migration Guide v5
This guide covers the upgrade from v4.4.0 to v5.0. Version 5 replaces the v4 attribute-based command inbox with explicit inbox and outbox modules. It also keeps the v5 branch diagnostics for message resolution, event predicates, namespace filtering, and open generic handler validation.
| Area | v4.4 behavior | v5 behavior |
|---|---|---|
| Deferred commands | Commands marked with [StoreInInbox] were diverted by ICommandMediator.SendAsync. |
SendAsync always executes in process. Use ICommandScheduler.ScheduleAsync for deferred execution. |
| Command inbox API |
StoreInInboxAttribute, ICommandInbox, ICommandInboxBatch, ICommandInboxProcessor, and CommandBatchHandler lived under command abstractions. |
Those APIs are removed. Use LiteBus.Inbox.Abstractions and LiteBus.Inbox. |
| Command inbox hosting |
LiteBus.Commands.Extensions.Microsoft.Hosting hosted the old inbox processor. |
The package is removed. Run ICommandInboxProcessor.ProcessPendingAsync from your worker or host adapter. |
| Result commands | A result command could be marked for inbox storage, then return default(TResult). |
Deferred commands must be ICommand. Use queries or tracking endpoints for results. |
| Store roles | v4 used application-owned inbox contracts. | Stores are split by role: writer, lease store, and state store. |
| Contracts | v4 inbox storage did not use the shared contract registry. | Every scheduled command and outbox event must have a stable contract name and version. |
| Event publication |
IEventPublisher.PublishAsync published in-process only. |
In-process publish is unchanged. Use IOutboxWriter.AddAsync or IIntegrationOutbox.AddAsync to store events for later publication. |
| Message descriptor failures | Some descriptor lookup failures used InvalidOperationException. |
Descriptor lookup failures throw MessageDescriptorNotFoundException. |
| Open generic handlers | Unsupported multi-parameter open generic handlers could be ignored. | Unsupported open generic handler shapes throw UnsupportedOpenGenericHandlerException. |
| Event predicates | Handler predicates did not apply consistently to all event publishing overloads. | Predicates apply to both PublishAsync(IEvent, ...) and PublishAsync<TEvent>(...). |
| Solution file | The repository used LiteBus.sln. |
The repository uses LiteBus.slnx. |
The command mediator no longer stores commands. A call to SendAsync means execute now.
Before:
[StoreInInbox]
public sealed record ProcessPaymentCommand(Guid OrderId, decimal Amount) : ICommand;
await commandMediator.SendAsync(new ProcessPaymentCommand(orderId, amount), cancellationToken: cancellationToken);After:
public sealed record ProcessPaymentCommand(Guid OrderId, decimal Amount) : ICommand;
var receipt = await commandScheduler.ScheduleAsync(
new ProcessPaymentCommand(orderId, amount),
new CommandScheduleOptions
{
IdempotencyKey = $"payment:{orderId}",
CorrelationId = correlationId
},
cancellationToken);CommandReceipt<TCommand> means the store accepted the command. It does not mean the handler has run.
Deferred execution cannot return a result to the caller. Convert deferred result commands to ICommand, then expose status through queries or application endpoints.
Before:
[StoreInInbox]
public sealed record GenerateInvoiceCommand(Guid OrderId) : ICommand<Guid>;
var invoiceId = await commandMediator.SendAsync(new GenerateInvoiceCommand(orderId), cancellationToken: cancellationToken);After:
public sealed record GenerateInvoiceCommand(Guid OrderId) : ICommand;
var receipt = await commandScheduler.ScheduleAsync(new GenerateInvoiceCommand(orderId), cancellationToken: cancellationToken);
return Results.Accepted($"/invoice-requests/{receipt.CommandId}");If the caller needs the value in the same request, keep ICommand<TResult> and call ICommandMediator.SendAsync without scheduling.
Every scheduled command and stored event needs a stable name and version. The stored row uses this contract, not an assembly-qualified CLR name.
builder.Services.AddLiteBus(liteBus =>
{
liteBus.AddCommandInboxModule(inbox =>
{
inbox.Contracts.Register<ProcessPaymentCommand>(
"payments.commands.process-payment",
version: 1);
});
liteBus.AddOutboxModule(outbox =>
{
outbox.Contracts.Register<OrderSubmittedIntegrationEvent>(
"orders.events.order-submitted",
version: 1);
});
});Closed generic messages are supported when each closed type is registered. Open generic contracts are rejected.
inbox.Contracts.Register<ArchiveCommand<CustomerSnapshot>>("archive.commands.customer-snapshot", 1);
outbox.Contracts.Register<ExportCompletedEvent<CustomerExport>>("exports.events.customer-export-completed", 1);The inbox scheduler and processor depend on narrow store roles.
| Role | Used by | Purpose |
|---|---|---|
ICommandInboxWriter |
ICommandScheduler |
Append accepted command envelopes. |
ICommandInboxLeaseStore |
ICommandInboxProcessor |
Lease due commands for one worker. |
ICommandInboxStateStore |
ICommandInboxProcessor |
Record completed, failed, and dead-lettered state. |
A single database class can implement all three roles. Application services should depend only on the role they need.
With PostgreSQL:
var dataSource = NpgsqlDataSource.Create(connectionString);
builder.Services.AddLiteBus(liteBus =>
{
liteBus.AddPostgreSqlCommandInboxStore(postgres =>
{
postgres.UseDataSource(dataSource);
});
});
await PostgreSqlInboxSchema.EnsureAsync(dataSource, cancellationToken: cancellationToken);For production, prefer copying PostgreSqlInboxSchema.GetCreateScript into your migration pipeline instead of calling EnsureAsync from every pod. See PostgreSQL Schema Management.
The old LiteBus.Commands.Extensions.Microsoft.Hosting package was removed. Run the new processor from your application host boundary.
Option A: LiteBus inbox hosting module (recommended when you use the generic host). Reference LiteBus.Inbox.Extensions.Microsoft.Hosting:
liteBus.AddCommandInboxModule(inbox => { /* contracts and processor options */ });
liteBus.AddCommandInboxProcessorHosting(host => host.PollInterval = TimeSpan.FromSeconds(1));Option B: your own worker calling one pass at a time:
public sealed class CommandInboxWorker : IHostedService
{
private readonly ICommandInboxProcessor _processor;
private Task? _loop;
public CommandInboxWorker(ICommandInboxProcessor processor) => _processor = processor;
public Task StartAsync(CancellationToken cancellationToken)
{
_loop = RunAsync(cancellationToken);
return Task.CompletedTask;
}
private async Task RunAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var pass = await _processor.ProcessPendingAsync(stoppingToken);
if (pass.LeasedCount == 0)
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
}
}
public Task StopAsync(CancellationToken cancellationToken) => _loop ?? Task.CompletedTask;
}There is no combined inbox+outbox host package in v5. Register two hosts or one worker that calls both processors explicitly. See Processor Hosting.
Keep retry settings, lease owner, batch size, and polling cadence explicit in your worker setup.
IEventPublisher.PublishAsync still publishes to in-process handlers now. Use the outbox when an event must be stored with the same business transaction and published later.
public sealed record OrderSubmittedIntegrationEvent(Guid OrderId) : IIntegrationEvent;
await integrationOutbox.AddAsync(
new OrderSubmittedIntegrationEvent(orderId),
new OutboxOptions
{
MessageId = eventId,
Topic = "orders",
CorrelationId = correlationId
},
cancellationToken);Use OutboxOptions.MessageId when the application already has a stable event id. The event contract should carry business data, not storage identity.
The outbox processor depends on narrow store roles and an IOutboxDispatcher.
| Role | Used by | Purpose |
|---|---|---|
IOutboxMessageWriter |
IOutboxWriter |
Append accepted event envelopes. |
IOutboxMessageLeaseStore |
IOutboxProcessor |
Lease due messages for one publisher. |
IOutboxMessageStateStore |
IOutboxProcessor |
Record published, failed, and dead-lettered state. |
IOutboxDispatcher |
IOutboxProcessor |
Publish a leased envelope to LiteBus or an external transport. |
With PostgreSQL and the LiteBus in-process dispatcher:
builder.Services.AddLiteBus(liteBus =>
{
liteBus.AddOutboxModule(outbox =>
{
outbox.Contracts.Register<OrderSubmittedIntegrationEvent>("orders.events.order-submitted", 1);
outbox.UseLiteBusEventDispatcher();
});
liteBus.AddPostgreSqlOutboxStore(postgres =>
{
postgres.UseDataSource(dataSource);
});
});
await PostgreSqlOutboxSchema.EnsureAsync(dataSource, cancellationToken: cancellationToken);See PostgreSQL Schema Management for migration-owned DDL and opt-in host bootstrap.
For a broker, register your own IOutboxDispatcher and map OutboxMessageEnvelope.Topic to the broker destination.
EventMediationRoutingSettings.HandlerPredicate now applies to both event publishing overloads:
Task PublishAsync(IEvent @event, EventMediationSettings? settings = null, CancellationToken cancellationToken = default);
Task PublishAsync<TEvent>(TEvent @event, EventMediationSettings? settings = null, CancellationToken cancellationToken = default)
where TEvent : notnull;Review code that passed an IEvent variable and expected all handlers to run regardless of the predicate.
Open generic handlers that LiteBus closes automatically must have one generic type parameter. v5 throws UnsupportedOpenGenericHandlerException for unsupported shapes.
Before:
public sealed class AuditPreHandler<TCommand, TContext> : ICommandPreHandler<TCommand>
where TCommand : ICommand;
module.Register(typeof(AuditPreHandler<,>));After:
public sealed class AuditPreHandler<TCommand> : ICommandPreHandler<TCommand>
where TCommand : ICommand
{
private readonly AuditContext _context;
public AuditPreHandler(AuditContext context)
{
_context = context;
}
public Task PreHandleAsync(TCommand message, CancellationToken cancellationToken = default)
{
return _context.RecordAsync(message, cancellationToken);
}
}
module.Register(typeof(AuditPreHandler<>));Closed generic messages with concrete handlers resolve the registered concrete handler type. This matters for handlers such as ICommandHandler<ArchiveCommand<CustomerSnapshot>>.
Code or tests that catch InvalidOperationException for unresolved message descriptors should catch MessageDescriptorNotFoundException instead.
catch (MessageDescriptorNotFoundException exception)
{
logger.LogError(
exception,
"Descriptor lookup failed for {MessageType} with {ResolveStrategy}",
exception.MessageType,
exception.ResolveStrategyType);
}The exception exposes the message type, resolve strategy type, whether on-the-spot registration was enabled, and the registered message count.
The registry now skips only the real System namespace and System.* namespaces. Application namespaces such as Systematic.Domain.Events are registered normally.
If you had messages in a namespace that merely starts with System, they may now become visible to the registry.
The repository now uses LiteBus.slnx. Update local scripts and CI commands.
dotnet restore LiteBus.slnx
dotnet build LiteBus.slnx --configuration Release --no-restore
dotnet test LiteBus.slnx --no-restoreSample projects are included in the root LiteBus.slnx under /samples/.
PostgreSQL integration tests use Testcontainers and require Docker. CI now reports Docker availability before running tests.
- Remove
[StoreInInbox]and old command inbox abstractions from application code. - Use
ICommandMediator.SendAsyncfor immediate command execution. - Use
ICommandScheduler.ScheduleAsyncfor deferred commands. - Convert deferred result commands to
ICommandand expose tracking or query endpoints. - Register command and event contracts with stable names and versions.
- Register inbox and outbox store roles, or use the PostgreSQL packages.
- Start inbox and outbox processors from your worker or host adapter.
- Use
OutboxOptions.MessageIdfor stable event ids. - Review event handler predicates on
PublishAsync(IEvent, ...)calls. - Update unsupported open generic handlers.
- Catch
MessageDescriptorNotFoundExceptionfor descriptor lookup failures. - Update scripts from
LiteBus.slntoLiteBus.slnx. - Run PostgreSQL integration tests with Docker available.