Migration Guide
This guide covers the patterns and steps required when upgrading between RCommon versions. Because RCommon follows semantic versioning, breaking changes only appear in major version increments.
For the specific changes in each release, see the Changelog or the GitHub Releases page.
General Upgrade Steps
Before upgrading to a new major version, follow these steps:
- Read the release notes for every version between your current version and the target version.
- Update NuGet packages one major version at a time rather than skipping multiple majors.
- Address compiler errors first — these represent API changes that must be resolved.
- Run the full test suite and address any runtime failures.
- Verify logging output during integration tests to catch behavioral differences.
Upgrading Package References
Update all RCommon.* packages in your project files at once. Because all packages share the same version, mixing versions across packages in the same application is not supported and may cause type resolution failures.
<PackageReference Include="RCommon.Core" Version="3.0.0" />
<PackageReference Include="RCommon.Entities" Version="3.0.0" />
<PackageReference Include="RCommon.Persistence" Version="3.0.0" />
<PackageReference Include="RCommon.EFCore" Version="3.0.0" />
<!-- All RCommon.* packages should share the same version -->
Upgrading to Modular Composition
This release introduces modular composition of AddRCommon(). No source changes are required. Existing single-call apps continue to compile and behave identically.
What changed
If you previously relied on WithSequentialGuidGenerator, WithSimpleGuidGenerator, or WithDateTimeSystem throwing RCommonBuilderException on a second call, that behavior is now relaxed:
- Same implementation type, repeated → idempotent no-op.
- Different implementation type → still throws
RCommonBuilderException(with both type names in the message).
Likewise, DataStoreFactoryOptions.Register<B,C>(name) now accepts identical re-registrations:
- Same
(name, B, C)triple → idempotent no-op. - Same
(name, B)with differentC→ still throwsUnsupportedDataStoreException.
What you can now do
Modules can independently configure RCommon features:
public sealed class OrderingModule : IServiceModule
{
public void Configure(IServiceCollection services) =>
services.AddRCommon()
.WithPersistence<EFCorePersistenceBuilder>(ef =>
ef.AddDbContext<OrderingDbContext>("Ordering", o => o.UseInMemoryDatabase("ord")));
}
public sealed class InventoryModule : IServiceModule
{
public void Configure(IServiceCollection services) =>
services.AddRCommon()
.WithPersistence<EFCorePersistenceBuilder>(ef =>
ef.AddDbContext<InventoryDbContext>("Inventory", o => o.UseInMemoryDatabase("inv")));
}
Both modules call AddRCommon() and WithPersistence<EFCorePersistenceBuilder>. The persistence sub-builder's constructor runs exactly once; both AddDbContext calls accumulate registrations.
See Modular Composition for the full conflict matrix and new helper APIs.
Upgrading to 3.2.0 (Event Handling & Outbox Redesign)
3.2.0 reworks event handling and the transactional outbox. It is a breaking release for event-handling code: durability became opt-in, in-process dispatch moved before commit, and the outbox is now datastore-aware. Non-event-handling code (repositories, unit of work, multi-tenancy, blob storage, etc.) is unaffected.
Read the new Event Handling Recipes guide first — it documents the six supported patterns and is the fastest way to see where your existing wiring maps.
1. Durability is now opt-in (most important change)
Before 3.2.0, a domain event raised on a tracked aggregate was automatically written to the outbox. In 3.2.0, Publish<T>()/Send<T>() are transient by default; you must opt in to durability.
// Before 3.2.0 — the event was implicitly persisted to the outbox.
events.AddSubscriber<OrderPlacedEvent, OrderPlacedEventHandler>();
// After 3.2.0 — declare the durable route explicitly.
events.AddSubscriber<OrderPlacedEvent, OrderPlacedEventHandler>(); // in-process, pre-commit (transient)
events.Publish<OrderPlacedEvent>().UseOutbox("AppDb"); // durable: persist + relay
There is no compiler error for this change — code compiles but silently stops persisting events. Audit every place that relied on outbox persistence and add .UseOutbox("store") (per event) or UseRCommonOutbox("store") (builder default). If neither is present, the event is transient.
2. In-process dispatch is now pre-commit
In-process ISubscriber<T> handlers now run inside the unit-of-work transaction, before it commits. A handler that throws rolls the whole transaction back (no state change, no outbox rows). Previously these ran after commit.
Consequence: pre-commit handlers must be in-process and side-effect-light — no external I/O (HTTP calls, sending email, calling another service). Move any such work to a durable outbound route (Publish<T>().UseOutbox(...)) that is relayed after commit.
3. IOutboxStore.SaveAsync gained a dataStoreName parameter
If you implemented a custom IOutboxStore, update the signature:
// Before
Task SaveAsync(OutboxMessage message, CancellationToken cancellationToken = default);
// After
Task SaveAsync(OutboxMessage message, string dataStoreName, CancellationToken cancellationToken = default);
4. Outbox registration: use the non-generic store
EFCoreOutboxStore<TContext> (the per-context subclass) is now [Obsolete]. A single non-generic EFCoreOutboxStore resolves the correct DbContext per datastore. Register it per datastore by name:
// Before (per-context subclass, one type per datastore)
ef.AddOutbox<EFCoreOutboxStore<AppDbContext>>();
// After (one store type, named datastore)
ef.AddOutbox<EFCoreOutboxStore>(dataStoreName: "AppDb");
The store generic is required — the design sketch's AddOutbox(o => o.OnDataStore("AppDb")) (no type argument) is not the shipped API. Use AddOutbox<EFCoreOutboxStore>(...).
5. Broker subscribers: AddSubscriber → Consume
On broker builders (MassTransit/Wolverine), inbound consumers are now registered with Consume<T,H>(). AddSubscriber<T,H>() still works as an [Obsolete] alias that forwards to Consume, so existing code compiles with a warning.
// Before
mt.AddSubscriber<OrderConfirmed, OrderConfirmedConsumer>();
// After
mt.Consume<OrderConfirmed, OrderConfirmedConsumer>();
AddSubscriber<T,H>() remains the correct verb for in-process handlers on the in-memory and MediatR builders.
6. Broker-native outbox: UseBrokerOutbox<TDbContext>
The broker outbox is now configured through a datastore-aware wrapper instead of raw broker APIs. Note the TDbContext generic argument:
// Before (raw MassTransit outbox on a DbContext)
mt.AddOutbox<AppDbContext>(o => { o.UsePostgres(); o.UseBusOutbox(); });
// After (RCommon wrapper bound to a named datastore)
mt.UseBrokerOutbox<AppDbContext>(o => o.OnDataStore("Orders").UsePostgres());
Your DbContext still maps MassTransit's tables with modelBuilder.AddTransactionalOutboxEntities().
Wolverine UseBrokerOutbox is unsupported and throws NotSupportedException. Wolverine's EF Core envelope transaction does not enlist in RCommon's TransactionScope, so atomic staging cannot be guaranteed. Migrate Wolverine outbox code to Recipe 2a — RCommon's own outbox with Wolverine as the relay transport — which gives the same at-least-once guarantee.
7. New delivery verbs
Publish<T>() (fan-out), Send<T>() (point-to-point), and Consume<T,H>() (inbound broker) join AddSubscriber<T,H>(). See the recipes guide for the full matrix of which verb is available on which builder.
Known limitations
Two limitations ship with 3.2.0, each with a supported workaround — see Known Issues:
- Dapper outbox store is SQL Server–only. On PostgreSQL, use
EFCoreOutboxStorefor the outbox datastore. - MediatR
Send<T>()may not resolve handlers. UsePublish<T>()for in-process MediatR event delivery.
Breaking Change Patterns
The following sections describe the categories of breaking changes that appear in major RCommon releases and how to address them.
Repository Interface Changes
When repository interfaces gain or lose methods, implement the new contract on any custom repository implementations you have written:
// Before: your custom repository implementing the old interface
public class MyCustomRepository : ILinqRepository<Product>
{
// ...
}
// After: check what methods were added or removed, then add or remove them
public class MyCustomRepository : ILinqRepository<Product>
{
// Add any new required methods from the updated interface
public async Task<long> CountAsync(Expression<Func<Product, bool>> predicate,
CancellationToken cancellationToken = default)
{
// implementation
}
}
If you only inject the built-in repositories (such as IGraphRepository<T> or ILinqRepository<T>) you are not affected by these changes.
Builder API Changes
The fluent builder methods on IRCommonBuilder occasionally move or are renamed. The compiler will surface these as errors on the AddRCommon(builder => ...) call in Program.cs.
// Before (hypothetical old API)
services.AddRCommon()
.WithPersistence(ef =>
{
ef.UsingEFCore<AppDbContext>("AppDb", /* ... */);
});
// After
services.AddRCommon()
.WithPersistence<EFCorePersistenceBuilder>(ef =>
{
ef.AddDbContext<AppDbContext>("AppDb", /* ... */);
ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb");
});
Entity Base Class Changes
If your entities inherit from BusinessEntity<TKey> or AuditedEntity and those classes gain new abstract or virtual members, you may need to implement or override them.
The most common change is the addition of new opt-in interfaces:
// Adding soft delete support to an existing entity
public class Customer : BusinessEntity<int>, ISoftDelete
{
// Add the required property from the ISoftDelete interface
public bool IsDeleted { get; set; }
// ... existing properties
}
// Adding multitenancy to an existing entity
public class Customer : BusinessEntity<int>, IMultiTenant
{
// Add the required property from the IMultiTenant interface
public string? TenantId { get; set; }
// ... existing properties
}
When you add ISoftDelete to an entity, you must also add a migration to your EF Core project to add the IsDeleted column.
When you add IMultiTenant, you must add a migration to add the TenantId column and ensure the ITenantIdAccessor is configured in DI.
Event Handling Changes
If IEventBus, IEventRouter, IEventProducer, or ISubscriber<T> change signatures, update your handler registrations and any custom event producer implementations.
The most common pattern is subscriber registration moving from one builder method to another:
// Register event handlers through the event handling builder
services.AddRCommon()
.WithEventHandling<InMemoryEventBusBuilder>(events =>
{
events.AddSubscriber<OrderCreatedEvent, OrderCreatedEventHandler>();
events.AddSubscriber<OrderShippedEvent, OrderShippedEventHandler>();
});
Mediator Adapter Changes
If you use IMediatorService through the RCommon.Mediatr adapter, check whether MediatRAdapter or the builder registration changed. The application-facing IMediatorService interface is stable; only the adapter registration may change:
// Configure the mediator with the MediatR adapter
services.AddRCommon()
.WithMediator<MediatRBuilder>(mediator =>
{
mediator.AddRequest<CreateOrderCommand, CreateOrderCommandHandler>();
mediator.AddNotification<OrderCreatedNotification, OrderCreatedHandler>();
mediator.AddLoggingToRequestPipeline();
mediator.AddUnitOfWorkToRequestPipeline();
});
Security and Web Changes
If your application uses ICurrentUser, ITenantIdAccessor, or ICurrentPrincipalAccessor, verify that the registration method you call still exists:
// For ASP.NET Core web applications (reads from HttpContext.User)
services.AddRCommon(config =>
{
config.WithClaimsAndPrincipalAccessorForWeb();
});
// For non-web applications (reads from Thread.CurrentPrincipal)
services.AddRCommon(config =>
{
config.WithClaimsAndPrincipalAccessor();
});
Multi-Tenancy Migration
If you are adding multi-tenancy to an existing application that previously had none, follow this sequence:
- Install
RCommon.MultiTenancyand a provider package (e.g.,RCommon.Finbuckle). - Add
IMultiTenantto any entities that should be tenant-scoped. - Create EF Core migrations for any new
TenantIdcolumns. - Register the tenancy provider in
Program.cs. - Populate
TenantIdon existing rows via a data migration if your application is being retrofitted.
// 1. Set up Finbuckle tenant resolution
builder.Services.AddMultiTenant<TenantInfo>()
.WithHeaderStrategy("X-Tenant")
.WithConfigurationStore();
// 2. Register RCommon with Finbuckle
builder.Services.AddRCommon(config =>
{
config
.WithClaimsAndPrincipalAccessorForWeb()
.WithPersistence<EFCorePersistenceBuilder>(ef =>
{
ef.AddDbContext<AppDbContext>("AppDb", options =>
options.UseSqlServer(connectionString));
ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb");
})
.WithMultiTenancy<FinbuckleMultiTenantBuilder<TenantInfo>>(mt => { });
});
Soft Delete Migration
If you are adding soft delete to entities that previously used physical deletion, follow this sequence:
- Add
ISoftDeleteto the entity class and add theIsDeletedproperty. - Create an EF Core migration to add the
IsDeletedcolumn with a default value offalse. - Verify that any existing raw SQL queries or stored procedures you use are updated to filter on
IsDeleted = 0.
// Entity before soft delete
public class Order : BusinessEntity<int>
{
public string ProductName { get; set; }
}
// Entity after adding soft delete
public class Order : BusinessEntity<int>, ISoftDelete
{
public string ProductName { get; set; }
public bool IsDeleted { get; set; }
}
After adding the migration, soft-deleted records are excluded from all repository queries automatically. Code that previously called DeleteAsync(order) continues to work — it performs a physical delete. To perform a soft delete, call DeleteAsync(order, isSoftDelete: true).
Target Framework Upgrades
RCommon targets .NET 8, .NET 9, and .NET 10. When upgrading your application's target framework, update the TargetFramework in your project file and ensure all RCommon packages are updated to a version that supports the new framework:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
Check the package listing on NuGet to confirm which frameworks each package supports. All current RCommon packages support .NET 8, .NET 9, and .NET 10.
Getting Help
If you encounter an upgrade issue not covered here:
- Search existing GitHub Issues — others may have hit the same problem.
- Start a GitHub Discussion for questions that are not bugs.
- Open a new issue with a minimal reproduction if you believe you have found a bug in the upgrade path.