# Migration Guide: Upgrading to LiteBus v5.0 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. ## Breaking Changes | 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(...)`. | | Solution file | The repository used `LiteBus.sln`. | The repository uses `LiteBus.slnx`. | ## Step 1: Replace `[StoreInInbox]` The command mediator no longer stores commands. A call to `SendAsync` means execute now. Before: ```csharp [StoreInInbox] public sealed record ProcessPaymentCommand(Guid OrderId, decimal Amount) : ICommand; await commandMediator.SendAsync(new ProcessPaymentCommand(orderId, amount), cancellationToken: cancellationToken); ``` After: ```csharp 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` means the store accepted the command. It does not mean the handler has run. ## Step 2: Move Result Work Out Of Deferred Commands Deferred execution cannot return a result to the caller. Convert deferred result commands to `ICommand`, then expose status through queries or application endpoints. Before: ```csharp [StoreInInbox] public sealed record GenerateInvoiceCommand(Guid OrderId) : ICommand; var invoiceId = await commandMediator.SendAsync(new GenerateInvoiceCommand(orderId), cancellationToken: cancellationToken); ``` After: ```csharp 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` and call `ICommandMediator.SendAsync` without scheduling. ## Step 3: Register Contracts Every scheduled command and stored event needs a stable name and version. The stored row uses this contract, not an assembly-qualified CLR name. ```csharp builder.Services.AddLiteBus(liteBus => { liteBus.AddCommandInboxModule(inbox => { inbox.Contracts.Register( "payments.commands.process-payment", version: 1); }); liteBus.AddOutboxModule(outbox => { outbox.Contracts.Register( "orders.events.order-submitted", version: 1); }); }); ``` Closed generic messages are supported when each closed type is registered. Open generic contracts are rejected. ```csharp inbox.Contracts.Register>("archive.commands.customer-snapshot", 1); outbox.Contracts.Register>("exports.events.customer-export-completed", 1); ``` ## Step 4: Register Inbox Store Roles 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: ```csharp 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](PostgreSQL-Schema-Management.md). ## Step 5: Run The Inbox Processor From A Worker 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`: ```csharp 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: ```csharp 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](Processor-Hosting.md). Keep retry settings, lease owner, batch size, and polling cadence explicit in your worker setup. ## Step 6: Add Outbox Writes For Integration Events `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. ```csharp 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. ## Step 7: Register Outbox Store Roles And Dispatcher 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: ```csharp builder.Services.AddLiteBus(liteBus => { liteBus.AddOutboxModule(outbox => { outbox.Contracts.Register("orders.events.order-submitted", 1); outbox.UseLiteBusEventDispatcher(); }); liteBus.AddPostgreSqlOutboxStore(postgres => { postgres.UseDataSource(dataSource); }); }); await PostgreSqlOutboxSchema.EnsureAsync(dataSource, cancellationToken: cancellationToken); ``` See [PostgreSQL Schema Management](PostgreSQL-Schema-Management.md) for migration-owned DDL and opt-in host bootstrap. For a broker, register your own `IOutboxDispatcher` and map `OutboxMessageEnvelope.Topic` to the broker destination. ## Step 8: Update Event Handler Predicates `EventMediationRoutingSettings.HandlerPredicate` now applies to both event publishing overloads: ```csharp Task PublishAsync(IEvent @event, EventMediationSettings? settings = null, CancellationToken cancellationToken = default); Task PublishAsync(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. ## Step 9: Update Open Generic Handlers Open generic handlers that LiteBus closes automatically must have one generic type parameter. v5 throws `UnsupportedOpenGenericHandlerException` for unsupported shapes. Before: ```csharp public sealed class AuditPreHandler : ICommandPreHandler where TCommand : ICommand; module.Register(typeof(AuditPreHandler<,>)); ``` After: ```csharp public sealed class AuditPreHandler : ICommandPreHandler 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>`. ## Step 10: Update Descriptor Exception Handling Code or tests that catch `InvalidOperationException` for unresolved message descriptors should catch `MessageDescriptorNotFoundException` instead. ```csharp 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. ## Step 11: Check `System` Namespace Filtering 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. ## Step 12: Move Scripts To `.slnx` The repository now uses `LiteBus.slnx`. Update local scripts and CI commands. ```bash dotnet restore LiteBus.slnx dotnet build LiteBus.slnx --configuration Release --no-restore dotnet test LiteBus.slnx --no-restore ``` Sample projects are included in the root `LiteBus.slnx` under `/samples/`. ## Contributor Notes PostgreSQL integration tests use Testcontainers and require Docker. CI now reports Docker availability before running tests. ## Checklist - Remove `[StoreInInbox]` and old command inbox abstractions from application code. - Use `ICommandMediator.SendAsync` for immediate command execution. - Use `ICommandScheduler.ScheduleAsync` for deferred commands. - Convert deferred result commands to `ICommand` and 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.MessageId` for stable event ids. - Review event handler predicates on `PublishAsync(IEvent, ...)` calls. - Update unsupported open generic handlers. - Catch `MessageDescriptorNotFoundException` for descriptor lookup failures. - Update scripts from `LiteBus.sln` to `LiteBus.slnx`. - Run PostgreSQL integration tests with Docker available.