# RCommon — Full Documentation > This file contains the complete documentation for RCommon, an open-source .NET infrastructure library. > Generated from source documentation at https://rcommon.com/docs > For a summary, see https://rcommon.com/llms.txt --- ## Changelog Source: https://rcommon.com/docs/api-reference/changelog RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags. # Changelog The authoritative release history for all RCommon packages is maintained on GitHub Releases. **Full release notes:** [https://github.com/RCommon-Team/RCommon/releases](https://github.com/RCommon-Team/RCommon/releases) ## Recent Changes ### 3.1.3 Outbox silent-failure hardening. Every change is observability-only (new warnings and a startup diagnostic); no method or interface signatures changed and no runtime behavior changed on the success path — fully backward-compatible. **Fixed** - **Synchronous `UnitOfWork.Commit()` silently dropped pending domain events.** The obsolete `Commit()` completes the transaction but deliberately skips outbox persistence (Phase 1) and dispatch (Phase 3), so any tracked entity carrying domain events lost them with no error. `Commit()` now logs a `Warning` when the event tracker holds entities with un-dispatched local events, directing callers to `CommitAsync`. Because the `AutoComplete`-on-dispose path routes through `Commit()`, that path is now covered too. - **`UnitOfWork` disposed without a successful commit was a silent rollback.** With `AutoComplete` off, disposing a unit of work that never called `CommitAsync` rolls back the ambient transaction and discards any writes made in the scope with no error (e.g. a repository write invoked outside a committing `[UnitOfWork]` scope). The dispose-without-commit path now logs a `Warning` so the discarded work is visible. - **Outbox poller silently dropped events with no subscriber.** When the durable `OutboxProcessingService` drains a message whose event type has no matching subscriber/producer on the poller (processor) host, it marks the row processed and moves on. It now logs a `Warning` once per event type, surfacing the common producer/processor topology mistake of registering a subscriber only on the producer host. **Added** - **Outbox routing startup diagnostic.** `AddOutbox` now registers a hosted diagnostic that warns at startup when the effective `IEventRouter` is the in-memory router instead of the outbox router — i.e. a later event-handling registration silently overrode outbox routing (DI is last-registration-wins), which would cause events to fire in-memory and never persist to the outbox. ### 3.1.2 Modular composition, bootstrapping ergonomics, opt-in CQRS transactions, and a docs consolidation pass covering navigation, eight new runnable examples, an end-to-end DDD recipe, and a machine-readable full API reference. All changes in this release are backward-compatible. **Added** - **Modular composition support** — `services.AddRCommon()` can now be called from multiple modules in the same process. Registrations merge instead of duplicating or throwing on agreement. See [Modular Composition](../core-concepts/modular-composition.mdx). - New public API: `IRCommonBuilder.GetOrAddBuilder(Func)` — third-party `WithX` extensions opt into sub-builder caching. - New public API: `IRCommonBuilder.GetBootstrapDiagnostics()` — retrieve the soft-duplicate report stashed at host startup. - New public API: `IServiceCollection.IsRCommonInitialized()` — true iff `AddRCommon()` has been called. - New internal `IHostedService` runs the duplicate-descriptor scanner at host startup and emits a single warning on soft duplicates. - **`ICqrsBuilder.AddUnitOfWorkToCommandBus()`** — opt-in unit-of-work wrapping for the native `ICommandBus`, via a new `UnitOfWorkCommandBus` decorator. Commands only, not queries; gives the native command bus parity with MediatR's `AddUnitOfWorkToRequestPipeline()`. - **`TenantScope.Bypass()`** (`RCommon.Security.Claims`) — an ambient, `AsyncLocal`-based, nestable scope that suspends tenant-based repository filtering and stamping for its lifetime. A new `TenantScopeAwareTenantIdAccessor` decorator wraps `ClaimsTenantIdAccessor` and Finbuckle's tenant accessor automatically. - When exactly one data store is registered and `SetDefaultDataStore` was never called, it is now auto-inferred (logged at `Information`); registering 2+ stores with no default now also logs informationally instead of only surfacing at first use. - Cross-builder startup diagnostics: a warning is logged when an in-memory event bus builder has subscriptions but no registered producer, and when the event router has events to dispatch but no producers are registered for them (both were previously silent). - 8 new runnable `Examples/` projects: Dapper, Linq2Db, Sagas, Blob Storage (Azure/S3), State Machines (Stateless), Multi-Tenancy (Finbuckle), and Event Handling (Outbox) — plus an end-to-end [Domain-Driven Design recipe](../examples-recipes/domain-driven-design.mdx) (`Team`/`TeamMembership` aggregate). - A machine-readable [full API reference](pathname:///api-reference-full.txt), generated directly from built assemblies and their XML doc-comment sidecars. **Fixed** - `EFCoreAggregateRepository`/`EFCoreRepository.UpdateAsync` misclassified newly-added child entities with non-default keys (e.g. sequential GUIDs) as `Modified` instead of `Added`, silently dropping the insert. Fixed with explicit reference-based change tracking. - `Include`/`ThenInclude` threw for collection navigations ("invalid inside an Include operation"); switched to EF Core's string-path `Include()` overload. - `AddSubscriber()` on `IInMemoryEventBusBuilder` silently did nothing (the handler was never invoked) unless a separate, undocumented producer registration call was also made. The required producer is now auto-registered. - The zero-argument `RCommon.FluentValidation.ValidationBuilderExtensions.WithValidation()` overload — a byte-for-byte duplicate of the `ApplicationServices` overload — was deleted, resolving an unconditional `CS0121` ambiguity error when both packages were referenced together. - **Dapper (`RCommon.Dapper`):** `AddAsync`/`AddRangeAsync` discarded database-generated keys instead of writing them back to the entity after insert. Separately, Dommel's default key resolver treated any `Guid`/`string`-typed `Id` property as database-generated, silently writing `NULL`/default into the primary key column. A new `RCommonKeyPropertyResolver` restricts auto-generation guessing to numeric key types. - **Linq2Db (`RCommon.Linq2Db`):** entities deriving from `BusinessEntity`/`AggregateRoot` failed every insert and query ("no such column") because Linq2Db does not honor `[NotMapped]`. A new `IMetadataReader` implementation excludes the framework-owned event-tracking properties from the entity's column mapping. - **`RCommon.Finbuckle`:** an unused `new()` generic constraint on `FinbuckleMultiTenantBuilder`/`FinbuckleTenantIdAccessor` broke compilation against Finbuckle's built-in `TenantInfo` type (which has `required` members) on net10.0. The constraint is now conditional per target framework, since net8.0/net9.0 reference an older Finbuckle version whose own interfaces still require it. **Changed** - `AddRCommon()` is now idempotent: subsequent calls return the cached `IRCommonBuilder`. - Singleton-style verbs (`WithSimpleGuidGenerator`, `WithSequentialGuidGenerator`, `WithDateTimeSystem`, `WithJsonSerialization`, `WithSmtpEmailServices`, `WithSendGridEmailServices`) are now idempotent on same-type re-registration. Different-type conflicts throw `RCommonBuilderException` with diagnostic messages. - `DataStoreFactoryOptions.Register<,>` is now idempotent for identical `(name, base, concrete)` triples. Conflicts (same `(name, base)` with different concrete) throw `UnsupportedDataStoreException` (exception type preserved). - `AddProducer` now deduplicates by concrete producer type via descriptor scan — distinct producer types still coexist. - All sub-builder `WithX` extensions route through `GetOrAddBuilder`; generic constraints tightened to `where T : class, ...`. - `DataStoreNotFoundException`'s message now lists registered stores and remediation steps (message text only; exception type unchanged). - `EFCorePerisistenceBuilder` (typo) renamed to `EFCorePersistenceBuilder`; the old name remains as an `[Obsolete]` forwarding subclass. - The `WithValidation(Action)` overload is marked `[Obsolete]` in favor of calling `UseWithCqrs(...)` inside `WithValidation(Action)`. **Notes** - Strictly additive public API surface across this release. No method or interface signature removals, and no exception types changed on existing APIs. - See `Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/` for a runnable demonstration of modular composition. ### 3.1.1 Bug-fix and security patch release. **Fixed** - **Cross-host transactional outbox delivery.** Added `OutboxOptions.ImmediateDispatch` (default `true`, fully backward-compatible). When set to `false`, a unit of work persists outbox events but skips the best-effort post-commit in-process dispatch (Phase 3), leaving the durable poller (`OutboxProcessingService`) as the sole dispatcher and marker. This fixes silent loss of cross-host delivery, where a producing host marked rows processed before a subscriber on a different host could consume them. Producer-only hosts should set `ImmediateDispatch = false`. See [Outbox Producer/Processor Topology](../event-handling/outbox-producer-processor-topology.mdx). - Outbox post-commit dispatch no longer marks a message processed when no in-process producer matched the event; it is left for the poller (defense-in-depth). - `EFCoreOutboxStore.DeleteProcessedAsync` / `DeleteDeadLetteredAsync` used a `DateTimeOffset` comparison the SQLite provider could not translate. The comparison is now evaluated client-side; SQL Server / PostgreSQL behaviour is unchanged. **Security** - Pinned patched **Microsoft.OpenApi 2.7.5** in `RCommon.Authorization.Web` (net10 target), resolving CVE-2026-49451 (GHSA-v5pm-xwqc-g5wc), which was pulled transitively via Swashbuckle 10.x. - Upgraded the native SQLite binary (**SQLitePCLRaw.lib.e_sqlite3 3.53.3**) in the SQLite-backed test projects, resolving CVE-2025-6965 (GHSA-2m69-gcr7-jv3q). **Notes** - Strictly additive public API surface (`OutboxOptions.ImmediateDispatch`); no signature removals, and no behavioural change at the default. - Website (Docusaurus) build-tooling npm advisories are tracked as a separate follow-up (Docusaurus 3.10 upgrade + theme swizzle migration). ### Blob Storage Abstractions Added `RCommon.Blobs` as a provider-agnostic blob storage abstraction layer, with two concrete implementations: - `RCommon.Azure.Blobs` — Azure Blob Storage implementation - `RCommon.Amazon.S3Objects` — Amazon S3 implementation ### Multi-Tenancy Support Added complete multi-tenancy infrastructure: - `RCommon.MultiTenancy` — builder abstraction for registering tenancy providers via `WithMultiTenancy()` - `RCommon.Finbuckle` — Finbuckle.MultiTenant integration, providing `FinbuckleTenantIdAccessor` that bridges Finbuckle's resolved tenant context to the `ITenantIdAccessor` consumed by all repositories - All persistence providers (`EFCoreRepository`, `DapperRepository`, `Linq2DbRepository`) automatically filter reads by tenant and stamp writes with the current tenant ID when entities implement `IMultiTenant` ### Domain-Driven Design Support Added soft delete and multitenancy to the entity layer: - `ISoftDelete` — opt-in interface for logical deletion; repositories set `IsDeleted = true` and perform an UPDATE instead of a physical DELETE - `IMultiTenant` — opt-in interface for tenant-scoped entities; repositories filter reads and stamp writes automatically - `SoftDeleteHelper` and `MultiTenantHelper` utility classes for filter expression building and entity stamping - `EntityNotFoundException` for consistent "entity not found" error handling with type and ID context ### Repository Soft Delete Extras Extended all persistence providers with explicit soft delete and bulk operation support: - `DeleteAsync(entity, isSoftDelete: bool)` overload on all write repositories - Automatic `!IsDeleted` filtering on all read operations for `ISoftDelete` entities - Bulk delete via `ExecuteDeleteAsync` on `EFCoreRepository` ### Editorconfig and Code Hygiene Added `.editorconfig` to enforce consistent code style across the solution. All public methods and complex code paths now carry XML documentation comments. ## Versioning RCommon uses [MinVer](https://github.com/adamralph/minver) for automatic semantic versioning from git tags. All packages in the solution share the same version number. Version numbers follow [Semantic Versioning](https://semver.org/): - **Major** (`x.0.0`) — breaking API changes - **Minor** (`0.x.0`) — new features, backward-compatible additions - **Patch** (`0.0.x`) — bug fixes and documentation updates Pre-release versions (alpha, beta, rc) are tagged accordingly (e.g., `v3.0.0-alpha.1`). ## Release Schedule RCommon does not follow a fixed release cadence. Releases are made when meaningful features, bug fixes, or breaking changes are ready. Subscribe to [GitHub Releases](https://github.com/RCommon-Team/RCommon/releases) or watch the repository to be notified of new versions. ## Filing Issues Found a bug or missing a feature? Please open an issue on GitHub: - **Bug reports:** [https://github.com/RCommon-Team/RCommon/issues/new?template=bug_report.md](https://github.com/RCommon-Team/RCommon/issues/new) - **Feature requests:** [https://github.com/RCommon-Team/RCommon/issues/new?template=feature_request.md](https://github.com/RCommon-Team/RCommon/issues/new) - **Discussions:** [https://github.com/RCommon-Team/RCommon/discussions](https://github.com/RCommon-Team/RCommon/discussions) --- ## migration-guide Source: https://rcommon.com/docs/api-reference/migration-guide --- title: Migration Guide sidebar_position: 3 description: Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades. --- # 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](./changelog.mdx) or the [GitHub Releases page](https://github.com/RCommon-Team/RCommon/releases). ## General Upgrade Steps Before upgrading to a new major version, follow these steps: 1. Read the release notes for every version between your current version and the target version. 2. Update NuGet packages one major version at a time rather than skipping multiple majors. 3. Address compiler errors first — these represent API changes that must be resolved. 4. Run the full test suite and address any runtime failures. 5. 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. ```xml ``` ## 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(name)` now accepts identical re-registrations: - **Same `(name, B, C)` triple** → idempotent no-op. - **Same `(name, B)` with different `C`** → still throws `UnsupportedDataStoreException`. ### What you can now do Modules can independently configure RCommon features: ```csharp public sealed class OrderingModule : IServiceModule { public void Configure(IServiceCollection services) => services.AddRCommon() .WithPersistence(ef => ef.AddDbContext("Ordering", o => o.UseInMemoryDatabase("ord"))); } public sealed class InventoryModule : IServiceModule { public void Configure(IServiceCollection services) => services.AddRCommon() .WithPersistence(ef => ef.AddDbContext("Inventory", o => o.UseInMemoryDatabase("inv"))); } ``` Both modules call `AddRCommon()` and `WithPersistence`. The persistence sub-builder's constructor runs exactly once; both `AddDbContext` calls accumulate registrations. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix and new helper APIs. ## 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: ```csharp // Before: your custom repository implementing the old interface public class MyCustomRepository : ILinqRepository { // ... } // After: check what methods were added or removed, then add or remove them public class MyCustomRepository : ILinqRepository { // Add any new required methods from the updated interface public async Task CountAsync(Expression> predicate, CancellationToken cancellationToken = default) { // implementation } } ``` If you only inject the built-in repositories (such as `IGraphRepository` or `ILinqRepository`) 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`. ```csharp // Before (hypothetical old API) services.AddRCommon() .WithPersistence(ef => { ef.UsingEFCore("AppDb", /* ... */); }); // After services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext("AppDb", /* ... */); ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb"); }); ``` ### Entity Base Class Changes If your entities inherit from `BusinessEntity` 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: ```csharp // Adding soft delete support to an existing entity public class Customer : BusinessEntity, 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, 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` 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: ```csharp // Register event handlers through the event handling builder services.AddRCommon() .WithEventHandling(events => { events.AddSubscriber(); events.AddSubscriber(); }); ``` ### 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: ```csharp // Configure the mediator with the MediatR adapter services.AddRCommon() .WithMediator(mediator => { mediator.AddRequest(); mediator.AddNotification(); 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: ```csharp // 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: 1. Install `RCommon.MultiTenancy` and a provider package (e.g., `RCommon.Finbuckle`). 2. Add `IMultiTenant` to any entities that should be tenant-scoped. 3. Create EF Core migrations for any new `TenantId` columns. 4. Register the tenancy provider in `Program.cs`. 5. Populate `TenantId` on existing rows via a data migration if your application is being retrofitted. ```csharp // 1. Set up Finbuckle tenant resolution builder.Services.AddMultiTenant() .WithHeaderStrategy("X-Tenant") .WithConfigurationStore(); // 2. Register RCommon with Finbuckle builder.Services.AddRCommon(config => { config .WithClaimsAndPrincipalAccessorForWeb() .WithPersistence(ef => { ef.AddDbContext("AppDb", options => options.UseSqlServer(connectionString)); ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb"); }) .WithMultiTenancy>(mt => { }); }); ``` ## Soft Delete Migration If you are adding soft delete to entities that previously used physical deletion, follow this sequence: 1. Add `ISoftDelete` to the entity class and add the `IsDeleted` property. 2. Create an EF Core migration to add the `IsDeleted` column with a default value of `false`. 3. Verify that any existing raw SQL queries or stored procedures you use are updated to filter on `IsDeleted = 0`. ```csharp // Entity before soft delete public class Order : BusinessEntity { public string ProductName { get; set; } } // Entity after adding soft delete public class Order : BusinessEntity, 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: ```xml ``` 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](https://github.com/RCommon-Team/RCommon/issues) — others may have hit the same problem. - Start a [GitHub Discussion](https://github.com/RCommon-Team/RCommon/discussions) 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. --- ## nuget-packages Source: https://rcommon.com/docs/api-reference/nuget-packages --- title: NuGet Packages sidebar_position: 1 description: Complete NuGet package listing for RCommon covering persistence, caching, messaging, mediator, serialization, email, security, multitenancy, and blob storage providers. --- # NuGet Packages All RCommon packages target .NET 8, .NET 9, and .NET 10 and are published to [NuGet.org](https://www.nuget.org/profiles/RCommon). The packages follow a layered design: a small set of foundation packages define abstractions and extension points, and provider-specific packages supply concrete implementations. ## Foundation Packages These packages define the abstractions that your application code programs against. They do not pull in any third-party libraries beyond the Microsoft.Extensions ecosystem. | Package | Description | |---|---| | [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) | Fluent `AddRCommon()` builder, in-memory event bus, `IEventRouter`, guard clauses, GUID generators, `ISystemTime`, extension methods, and reflection utilities | | [RCommon.Entities](https://www.nuget.org/packages/RCommon.Entities) | `BusinessEntity`, `AuditedEntity`, transactional event tracking, `IEntityEventTracker`, soft delete (`ISoftDelete`), and multitenancy (`IMultiTenant`) | | [RCommon.Models](https://www.nuget.org/packages/RCommon.Models) | CQRS contracts (`ICommand`, `IQuery`), event markers (`IAsyncEvent`, `ISyncEvent`), `ExecutionResult`, and pagination models | | [RCommon.Persistence](https://www.nuget.org/packages/RCommon.Persistence) | Repository pattern interfaces (`IReadOnlyRepository`, `IWriteOnlyRepository`, `ILinqRepository`, `IGraphRepository`, `ISqlMapperRepository`), unit of work, specification pattern, and `IDataStoreFactory` | | [RCommon.Caching](https://www.nuget.org/packages/RCommon.Caching) | `ICacheService`, `CacheKey`, and builder contracts for memory and distributed caching providers | | [RCommon.Emailing](https://www.nuget.org/packages/RCommon.Emailing) | `IEmailService` abstraction with a built-in SMTP implementation | | [RCommon.Security](https://www.nuget.org/packages/RCommon.Security) | `ICurrentUser`, `ICurrentClient`, `ICurrentPrincipalAccessor`, `ITenantIdAccessor`, claims extensions, and `AuthorizationException` | | [RCommon.Mediator](https://www.nuget.org/packages/RCommon.Mediator) | `IMediatorService`, `IMediatorAdapter`, and request/notification marker interfaces for library-agnostic mediator usage | | [RCommon.ApplicationServices](https://www.nuget.org/packages/RCommon.ApplicationServices) | CQRS command and query buses (`ICommandBus`, `IQueryBus`), handler registration, and `IValidationService` pipeline integration | | [RCommon.MultiTenancy](https://www.nuget.org/packages/RCommon.MultiTenancy) | `WithMultiTenancy()` builder extension and `IMultiTenantBuilder` interface for registering tenancy providers | ## Persistence Providers Drop-in implementations of the persistence abstractions for the ORM or SQL mapper of your choice. | Package | ORM / Driver | Description | |---|---|---| | [RCommon.EFCore](https://www.nuget.org/packages/RCommon.EFCore) | Entity Framework Core | `EFCoreRepository`, `RCommonDbContext`, `EFCorePersistenceBuilder`; full LINQ, eager loading, change tracking, and bulk delete | | [RCommon.Dapper](https://www.nuget.org/packages/RCommon.Dapper) | Dapper + Dommel | `DapperRepository`, `RDbConnection`, `DapperPersistenceBuilder`; expression-based CRUD via Dommel | | [RCommon.Linq2Db](https://www.nuget.org/packages/RCommon.Linq2Db) | Linq2Db | `Linq2DbRepository`, `RCommonDataConnection`, `Linq2DbPersistenceBuilder`; full LINQ, eager loading via `LoadWith`, and paginated queries | ### Choosing a Persistence Provider Best when you need full change tracking, navigation properties, eager loading with `Include`/`ThenInclude`, and rich LINQ queries. Supports `IGraphRepository`, `ILinqRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository`. ```csharp builder.Services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext("AppDb", options => options.UseSqlServer(connectionString)); ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb"); }); ``` Best for performance-critical paths where you want lightweight SQL mapping without the overhead of a full ORM. Uses Dommel for expression-based queries. Supports `ISqlMapperRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository`. ```csharp builder.Services.AddRCommon() .WithPersistence(dapper => { dapper.AddDbConnection("AppDb", options => { options.DbFactory = SqlClientFactory.Instance; options.ConnectionString = connectionString; }); dapper.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb"); }); ``` Best when you need LINQ-based queries and eager loading with a lighter runtime than EF Core. Supports `ILinqRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository`. ```csharp builder.Services.AddRCommon() .WithPersistence(linq2db => { linq2db.AddDataConnection("AppDb", (sp, options) => options.UseSqlServer(connectionString)); linq2db.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb"); }); ``` ## Caching Providers | Package | Backing Store | Description | |---|---|---| | [RCommon.MemoryCache](https://www.nuget.org/packages/RCommon.MemoryCache) | `IMemoryCache` / `IDistributedCache` (in-memory) | Two `ICacheService` implementations: `InMemoryCacheService` (in-process) and `DistributedMemoryCacheService` | | [RCommon.RedisCache](https://www.nuget.org/packages/RCommon.RedisCache) | Redis via StackExchange.Redis | `RedisCacheService` implementing `ICacheService` with JSON serialization | | [RCommon.Persistence.Caching](https://www.nuget.org/packages/RCommon.Persistence.Caching) | Any `ICacheService` | Caching decorator repositories (`CachingGraphRepository`, `CachingLinqRepository`, `CachingSqlMapperRepository`) wrapping any persistence provider | | [RCommon.Persistence.Caching.MemoryCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.MemoryCache) | Memory | Wires `InMemoryCacheService` into the persistence caching decorators | | [RCommon.Persistence.Caching.RedisCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.RedisCache) | Redis | Wires `RedisCacheService` into the persistence caching decorators | ## Messaging Providers These packages bridge RCommon's `IEventProducer` and `ISubscriber` abstractions to specific messaging libraries. | Package | Library | Description | |---|---|---| | [RCommon.MassTransit](https://www.nuget.org/packages/RCommon.MassTransit) | MassTransit | Publish/send event producers and `MassTransitEventHandler` consumer bridge | | [RCommon.MassTransit.Outbox](https://www.nuget.org/packages/RCommon.MassTransit.Outbox) | MassTransit + EF Core | Entity Framework Core outbox integration for durable MassTransit messaging | | [RCommon.MassTransit.StateMachines](https://www.nuget.org/packages/RCommon.MassTransit.StateMachines) | MassTransit | Dictionary-based state machine adapter implementing `IStateMachine` | | [RCommon.Wolverine](https://www.nuget.org/packages/RCommon.Wolverine) | Wolverine | Publish/send event producers and `WolverineEventHandler` message handler bridge | | [RCommon.Wolverine.Outbox](https://www.nuget.org/packages/RCommon.Wolverine.Outbox) | Wolverine + EF Core | Wolverine durable messaging outbox integration for RCommon | ## Mediator Providers | Package | Library | Description | |---|---|---| | [RCommon.Mediatr](https://www.nuget.org/packages/RCommon.Mediatr) | MediatR | `MediatRAdapter` implementing `IMediatorAdapter`; event producers, notification/request handlers, and pipeline behaviors (logging, validation, unit of work) | ## Serialization | Package | Library | Description | |---|---|---| | [RCommon.Json](https://www.nuget.org/packages/RCommon.Json) | — | `IJsonSerializer` abstraction used internally by caching and messaging packages | | [RCommon.SystemTextJson](https://www.nuget.org/packages/RCommon.SystemTextJson) | System.Text.Json | `IJsonSerializer` implementation using `System.Text.Json` | | [RCommon.JsonNet](https://www.nuget.org/packages/RCommon.JsonNet) | Newtonsoft.Json | `IJsonSerializer` implementation using Newtonsoft.Json | ## Email Providers | Package | Provider | Description | |---|---|---| | [RCommon.Emailing](https://www.nuget.org/packages/RCommon.Emailing) | SMTP | Built-in SMTP implementation (`SmtpEmailService`) and the `IEmailService` abstraction | | [RCommon.SendGrid](https://www.nuget.org/packages/RCommon.SendGrid) | SendGrid API | `SendGridEmailService` implementing `IEmailService` via the SendGrid API client | ## Security and Web | Package | Description | |---|---| | [RCommon.Security](https://www.nuget.org/packages/RCommon.Security) | Claims-based `ICurrentUser`, `ICurrentClient`, `ITenantIdAccessor`, and principal accessor abstractions | | [RCommon.Web](https://www.nuget.org/packages/RCommon.Web) | `HttpContextCurrentPrincipalAccessor` for ASP.NET Core; use `WithClaimsAndPrincipalAccessorForWeb()` instead of the non-web variant | | [RCommon.Authorization.Web](https://www.nuget.org/packages/RCommon.Authorization.Web) | Swashbuckle/OpenAPI operation filters that surface authorization requirements in Swagger UI | ## Multitenancy | Package | Provider | Description | |---|---|---| | [RCommon.MultiTenancy](https://www.nuget.org/packages/RCommon.MultiTenancy) | — | Builder abstraction (`WithMultiTenancy`) for registering tenancy providers | | [RCommon.Finbuckle](https://www.nuget.org/packages/RCommon.Finbuckle) | Finbuckle.MultiTenant | `FinbuckleTenantIdAccessor` bridging Finbuckle's tenant context to `ITenantIdAccessor` | ## Validation | Package | Library | Description | |---|---|---| | [RCommon.FluentValidation](https://www.nuget.org/packages/RCommon.FluentValidation) | FluentValidation | `FluentValidationProvider` implementing `IValidationProvider`; resolves and runs all `IValidator` instances from DI | ## State Machines | Package | Library | Description | |---|---|---| | [RCommon.Stateless](https://www.nuget.org/packages/RCommon.Stateless) | Stateless | Adapter wrapping the Stateless library to implement `IStateMachine` | ## Blob Storage | Package | Provider | Description | |---|---|---| | [RCommon.Blobs](https://www.nuget.org/packages/RCommon.Blobs) | — | Blob storage abstraction layer | | [RCommon.Azure.Blobs](https://www.nuget.org/packages/RCommon.Azure.Blobs) | Azure Blob Storage | Azure Blob Storage implementation | | [RCommon.Amazon.S3Objects](https://www.nuget.org/packages/RCommon.Amazon.S3Objects) | Amazon S3 | Amazon S3 implementation | ## Dependency Map The following shows which foundation packages each provider depends on: ``` RCommon.Core └─ RCommon.Entities └─ RCommon.Persistence ├─ RCommon.EFCore ├─ RCommon.Dapper ├─ RCommon.Linq2Db └─ RCommon.ApplicationServices (IUnitOfWorkFactory, for AddUnitOfWorkToCommandBus()) RCommon.Models └─ RCommon.ApplicationServices (also depends on RCommon.Persistence, above) └─ RCommon.FluentValidation (optional validation pipeline) RCommon.Mediator └─ RCommon.Mediatr RCommon.Core (IEventProducer, ISubscriber) ├─ RCommon.MassTransit └─ RCommon.Wolverine RCommon.Caching ├─ RCommon.MemoryCache └─ RCommon.RedisCache RCommon.Emailing └─ RCommon.SendGrid RCommon.Security ├─ RCommon.Web └─ RCommon.MultiTenancy └─ RCommon.Finbuckle ``` ## Machine-Readable API Reference For the complete public API surface of every RCommon package (every public type and member, with signatures and XML doc-comment summaries), see [`/api-reference-full.txt`](pathname:///api-reference-full.txt). It's generated directly from the built assemblies and their XML doc-comment sidecars, so it never goes stale relative to the code. For prose documentation aimed at LLMs, see [`/llms-full.txt`](pathname:///llms-full.txt). --- ## clean-architecture Source: https://rcommon.com/docs/architecture-guides/clean-architecture --- title: Clean Architecture sidebar_position: 1 description: Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers. --- # Clean Architecture with RCommon Clean Architecture organizes code into concentric layers where the dependency rule states that source code dependencies can only point inward. The domain and application layers have no knowledge of databases, frameworks, or external systems. RCommon supports this structure by providing infrastructure abstractions that your application layer depends on via interfaces, with concrete implementations registered at the composition root. ## When to Use This Approach Use Clean Architecture with RCommon when: - Your domain logic is complex enough to warrant isolation from infrastructure concerns - You want to swap persistence providers (EF Core to NHibernate, SQL to NoSQL) without rewriting business logic - You need testable handlers that do not depend on a real database or message broker - Multiple teams work on different layers independently ## Layer Overview Clean Architecture splits the codebase into four layers. With RCommon, the boundaries map as follows: ``` +-------------------------------+ | Presentation | ASP.NET Core Controllers / MVC +-------------------------------+ | Application | Command/Query Handlers, DTOs, Validation +-------------------------------+ | Domain | Entities, Specifications, Domain Events +-------------------------------+ | Infrastructure | EF Core DbContext, Identity, Email, etc. +-------------------------------+ ``` Dependency direction: Presentation -> Application -> Domain. Infrastructure implements the interfaces defined in Application. ## Project Structure The HR Leave Management sample included with RCommon demonstrates this layout: ``` HR.LeaveManagement.Domain/ LeaveType.cs LeaveRequest.cs LeaveAllocation.cs Common/BaseDomainEntity.cs Specifications/AllocationExistsSpec.cs HR.LeaveManagement.Application/ Features/ LeaveTypes/ Requests/Commands/CreateLeaveTypeCommand.cs Requests/Queries/GetLeaveTypeListRequest.cs Handlers/Commands/CreateLeaveTypeCommandHandler.cs Handlers/Queries/GetLeaveTypeListRequestHandler.cs DTOs/LeaveType/ Contracts/Identity/IUserService.cs ApplicationServicesRegistration.cs HR.LeaveManagement.Persistence/ LeaveManagementDbContext.cs Configurations/ HR.LeaveManagement.Identity/ IdentityServicesRegistration.cs Services/AuthService.cs HR.LeaveManagement.API/ Controllers/LeaveTypesController.cs Program.cs ``` Each project references only what is allowed by the dependency rule. The Domain project has no external references beyond RCommon.Entities. The Application project references Domain but not Persistence or Identity. ## The Domain Layer Domain entities inherit from RCommon's `AuditedEntity` base class, which provides auditing fields (created by, modified by, timestamps) automatically. ```csharp // HR.LeaveManagement.Domain/Common/BaseDomainEntity.cs using RCommon.Entities; namespace HR.LeaveManagement.Domain.Common { public abstract class BaseDomainEntity : AuditedEntity { } } // HR.LeaveManagement.Domain/LeaveType.cs public class LeaveType : BaseDomainEntity { public string Name { get; set; } public int DefaultDays { get; set; } } // HR.LeaveManagement.Domain/LeaveRequest.cs public class LeaveRequest : BaseDomainEntity { public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public LeaveType LeaveType { get; set; } public int LeaveTypeId { get; set; } public DateTime DateRequested { get; set; } public string RequestComments { get; set; } public bool? Approved { get; set; } public bool Cancelled { get; set; } public string RequestingEmployeeId { get; set; } } ``` ### Specifications Domain specifications encapsulate business query logic as reusable, composable objects. RCommon's `Specification` provides this: ```csharp // HR.LeaveManagement.Domain/Specifications/AllocationExistsSpec.cs using RCommon; public class AllocationExistsSpec : Specification { public AllocationExistsSpec(string userId, int leaveTypeId, int period) : base(q => q.EmployeeId == userId && q.LeaveTypeId == leaveTypeId && q.Period == period) { } } ``` The specification is used in the Application layer without any knowledge of EF Core or SQL: ```csharp var allocationCount = await _leaveAllocationRepository .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period)); ``` ## The Application Layer The Application layer contains commands, queries, and their handlers. Handlers implement RCommon's `IAppRequestHandler` interface. ### Commands A command encapsulates the intent to change state: ```csharp // CreateLeaveTypeCommand.cs using RCommon.Mediator.Subscribers; public class CreateLeaveTypeCommand : IAppRequest { public CreateLeaveTypeDto LeaveTypeDto { get; set; } } ``` The handler receives the command, runs validation through `IValidationService`, and persists via the repository interface: ```csharp // CreateLeaveTypeCommandHandler.cs public class CreateLeaveTypeCommandHandler : IAppRequestHandler { private readonly IGraphRepository _leaveTypeRepository; private readonly IValidationService _validationService; public CreateLeaveTypeCommandHandler( IGraphRepository leaveTypeRepository, IValidationService validationService) { _leaveTypeRepository = leaveTypeRepository; _validationService = validationService; _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement; } public async Task HandleAsync( CreateLeaveTypeCommand request, CancellationToken cancellationToken) { var response = new BaseCommandResponse(); var validationResult = await _validationService.ValidateAsync(request.LeaveTypeDto); if (!validationResult.IsValid) { response.Success = false; response.Message = "Creation Failed"; response.Errors = validationResult.Errors .Select(q => q.ErrorMessage).ToList(); } else { var leaveType = request.LeaveTypeDto.ToLeaveType(); await _leaveTypeRepository.AddAsync(leaveType); response.Success = true; response.Message = "Creation Successful"; response.Id = leaveType.Id; } return response; } } ``` ### Queries Queries return data without modifying state: ```csharp // GetLeaveTypeListRequestHandler.cs public class GetLeaveTypeListRequestHandler : IAppRequestHandler> { private readonly IGraphRepository _leaveTypeRepository; public GetLeaveTypeListRequestHandler(IGraphRepository leaveTypeRepository) { _leaveTypeRepository = leaveTypeRepository; _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement; } public async Task> HandleAsync( GetLeaveTypeListRequest request, CancellationToken cancellationToken) { var leaveTypes = await _leaveTypeRepository.FindAsync(x => true); return leaveTypes.Select(x => x.ToLeaveTypeDto()).ToList(); } } ``` ### Cross-Cutting Concerns in the Application Layer The Application layer defines contracts for cross-cutting concerns as interfaces. Infrastructure implements them: ```csharp // Contracts/Identity/IUserService.cs public interface IUserService { Task> GetEmployees(); } // Contracts/Identity/IAuthService.cs public interface IAuthService { Task Login(AuthRequest request); Task Register(RegistrationRequest request); } ``` ## The Infrastructure Layer The `LeaveManagementDbContext` inherits from RCommon's `AuditableDbContext`, which automatically stamps the `CreatedBy`, `ModifiedBy`, `CreateDate`, and `ModifyDate` fields on every save operation: ```csharp // HR.LeaveManagement.Persistence/LeaveManagementDbContext.cs public class LeaveManagementDbContext : AuditableDbContext { public LeaveManagementDbContext( DbContextOptions options, ICurrentUser currentUser, ISystemTime systemTime) : base(options, currentUser, systemTime) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly( typeof(LeaveManagementDbContext).Assembly); } public DbSet LeaveRequests { get; set; } public DbSet LeaveTypes { get; set; } public DbSet LeaveAllocations { get; set; } } ``` ## The Composition Root (Program.cs) All layers are wired together in `Program.cs` using RCommon's fluent builder. This is the only place where concrete implementations are named: :::tip Modular composition A single `Program.cs` is the easiest pattern, but it is not the only one. RCommon's bootstrapper is cache-aware: each layer (or feature folder) can expose its own static `Add*ModuleServices(IServiceCollection)` method that calls `services.AddRCommon().With...(...)` independently, and the registrations merge predictably. Layer-owned wiring lets the Application layer register handlers and validators while the Infrastructure layer registers the `DbContext` and SendGrid — without coordinating through one giant `Program.cs`. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full contract. ::: ```csharp builder.Services.AddRCommon() .WithClaimsAndPrincipalAccessor() .WithSendGridEmailServices(x => { var sendGridSettings = builder.Configuration.Get(); x.SendGridApiKey = sendGridSettings.SendGridApiKey; x.FromNameDefault = sendGridSettings.FromNameDefault; x.FromEmailDefault = sendGridSettings.FromEmailDefault; }) .WithDateTimeSystem(dateTime => dateTime.Kind = DateTimeKind.Utc) .WithSequentialGuidGenerator(guid => guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString) .WithMediator(mediator => { mediator.AddRequest(); mediator.AddRequest, GetLeaveTypeListRequestHandler>(); // ... register all handlers mediator.Configure(config => { config.RegisterServicesFromAssemblies( typeof(ApplicationServicesRegistration).GetTypeInfo().Assembly); }); mediator.AddLoggingToRequestPipeline(); mediator.AddUnitOfWorkToRequestPipeline(); }) .WithPersistence(ef => { ef.AddDbContext( DataStoreNamesConst.LeaveManagement, options => { options.UseSqlServer( builder.Configuration.GetConnectionString( DataStoreNamesConst.LeaveManagement)); }); ef.SetDefaultDataStore(dataStore => { dataStore.DefaultDataStoreName = DataStoreNamesConst.LeaveManagement; }); }) .WithUnitOfWork(unitOfWork => { unitOfWork.SetOptions(options => { options.AutoCompleteScope = true; options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }) .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining( typeof(ApplicationServicesRegistration)); }); ``` ## The Presentation Layer Controllers depend only on `IMediatorService`. They dispatch commands and queries without knowing which handler handles them: ```csharp [Route("api/[controller]")] [ApiController] [Authorize] public class LeaveTypesController : ControllerBase { private readonly IMediatorService _mediator; public LeaveTypesController(IMediatorService mediator) { _mediator = mediator; } [HttpGet] public async Task>> Get() { var leaveTypes = await _mediator.Send>( new GetLeaveTypeListRequest()); return Ok(leaveTypes); } [HttpPost] [Authorize(Roles = "Administrator")] public async Task> Post( [FromBody] CreateLeaveTypeDto leaveType) { var command = new CreateLeaveTypeCommand { LeaveTypeDto = leaveType }; var response = await _mediator.Send(command); return Ok(response); } } ``` ## Testing Application Handlers Because handlers depend only on interfaces, they can be tested with mocks. No database or web server is required: ```csharp [TestFixture] public class CreateLeaveTypeCommandHandlerTests { private readonly CreateLeaveTypeDto _leaveTypeDto; private readonly CreateLeaveTypeCommandHandler _handler; public CreateLeaveTypeCommandHandlerTests() { var mock = new Mock>(); var validationMock = new Mock(); _leaveTypeDto = new CreateLeaveTypeDto { DefaultDays = 15, Name = "Test DTO" }; validationMock .Setup(x => x.ValidateAsync(_leaveTypeDto, false, CancellationToken.None)) .Returns(() => Task.FromResult(new ValidationOutcome())); _handler = new CreateLeaveTypeCommandHandler(mock.Object, validationMock.Object); } [Test] public async Task Valid_LeaveType_Added() { var result = await _handler.HandleAsync( new CreateLeaveTypeCommand { LeaveTypeDto = _leaveTypeDto }, CancellationToken.None); result.ShouldBeOfType(); } } ``` ## Key Design Decisions **`IGraphRepository` as the persistence abstraction.** Handlers declare a dependency on `IGraphRepository`. EF Core, NHibernate, or any future provider implements this interface. Swapping providers requires a one-line change in the composition root. **`DataStoreName` for multi-context routing.** When an application has multiple databases, handlers set `repository.DataStoreName` to tell RCommon which registered `DbContext` to use. This keeps the routing decision in the handler where it is most visible. **`AuditableDbContext` for automatic audit trails.** Inheriting from `AuditableDbContext` removes the need for explicit audit field management across every handler. The `ICurrentUser` and `ISystemTime` services supply the values automatically. **Pipeline behaviors via `.AddLoggingToRequestPipeline()` and `.AddUnitOfWorkToRequestPipeline()`.** These decorators wrap every handler with structured logging and unit-of-work demarcation without modifying the handler code. ## API Reference | Type | Package | Purpose | |------|---------|---------| | `AuditedEntity` | `RCommon.Entities` | Base class for domain entities with auditing | | `Specification` | `RCommon` | Composable predicate for domain queries | | `IAppRequest` | `RCommon.Mediator` | Marker interface for commands and queries | | `IAppRequestHandler` | `RCommon.Mediator` | Handler contract | | `IGraphRepository` | `RCommon.Persistence` | Full-featured repository with LINQ support | | `AuditableDbContext` | `RCommon.Persistence.EFCore` | EF Core DbContext with automatic audit stamping | | `IMediatorService` | `RCommon.Mediator` | Dispatcher used in controllers | | `IValidationService` | `RCommon.ApplicationServices` | Validation abstraction used in handlers | --- ## Event-Driven Architecture Source: https://rcommon.com/docs/architecture-guides/event-driven Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers. # Event-Driven Architecture with RCommon Event-driven architecture decouples components by having them communicate through events rather than direct calls. Producers emit events without knowing which consumers exist. Consumers react to events without knowing which producer emitted them. RCommon provides a consistent `IEventProducer` / `ISubscriber` abstraction over in-process buses (InMemoryEventBus, MediatR) and distributed brokers (MassTransit, Wolverine). ## When to Use This Approach Use event-driven design with RCommon when: - Side effects (sending email, updating a read model, audit logging) should not be tangled into command handlers - You want to scale event consumers independently from producers - Components must remain decoupled so they can evolve at different rates - You need reliable delivery guarantees provided by a message broker ## Core Concepts ### Events An event represents something that happened. It is named in the past tense and is immutable after construction: ```csharp using RCommon.Models.Events; // An in-process domain event public class LeaveRequestApprovedEvent : ISyncEvent { public LeaveRequestApprovedEvent() { } public LeaveRequestApprovedEvent(int leaveRequestId, string employeeId) { LeaveRequestId = leaveRequestId; EmployeeId = employeeId; } public int LeaveRequestId { get; } public string EmployeeId { get; } } ``` `ISyncEvent` is the base marker interface. All event types in RCommon derive from it. ### Producers A producer publishes an event to one or more subscribers. At the call site, you depend on `IEventProducer`: ```csharp public class ApproveLeaveRequestCommandHandler : IAppRequestHandler { private readonly IGraphRepository _repository; private readonly IEnumerable _eventProducers; public ApproveLeaveRequestCommandHandler( IGraphRepository repository, IEnumerable eventProducers) { _repository = repository; _eventProducers = eventProducers; } public async Task HandleAsync( ApproveLeaveRequestCommand request, CancellationToken cancellationToken) { var leaveRequest = await _repository.FindAsync(request.Id); leaveRequest.Approved = true; await _repository.UpdateAsync(leaveRequest); var @event = new LeaveRequestApprovedEvent(leaveRequest.Id, leaveRequest.RequestingEmployeeId); foreach (var producer in _eventProducers) { await producer.ProduceEventAsync(@event); } return new BaseCommandResponse { Success = true }; } } ``` ### Subscribers A subscriber reacts to a specific event type. It implements `ISubscriber`: ```csharp public class SendApprovalEmailHandler : ISubscriber { private readonly IEmailService _emailService; public SendApprovalEmailHandler(IEmailService emailService) { _emailService = emailService; } public async Task HandleAsync( LeaveRequestApprovedEvent notification, CancellationToken cancellationToken = default) { await _emailService.SendAsync(new EmailRequest { To = notification.EmployeeId, Subject = "Leave Request Approved", Body = $"Your leave request #{notification.LeaveRequestId} has been approved." }); } } ``` ## Event Handling Providers RCommon ships with four event handling providers. You choose one (or more) at startup via the fluent builder. :::tip Modular composition `WithEventHandling` is cache-aware. In a modular codebase, each feature module can call `services.AddRCommon().WithEventHandling(eh => eh.AddSubscriber())` independently — the cached `InMemoryEventBusBuilder` is reused across modules and subscriber/producer registrations accumulate. `AddProducer` deduplicates by concrete producer type, so the same producer registered from two modules yields exactly one descriptor. This pattern works for `InMemoryEventBusBuilder`, `MediatREventHandlingBuilder`, and `WolverineEventHandlingBuilder`; `MassTransitEventHandlingBuilder` has a known cross-module limitation documented on its [MassTransit page](../event-handling/masstransit.mdx#configuration). See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ### In-Memory Event Bus The simplest provider. Events are dispatched synchronously within the same process. No external broker required. Best for domain events within a single service. ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` ### MediatR Uses MediatR's notification pipeline. Integrates with existing MediatR configurations and supports MediatR pipeline behaviors: ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` ### MassTransit Durable, distributed messaging. Supports RabbitMQ, Azure Service Bus, Amazon SQS, and others. Events survive process restarts and can be delivered to subscribers in separate services: ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.UsingRabbitMq((context, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(context); }); eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` For development and testing, use the in-memory transport instead of RabbitMQ: ```csharp eventHandling.UsingInMemory((context, cfg) => { cfg.ConfigureEndpoints(context); }); ``` ### Wolverine Wolverine focuses on high throughput and local queue processing: ```csharp // In Program.cs, before services.AddRCommon() builder.Host.UseWolverine(options => { options.LocalQueue("leave-events"); }); services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` ## Subscription Isolation When a single service needs both in-process events and cross-service events, register multiple builders. Each builder maintains an independent routing table: ```csharp services.AddRCommon() // Internal domain events: stay in-process .WithEventHandling(inMemory => { inMemory.AddProducer(); inMemory.AddSubscriber(); inMemory.AddSubscriber(); }) // Cross-service events: leave the process via the broker .WithEventHandling(masstransit => { masstransit.UsingRabbitMq((context, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(context); }); masstransit.AddProducer(); masstransit.AddSubscriber(); }); ``` `LeaveRequestApprovedEvent` is only handled by the InMemoryEventBus producers. `PayrollSyncRequiredEvent` is only handled by MassTransit producers. Events do not cross builder boundaries unless the same event type and handler are registered with both builders. The subscription isolation example from the RCommon Examples project demonstrates this with three event types: - `InMemoryOnlyEvent` — subscribed only to `InMemoryEventBusBuilder`, ignored by MassTransit producers - `MassTransitOnlyEvent` — subscribed only to `MassTransitEventHandlingBuilder`, ignored by in-memory producers - `SharedEvent` — subscribed to both builders, handled by both producer types ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }) .WithEventHandling(eventHandling => { eventHandling.UsingInMemory((context, cfg) => { cfg.ConfigureEndpoints(context); }); eventHandling.AddProducer(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }); ``` ## Publishing Events Regardless of which provider is configured, publishing always uses the same `IEventProducer` abstraction. Inject `IEnumerable` to publish to all registered producers: ```csharp // From a background worker public class Worker : BackgroundService { private readonly IServiceProvider _serviceProvider; public Worker(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var eventProducers = _serviceProvider.GetServices(); var @event = new TestEvent(DateTime.UtcNow, Guid.NewGuid()); foreach (var producer in eventProducers) { await producer.ProduceEventAsync(@event); } } } ``` ## Outbox Pattern Considerations When an event must be published only if the database transaction succeeds, use the unit-of-work pipeline together with event publishing. RCommon's `WithUnitOfWorkToRequestPipeline()` wraps each mediator request in a transaction scope. Publish the event after the state change within the same handler so the event and the state change succeed or fail together: ```csharp .WithMediator(mediator => { mediator.AddUnitOfWorkToRequestPipeline(); // wraps handlers in a transaction }) .WithUnitOfWork(unitOfWork => { unitOfWork.SetOptions(options => { options.AutoCompleteScope = true; options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }) ``` For full outbox durability (where events survive process crashes between the write and the publish), integrate the MassTransit outbox or Wolverine's outbox support at the transport layer. ## Testing Event Handlers Event handlers are ordinary classes with injected dependencies. Testing is straightforward: ```csharp [Test] public async Task SendApprovalEmailHandler_Sends_Email_On_Approval() { var emailServiceMock = new Mock(); var handler = new SendApprovalEmailHandler(emailServiceMock.Object); await handler.HandleAsync( new LeaveRequestApprovedEvent(leaveRequestId: 42, employeeId: "emp-001"), CancellationToken.None); emailServiceMock.Verify( x => x.SendAsync(It.Is(r => r.To == "emp-001")), Times.Once); } ``` ## Choosing a Provider | Scenario | Recommended Provider | |---------|---------------------| | Single-service, in-process only | `InMemoryEventBusBuilder` | | Existing MediatR codebase | `MediatREventHandlingBuilder` | | Distributed, cross-service, high reliability | `MassTransitEventHandlingBuilder` | | High-throughput local queues, Wolverine ecosystem | `WolverineEventHandlingBuilder` | | Mixed: in-process + distributed | Multiple builders with subscription isolation | ## API Reference | Type | Package | Purpose | |------|---------|---------| | `ISyncEvent` | `RCommon.Models` | Base marker interface for all events | | `IEventProducer` | `RCommon.EventHandling` | Publishes events to subscribers | | `ISubscriber` | `RCommon.EventHandling` | Receives and handles events | | `InMemoryEventBusBuilder` | `RCommon.EventHandling` | Configures the in-process event bus | | `MediatREventHandlingBuilder` | `RCommon.MediatR` | Configures MediatR-backed event handling | | `MassTransitEventHandlingBuilder` | `RCommon.MassTransit` | Configures MassTransit-backed event handling | | `WolverineEventHandlingBuilder` | `RCommon.Wolverine` | Configures Wolverine-backed event handling | | `PublishWithEventBusEventProducer` | `RCommon.EventHandling` | In-memory producer implementation | | `PublishWithMediatREventProducer` | `RCommon.MediatR` | MediatR producer implementation | | `PublishWithMassTransitEventProducer` | `RCommon.MassTransit` | MassTransit producer implementation | | `PublishWithWolverineEventProducer` | `RCommon.Wolverine` | Wolverine producer implementation | --- ## microservices Source: https://rcommon.com/docs/architecture-guides/microservices --- title: Microservices sidebar_position: 2 description: Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions. --- # Microservices with RCommon Microservices decompose a system into independently deployable services that communicate over a network. RCommon provides the abstractions and integrations that make this communication consistent, testable, and swappable across a distributed system. ## When to Use This Approach Consider a microservices architecture with RCommon when: - Different parts of your domain have different scaling requirements - Separate teams own separate services and need clear contracts between them - You need to evolve service internals without redeploying the entire system - Integration points require durable, reliable messaging (not just HTTP calls) ## Core Patterns for Microservices RCommon supports three primary patterns that are essential in microservice environments: 1. **Messaging** — Publish events that other services subscribe to via MassTransit or Wolverine 2. **Shared abstractions** — `IGraphRepository`, `IMediatorService`, and `IEventProducer` are the same across every service; only the registered implementation changes 3. **Subscription isolation** — Each service subscribes to exactly the events it needs, routing them to the correct transport ## Setting Up a Service with Messaging Each microservice calls `AddRCommon()` independently. Inside a single service, feature-folder modules can each call `AddRCommon().With...(...)` again — the bootstrapper caches the builder and merges registrations across calls. This lets a service's internal modules wire their own subscribers, validators, and DbContexts without funneling everything through one static initializer. See [Modular Composition](../core-concepts/modular-composition.mdx) for the contract that makes this safe. The following shows how a service is configured to publish and receive messages via MassTransit: ```csharp // Program.cs — Order Service services.AddRCommon() .WithEventHandling(eventHandling => { // Configure the transport (RabbitMQ, Azure Service Bus, etc.) eventHandling.UsingRabbitMq((context, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(context); }); // This service produces order events eventHandling.AddProducer(); // This service consumes inventory events eventHandling.AddSubscriber(); }); ``` On the inventory service side: ```csharp // Program.cs — Inventory Service services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.UsingRabbitMq((context, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(context); }); eventHandling.AddProducer(); // This service handles order placed events eventHandling.AddSubscriber(); }); ``` ## Defining Events as Shared Contracts Events that cross service boundaries should be defined in a shared contracts library. Both services reference this library, never each other: ```csharp // SharedContracts/OrderPlacedEvent.cs using RCommon.Models.Events; public class OrderPlacedEvent : ISyncEvent { public OrderPlacedEvent() { } public OrderPlacedEvent(Guid orderId, string customerId, decimal total) { OrderId = orderId; CustomerId = customerId; Total = total; } public Guid OrderId { get; } public string CustomerId { get; } public decimal Total { get; } } ``` `ISyncEvent` is RCommon's base marker interface. The transport-specific serialization and routing is handled by the infrastructure layer — the event itself stays clean. ## Publishing Events Services publish events through `IEventProducer`. The call site does not know which transport is registered: ```csharp public class PlaceOrderCommandHandler : IAppRequestHandler { private readonly IGraphRepository _orderRepository; private readonly IEnumerable _eventProducers; public PlaceOrderCommandHandler( IGraphRepository orderRepository, IEnumerable eventProducers) { _orderRepository = orderRepository; _eventProducers = eventProducers; } public async Task HandleAsync( PlaceOrderCommand request, CancellationToken cancellationToken) { var order = new Order(request.CustomerId, request.Items); await _orderRepository.AddAsync(order); var @event = new OrderPlacedEvent(order.Id, order.CustomerId, order.Total); foreach (var producer in _eventProducers) { await producer.ProduceEventAsync(@event); } return new BaseCommandResponse { Success = true, Id = order.Id }; } } ``` ## Subscribing to Events Event handlers implement `ISubscriber`. The handler contains only business logic — no transport concerns: ```csharp // Inventory Service public class ReserveInventoryHandler : ISubscriber { private readonly IGraphRepository _inventoryRepository; private readonly IEnumerable _eventProducers; public ReserveInventoryHandler( IGraphRepository inventoryRepository, IEnumerable eventProducers) { _inventoryRepository = inventoryRepository; _eventProducers = eventProducers; } public async Task HandleAsync( OrderPlacedEvent notification, CancellationToken cancellationToken = default) { // Reserve inventory items var reserved = await ReserveItems(notification.OrderId); // Emit a confirmation event back var confirmEvent = new InventoryReservedEvent(notification.OrderId, reserved); foreach (var producer in _eventProducers) { await producer.ProduceEventAsync(confirmEvent); } } } ``` ## Subscription Isolation In a service with mixed internal and external event routing, RCommon allows you to register multiple event handling builders. Each builder maintains its own set of subscribers, so an event published to one transport does not bleed into another. This is useful when: - Some events stay within the process (in-memory, for domain event notifications) - Other events cross service boundaries (MassTransit or Wolverine) ```csharp services.AddRCommon() // In-process events: only handled internally .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }) // Cross-service events: routed through the message broker .WithEventHandling(eventHandling => { eventHandling.UsingRabbitMq((context, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(context); }); eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` Events subscribed only in `InMemoryEventBusBuilder` will not be published to the RabbitMQ exchange and vice versa. ## Wolverine Alternative For services using Wolverine as the message transport, the configuration pattern is identical — only the builder type changes: ```csharp // In Program.cs, before AddRCommon builder.Host.UseWolverine(options => { options.UseRabbitMq(rabbit => { rabbit.HostName = "localhost"; }).AutoProvision(); }); // Then in services services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` ## Sharing Infrastructure Abstractions Across Services Because RCommon's abstractions are transport-agnostic, a shared application services library can be referenced by multiple services without binding them to a specific message broker. Each service registers its own concrete implementations at startup. ```csharp // Shared library public interface IOrderEventPublisher { Task PublishOrderPlacedAsync(OrderPlacedEvent @event); } // Service A: MassTransit implementation public class MassTransitOrderEventPublisher : IOrderEventPublisher { private readonly IEnumerable _producers; public MassTransitOrderEventPublisher(IEnumerable producers) => _producers = producers; public async Task PublishOrderPlacedAsync(OrderPlacedEvent @event) { foreach (var producer in _producers) await producer.ProduceEventAsync(@event); } } // Service B: Wolverine implementation public class WolverineOrderEventPublisher : IOrderEventPublisher { private readonly IEnumerable _producers; public WolverineOrderEventPublisher(IEnumerable producers) => _producers = producers; public async Task PublishOrderPlacedAsync(OrderPlacedEvent @event) { foreach (var producer in _producers) await producer.ProduceEventAsync(@event); } } ``` Both implementations are structurally identical. The difference is which `IEventProducer` is injected, determined by the `WithEventHandling<>` call in `Program.cs`. ## Per-Service Persistence Each microservice owns its own database and schema. The `WithPersistence<>` builder call and `DbContext` registration are scoped to the service: ```csharp // Order Service services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext("Orders", options => { options.UseSqlServer(config.GetConnectionString("Orders")); }); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Orders"); }); // Inventory Service services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext("Inventory", options => { options.UseNpgsql(config.GetConnectionString("Inventory")); }); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Inventory"); }); ``` The Order Service uses SQL Server; the Inventory Service uses PostgreSQL. Both use the same `IGraphRepository` interface in their handlers. ## Health Considerations - Register transport-specific health checks alongside your normal ASP.NET health checks - Use `WithUnitOfWork` in services that need transactional consistency around persistence and event publishing - Consider idempotency in handlers — MassTransit and Wolverine may redeliver messages on failure ## API Reference | Type | Package | Purpose | |------|---------|---------| | `ISyncEvent` | `RCommon.Models` | Base interface for all events | | `IEventProducer` | `RCommon.EventHandling` | Produces events to a transport | | `ISubscriber` | `RCommon.EventHandling` | Handles events from a transport | | `MassTransitEventHandlingBuilder` | `RCommon.MassTransit` | Configures MassTransit as the event transport | | `WolverineEventHandlingBuilder` | `RCommon.Wolverine` | Configures Wolverine as the event transport | | `InMemoryEventBusBuilder` | `RCommon.EventHandling` | In-process event bus (no external broker) | | `PublishWithMassTransitEventProducer` | `RCommon.MassTransit` | MassTransit-backed event producer | | `PublishWithWolverineEventProducer` | `RCommon.Wolverine` | Wolverine-backed event producer | --- ## Azure Blob Storage Source: https://rcommon.com/docs/blob-storage/azure RCommon.Azure.Blobs wraps the Azure Blob Storage SDK behind IBlobStorageService, supporting connection strings, managed identity, and multiple named stores. # Azure Blob Storage ## Overview The `RCommon.Azure.Blobs` package wraps the official Azure Blob Storage SDK (`Azure.Storage.Blobs`) behind the `IBlobStorageService` interface. Your application code uses the provider-agnostic interface while the Azure implementation handles all SDK-specific details. Multiple stores can be registered under different names, which is useful when different containers should use different credentials or service URIs (for example, a development emulator alongside a production account). ## Installation This package depends on `RCommon.Blobs` and will bring it in automatically. ## Configuration Call `WithBlobStorage` inside your `AddRCommon()` block. Use `AddBlobStore` to register each named store. Provide either a connection string or a service URI combined with a `TokenCredential`. ### Using a connection string A connection string is the simplest option for development and for environments where managed identity is not available. ```csharp using RCommon; using RCommon.Azure.Blobs; builder.Services.AddRCommon() .WithBlobStorage(azure => { azure.AddBlobStore("primary", opts => { opts.ConnectionString = builder.Configuration .GetConnectionString("AzureStorage"); }); }); ``` Typical `appsettings.json`: ```json { "ConnectionStrings": { "AzureStorage": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=...;EndpointSuffix=core.windows.net" } } ``` For local development with the Azurite emulator: ```json { "ConnectionStrings": { "AzureStorage": "UseDevelopmentStorage=true" } } ``` ### Using a service URI with managed identity Managed identity avoids storing credentials in configuration. Pass the storage account endpoint URI and a `DefaultAzureCredential` (or any other `TokenCredential` from `Azure.Identity`). ```csharp using Azure.Identity; using RCommon; using RCommon.Azure.Blobs; builder.Services.AddRCommon() .WithBlobStorage(azure => { azure.AddBlobStore("primary", opts => { opts.ServiceUri = new Uri( "https://myaccount.blob.core.windows.net"); opts.Credential = new DefaultAzureCredential(); }); }); ``` ### Registering multiple stores ```csharp builder.Services.AddRCommon() .WithBlobStorage(azure => { azure.AddBlobStore("uploads", opts => opts.ConnectionString = uploadsConnectionString); azure.AddBlobStore("backups", opts => { opts.ServiceUri = new Uri( "https://backupaccount.blob.core.windows.net"); opts.Credential = new DefaultAzureCredential(); }); }); ``` ## Usage Inject `IBlobStoreFactory` and resolve the store by name, or inject `IBlobStorageService` directly when only one store is registered. :::tip Runnable example See `Examples/BlobStorage/Examples.BlobStorage.Azure/` for a complete console app exercising upload/download/delete against the [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) emulator — no real Azure Storage account required. ::: ```csharp public class FileUploadService { private readonly IBlobStorageService _storage; public FileUploadService(IBlobStoreFactory factory) { _storage = factory.Resolve("uploads"); } public async Task UploadAsync(string fileName, Stream content, CancellationToken ct = default) { // Ensure the container exists before uploading. if (!await _storage.ContainerExistsAsync("user-files", ct)) await _storage.CreateContainerAsync("user-files", ct); await _storage.UploadAsync( containerName: "user-files", blobName: fileName, content: content, options: new BlobUploadOptions { ContentType = "application/octet-stream" }, token: ct); } public async Task DownloadAsync(string fileName, CancellationToken ct = default) { return await _storage.DownloadAsync("user-files", fileName, ct); } } ``` ## API Summary ### `AzureBlobStorageBuilder` Implements `IBlobStorageBuilder` and `IAzureBlobStorageBuilder`. Registered automatically by `WithBlobStorage`. | Method | Description | |--------|-------------| | `AddBlobStore(name, configure)` | Registers a named Azure Blob Storage store. The `configure` action receives an `AzureBlobStoreOptions` instance. | ### `AzureBlobStoreOptions` | Property | Description | |----------|-------------| | `ConnectionString` | Azure Storage connection string. Use when not using managed identity. | | `ServiceUri` | URI of the Azure Blob Storage service (e.g., `https://account.blob.core.windows.net`). Required when using `Credential`. | | `Credential` | A `TokenCredential` instance from `Azure.Identity`. Required when using `ServiceUri`. | Exactly one of the following must be provided: - `ConnectionString` - `ServiceUri` and `Credential` If neither combination is supplied, the store throws an `InvalidOperationException` when first resolved. --- ## Overview Source: https://rcommon.com/docs/blob-storage/overview Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory. # Blob Storage Overview ## Overview RCommon provides a provider-agnostic abstraction for blob and object storage. The abstraction covers the full lifecycle of binary content: container management, blob upload and download, metadata, copy/move, and presigned URL generation. Your application code depends only on `IBlobStorageService` — switching from Azure Blob Storage to Amazon S3 (or a local substitute for testing) is a configuration-only change. ### Multiple named stores Applications that need more than one storage backend register each store under a unique name. The `IBlobStoreFactory` resolves the correct `IBlobStorageService` at runtime based on that name. This is useful when, for example, user uploads go to one bucket while generated reports go to another. ### Core abstractions `IBlobStorageService` is the primary interface. It groups operations into four categories: - **Container operations** — create, delete, check existence, list containers. - **Blob CRUD** — upload, download, delete, check existence, list blobs (with optional prefix filter). - **Metadata** — read detailed blob properties, set arbitrary key/value metadata. - **Transfer** — copy or move a blob between containers without downloading it locally. - **Presigned URLs** — generate time-limited download or upload URLs that can be shared with external clients. `IBlobStoreFactory` resolves a named `IBlobStorageService` that was registered during startup. `BlobUploadOptions` controls how a blob is stored during upload. `BlobItem` represents a single entry returned from listing operations. `BlobProperties` carries the full metadata of an existing blob. ## Installation Install the core abstractions package: Then install the provider package that matches your storage backend: or ## Configuration Register one or more blob stores inside `AddRCommon()`. Each call to `WithBlobStorage` registers a builder of type `T` and makes the named stores it configures available through `IBlobStoreFactory`. ```csharp using RCommon; using RCommon.Azure.Blobs; builder.Services.AddRCommon() .WithBlobStorage(azure => { azure.AddBlobStore("uploads", opts => { opts.ConnectionString = builder.Configuration .GetConnectionString("AzureStorage"); }); }); ``` Multiple providers can be registered in a single application by chaining additional `WithBlobStorage` calls: ```csharp builder.Services.AddRCommon() .WithBlobStorage(azure => { azure.AddBlobStore("hot", opts => opts.ConnectionString = azureConnectionString); }) .WithBlobStorage(s3 => { s3.AddBlobStore("archive", opts => { opts.Region = "us-east-1"; opts.AccessKeyId = awsKey; opts.SecretAccessKey = awsSecret; }); }); ``` :::tip Modular composition `WithBlobStorage` is a cache-aware sub-builder verb. When multiple modules call `WithBlobStorage` (or any single concrete builder), the cached builder is reused and each module's `AddBlobStore(name, ...)` calls accumulate on the same instance — store names from every module are visible through `IBlobStoreFactory`. Different concrete builder types (e.g. one module using `AzureBlobStorageBuilder` and another using `AmazonS3ObjectsBuilder`) coexist side by side, exactly as the chained example above shows; calling the same builder type twice with the same store name is idempotent for that name's options. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Usage ### Resolving a store Inject `IBlobStoreFactory` and call `Resolve` with the name you used at registration: ```csharp public class DocumentService { private readonly IBlobStorageService _storage; public DocumentService(IBlobStoreFactory factory) { _storage = factory.Resolve("uploads"); } } ``` ### Uploading a blob ```csharp await using var stream = File.OpenRead("/tmp/report.pdf"); await _storage.UploadAsync( containerName: "documents", blobName: "reports/q1-2025.pdf", content: stream, options: new BlobUploadOptions { ContentType = "application/pdf", Overwrite = true, Metadata = new Dictionary { ["author"] = "finance-bot" } }); ``` ### Downloading a blob ```csharp Stream content = await _storage.DownloadAsync("documents", "reports/q1-2025.pdf"); ``` ### Listing blobs ```csharp IEnumerable items = await _storage.ListBlobsAsync( containerName: "documents", prefix: "reports/"); foreach (BlobItem item in items) { Console.WriteLine($"{item.Name} {item.Size} bytes {item.LastModified}"); } ``` ### Reading and updating metadata ```csharp BlobProperties props = await _storage.GetPropertiesAsync("documents", "reports/q1-2025.pdf"); Console.WriteLine(props.ContentType); Console.WriteLine(props.ContentLength); await _storage.SetMetadataAsync( "documents", "reports/q1-2025.pdf", new Dictionary { ["reviewed"] = "true" }); ``` ### Generating a presigned download URL ```csharp Uri url = await _storage.GetPresignedDownloadUrlAsync( containerName: "documents", blobName: "reports/q1-2025.pdf", expiry: TimeSpan.FromHours(1)); // Share `url` with a client — it expires after one hour. ``` ### Copying and moving blobs ```csharp // Copy without removing the source. await _storage.CopyAsync("documents", "reports/q1-2025.pdf", "archive", "2025/q1-final.pdf"); // Move (copy then delete source). await _storage.MoveAsync("staging", "upload-001.pdf", "documents", "final-001.pdf"); ``` ## API Summary ### `IBlobStorageService` | Method | Description | |--------|-------------| | `CreateContainerAsync(name)` | Creates a new container or bucket. | | `DeleteContainerAsync(name)` | Deletes a container and all its contents. | | `ContainerExistsAsync(name)` | Returns `true` if the named container exists. | | `ListContainersAsync()` | Returns the names of all containers in the store. | | `UploadAsync(container, blob, stream, options?)` | Uploads content to a blob. | | `DownloadAsync(container, blob)` | Returns a readable stream for the blob content. | | `DeleteAsync(container, blob)` | Permanently removes a blob. | | `ExistsAsync(container, blob)` | Returns `true` if the blob exists. | | `ListBlobsAsync(container, prefix?)` | Lists blobs, optionally filtered by prefix. | | `GetPropertiesAsync(container, blob)` | Returns content type, size, ETag, and metadata. | | `SetMetadataAsync(container, blob, metadata)` | Replaces the blob's custom metadata. | | `CopyAsync(srcContainer, srcBlob, dstContainer, dstBlob)` | Copies a blob without downloading it. | | `MoveAsync(srcContainer, srcBlob, dstContainer, dstBlob)` | Copies then deletes the source blob. | | `GetPresignedDownloadUrlAsync(container, blob, expiry)` | Generates a time-limited download URL. | | `GetPresignedUploadUrlAsync(container, blob, expiry)` | Generates a time-limited upload URL. | ### `IBlobStoreFactory` | Method | Description | |--------|-------------| | `Resolve(name)` | Returns the `IBlobStorageService` registered under the given name. Throws `BlobStoreNotFoundException` if the name is not registered. | ### `BlobUploadOptions` | Property | Default | Description | |----------|---------|-------------| | `ContentType` | `null` | MIME type stored with the blob. | | `Metadata` | `null` | Key/value pairs attached to the blob. | | `Overwrite` | `true` | Whether to replace an existing blob with the same name. | ### `BlobItem` | Property | Description | |----------|-------------| | `Name` | Blob name as returned by the provider. | | `Size` | Size in bytes, if available. | | `ContentType` | MIME type, if available. | | `LastModified` | Timestamp of last modification, if available. | | `Metadata` | Custom key/value metadata attached to the blob. | ### `BlobProperties` | Property | Description | |----------|-------------| | `ContentType` | MIME type of the blob. | | `ContentLength` | Size in bytes. | | `LastModified` | Timestamp of last modification. | | `ETag` | Entity tag for optimistic concurrency. | | `Metadata` | Custom key/value metadata attached to the blob. | --- ## Amazon S3 Source: https://rcommon.com/docs/blob-storage/s3 RCommon.Amazon.S3Objects wraps the AWS SDK behind IBlobStorageService, supporting explicit credentials, named profiles, IAM instance roles, and S3-compatible stores. # Amazon S3 ## Overview The `RCommon.Amazon.S3Objects` package wraps the AWS SDK for .NET (`AWSSDK.S3`) behind the `IBlobStorageService` interface. All standard blob operations — upload, download, list, copy, move, presigned URLs — work identically to any other RCommon blob provider; only the startup configuration differs. The package also supports S3-compatible stores such as MinIO and LocalStack by accepting a custom `ServiceUrl` and enabling path-style addressing. ## Installation This package depends on `RCommon.Blobs` and will bring it in automatically. ## Configuration Call `WithBlobStorage` inside your `AddRCommon()` block. Use `AddBlobStore` to register each named store. ### Using explicit credentials ```csharp using RCommon; using RCommon.Amazon.S3Objects; builder.Services.AddRCommon() .WithBlobStorage(s3 => { s3.AddBlobStore("primary", opts => { opts.Region = "us-east-1"; opts.AccessKeyId = builder.Configuration["Aws:AccessKeyId"]; opts.SecretAccessKey = builder.Configuration["Aws:SecretAccessKey"]; }); }); ``` Store credentials in environment variables or a secrets manager rather than in `appsettings.json`. A typical pattern with environment variables: ```json { "Aws": { "AccessKeyId": "", "SecretAccessKey": "" } } ``` Set actual values via `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` environment variables or through `dotnet user-secrets` during development. ### Using a named AWS profile Named profiles in `~/.aws/credentials` or the Windows credential store are resolved via `CredentialProfileStoreChain`. ```csharp builder.Services.AddRCommon() .WithBlobStorage(s3 => { s3.AddBlobStore("primary", opts => { opts.Region = "eu-west-1"; opts.Profile = "my-app-profile"; }); }); ``` ### Using the default credential chain When neither `AccessKeyId`/`SecretAccessKey` nor `Profile` is provided, the AWS SDK falls back to its standard credential chain: environment variables, container task role, EC2 instance profile, and so on. This is the recommended approach for applications deployed to AWS infrastructure. ```csharp builder.Services.AddRCommon() .WithBlobStorage(s3 => { s3.AddBlobStore("primary", opts => { opts.Region = "us-east-1"; // No credentials — instance profile is used automatically. }); }); ``` ### Using MinIO or LocalStack (S3-compatible stores) Point the service URL at your local endpoint and enable path-style addressing, which most S3-compatible stores require. ```csharp builder.Services.AddRCommon() .WithBlobStorage(s3 => { s3.AddBlobStore("local", opts => { opts.ServiceUrl = "http://localhost:9000"; opts.ForcePathStyle = true; opts.AccessKeyId = "minioadmin"; opts.SecretAccessKey = "minioadmin"; }); }); ``` ### Registering multiple stores ```csharp builder.Services.AddRCommon() .WithBlobStorage(s3 => { s3.AddBlobStore("uploads", opts => { opts.Region = "us-east-1"; opts.Profile = "app-uploads"; }); s3.AddBlobStore("archive", opts => { opts.Region = "us-west-2"; opts.Profile = "app-archive"; }); }); ``` ## Usage Inject `IBlobStoreFactory` and resolve the store by name: :::tip Runnable example See `Examples/BlobStorage/Examples.BlobStorage.S3/` for a complete console app exercising upload/download/delete/presigned-URL generation against a local MinIO-style endpoint — no real AWS account required. ::: ```csharp public class ReportStorageService { private readonly IBlobStorageService _storage; public ReportStorageService(IBlobStoreFactory factory) { _storage = factory.Resolve("primary"); } public async Task StoreReportAsync(string key, Stream pdf, CancellationToken ct = default) { await _storage.UploadAsync( containerName: "reports", blobName: key, content: pdf, options: new BlobUploadOptions { ContentType = "application/pdf" }, token: ct); } public async Task GetDownloadLinkAsync(string key, CancellationToken ct = default) { return await _storage.GetPresignedDownloadUrlAsync( containerName: "reports", blobName: key, expiry: TimeSpan.FromMinutes(15), token: ct); } } ``` ## API Summary ### `AmazonS3ObjectsBuilder` Implements `IBlobStorageBuilder` and `IAmazonS3ObjectsBuilder`. Registered automatically by `WithBlobStorage`. | Method | Description | |--------|-------------| | `AddBlobStore(name, configure)` | Registers a named S3 store. The `configure` action receives an `AmazonS3StoreOptions` instance. | ### `AmazonS3StoreOptions` | Property | Description | |----------|-------------| | `Region` | AWS region name (e.g., `"us-east-1"`). Used to set `RegionEndpoint`. | | `AccessKeyId` | AWS access key ID. Use together with `SecretAccessKey`. | | `SecretAccessKey` | AWS secret access key. Use together with `AccessKeyId`. | | `Profile` | Named profile in the local AWS credential store. | | `ServiceUrl` | Custom endpoint URL for S3-compatible stores (MinIO, LocalStack). | | `ForcePathStyle` | When `true`, uses path-style addressing required by most S3-compatible stores. | Credential resolution priority: 1. `AccessKeyId` + `SecretAccessKey` — used when both are non-null. 2. `Profile` — used when a profile name is provided. 3. Default AWS credential chain — used when neither of the above is set. --- ## Memory Cache Source: https://rcommon.com/docs/caching/memory RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization. # Memory Cache `RCommon.MemoryCache` provides two in-process caching implementations behind the `ICacheService` abstraction: - **`InMemoryCacheService`** — backed by `Microsoft.Extensions.Caching.Memory.IMemoryCache` - **`DistributedMemoryCacheService`** — backed by `Microsoft.Extensions.Caching.Distributed.IDistributedCache` using an in-memory store Both serve single-process applications. Use `DistributedMemoryCacheService` when your service registration already relies on `IDistributedCache` and you do not yet need a real distributed store. ## Installation ## In-memory cache setup Register the in-process memory cache with `WithMemoryCaching`: ```csharp using RCommon; using RCommon.MemoryCache; builder.Services.AddRCommon() .WithMemoryCaching(cache => { // Optional: configure MemoryCacheOptions cache.Configure(options => { options.SizeLimit = 1024; options.CompactionPercentage = 0.25; }); }); ``` This registers `IMemoryCache` (via `AddMemoryCache`) and makes `InMemoryCacheService` available as `ICacheService`. ### Minimal setup If you only want the default options, omit the configuration delegate: ```csharp builder.Services.AddRCommon() .WithMemoryCaching(); ``` ## Distributed memory cache setup Use `DistributedMemoryCacheBuilder` when you want the `IDistributedCache` abstraction backed by an in-process store. This is useful during development or for services that share the `IDistributedCache` interface with a Redis implementation in production: ```csharp using RCommon; using RCommon.MemoryCache; builder.Services.AddRCommon() .WithDistributedCaching(cache => { cache.Configure(options => { options.SizeLimit = 512 * 1024 * 1024; // 512 MB }); }); ``` This registers `IDistributedCache` via `AddDistributedMemoryCache` and makes `DistributedMemoryCacheService` available as `ICacheService`. Data is serialized to JSON before storage. :::tip Modular composition `WithMemoryCaching` and `WithDistributedCaching` are cache-aware sub-builder verbs. When multiple modules call the same verb with the same builder type, the cached sub-builder is reused and each module's `Configure(...)` and `CacheDynamicallyCompiledExpressions()` calls accumulate against one instance. This lets an "infrastructure" module enable expression caching while a "feature" module separately tunes `MemoryCacheOptions`. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Expression caching RCommon uses `ICacheService` internally to cache compiled LINQ expression trees and reflection results. Call `CacheDynamicallyCompiledExpressions` to activate this optimization: ```csharp builder.Services.AddRCommon() .WithMemoryCaching(cache => { cache.CacheDynamicallyCompiledExpressions(); }); ``` This is the recommended minimum caching configuration for applications that use the persistence layer. It enables: - `ICacheService` registered as `InMemoryCacheService` - `CachingOptions` with `CachingEnabled = true` and `CacheDynamicallyCompiledExpressions = true` - A `Func` factory for strategy-based cache resolution The same method is available on `DistributedMemoryCacheBuilder`: ```csharp builder.Services.AddRCommon() .WithDistributedCaching(cache => { cache.CacheDynamicallyCompiledExpressions(); }); ``` ## Using ICacheService directly Inject `ICacheService` wherever you need caching. The get-or-create pattern eliminates the need for manual null checks: ```csharp public class ProductCatalogService { private readonly ICacheService _cache; private readonly IProductRepository _repository; public ProductCatalogService(ICacheService cache, IProductRepository repository) { _cache = cache; _repository = repository; } public async Task> GetActiveProductsAsync(CancellationToken ct) { var key = CacheKey.With(typeof(ProductCatalogService), "active-products"); return await _cache.GetOrCreateAsync(key, () => { return _repository.FindAsync(p => p.IsActive).Result; }); } } ``` ## Eviction policies Eviction is controlled through `MemoryCacheOptions` when calling `.Configure(options => ...)`: | Option | Description | |--------|-------------| | `SizeLimit` | Maximum number of cache entries (requires each entry to set a size) | | `CompactionPercentage` | Fraction of entries removed when the cache exceeds `SizeLimit` (default 0.05) | | `ExpirationScanFrequency` | How often the cache scans for expired entries (default 1 minute) | | `TrackStatistics` | Enables hit/miss statistics via `IMemoryCache.GetCurrentStatistics()` | Individual entry expiration is configured through the `IMemoryCache.GetOrCreate` callback (the underlying `ICacheEntry` object). `InMemoryCacheService` delegates directly to `IMemoryCache.GetOrCreate`, so you can wrap it to set per-entry options if required. ## API summary | Type | Package | Description | |------|---------|-------------| | `InMemoryCachingBuilder` | `RCommon.MemoryCache` | Concrete builder for `IMemoryCache`-backed caching | | `IInMemoryCachingBuilder` | `RCommon.MemoryCache` | Marker interface extending `IMemoryCachingBuilder` | | `InMemoryCacheService` | `RCommon.MemoryCache` | `ICacheService` implementation backed by `IMemoryCache` | | `DistributedMemoryCacheBuilder` | `RCommon.MemoryCache` | Concrete builder for in-process `IDistributedCache`-backed caching | | `IDistributedMemoryCachingBuilder` | `RCommon.MemoryCache` | Marker interface extending `IDistributedCachingBuilder` | | `DistributedMemoryCacheService` | `RCommon.MemoryCache` | `ICacheService` implementation backed by `IDistributedCache` with JSON serialization | | `Configure(options)` | `RCommon.MemoryCache` | Extension on `IInMemoryCachingBuilder`; configures `MemoryCacheOptions` | | `CacheDynamicallyCompiledExpressions()` | `RCommon.MemoryCache` | Extension enabling expression caching optimization | | `ICacheService` | `RCommon.Caching` | Core abstraction for get-or-create caching | | `CacheKey` | `RCommon.Caching` | Strongly-typed cache key with factory methods | | `WithMemoryCaching()` | `RCommon.Caching` | Extension method on `IRCommonBuilder` | | `WithDistributedCaching()` | `RCommon.Caching` | Extension method on `IRCommonBuilder` | --- ## Overview Source: https://rcommon.com/docs/caching/overview Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends. # Caching Overview RCommon provides a provider-agnostic caching abstraction built around a single interface: `ICacheService`. The same interface is implemented by in-process memory caching, distributed memory caching, and Redis. Application code that depends only on `ICacheService` can switch caching backends without any handler or service changes. ## ICacheService `ICacheService` exposes a get-or-create pattern. If the requested key exists in the cache the cached value is returned immediately. If the key is absent the factory delegate is invoked, the result is stored in the cache, and the value is returned. ```csharp public interface ICacheService { TData GetOrCreate(object key, Func data); Task GetOrCreateAsync(object key, Func data); } ``` All implementations — `InMemoryCacheService`, `DistributedMemoryCacheService`, and `RedisCacheService` — implement this interface. The provider is swapped by changing the builder call in startup; consuming code does not change. ## CacheKey `CacheKey` is a strongly-typed wrapper around a cache key string. It validates that the key is non-empty and does not exceed 256 characters. Use the factory methods to build composite keys: ```csharp using RCommon.Caching; // Simple key from string parts var key = CacheKey.With("orders", orderId.ToString()); // Scoped to a type var key = CacheKey.With(typeof(OrderService), "list", tenantId.ToString()); // Produces: "OrderService:list-" ``` Passing a `CacheKey` as the `key` argument to `ICacheService` methods ensures consistent key formatting across the application. ## When to use caching Use `ICacheService` when: - A query or computation is expensive and the result changes infrequently (reference data, configuration, aggregated reports). - You want to protect a downstream service or database from repeated identical requests. - You are caching dynamically compiled expressions or reflection results to improve performance (RCommon uses this internally for repository expression caching). Do not use caching when: - The data must always be fresh and stale reads are unacceptable. - The data is user-specific and not shared across requests (unless you scope the cache key per user). - The cached value is trivially cheap to compute. ## Expression caching RCommon uses caching internally to compile and cache LINQ expression trees that would otherwise be recompiled on every repository call. Enabling `CacheDynamicallyCompiledExpressions` on either the in-memory or Redis builder activates this optimization: ```csharp builder.Services.AddRCommon() .WithMemoryCaching(cache => { cache.CacheDynamicallyCompiledExpressions(); }); ``` This is the recommended minimum caching configuration for applications that use the persistence layer. ## Choosing a caching provider | Provider | Package | Backing store | Cross-process | |----------|---------|--------------|---------------| | In-memory | `RCommon.MemoryCache` | `IMemoryCache` | No | | Distributed memory | `RCommon.MemoryCache` | `IDistributedCache` (in-process) | No | | Redis | `RCommon.RedisCache` | StackExchange.Redis | Yes | Use the in-memory provider for single-process applications or development environments. Use Redis when you need a cache that is shared across multiple instances of the same service. ## Builder hierarchy ``` IRCommonBuilder .WithMemoryCaching() → IMemoryCachingBuilder .WithDistributedCaching() → IDistributedCachingBuilder ``` `IMemoryCachingBuilder` and `IDistributedCachingBuilder` are marker interfaces. Provider packages implement them with concrete builder types (`InMemoryCachingBuilder`, `DistributedMemoryCacheBuilder`, `RedisCachingBuilder`) that expose provider-specific extension methods. :::tip Modular composition Both `WithMemoryCaching` and `WithDistributedCaching` are cache-aware sub-builder verbs. When two modules call `WithMemoryCaching` (or `WithDistributedCaching`) with the same concrete builder type, the cached sub-builder is reused and each module's configuration delegate runs against the same instance — `Configure(...)` and `CacheDynamicallyCompiledExpressions()` registrations accumulate. Different concrete builder types coexist side by side (e.g. `WithMemoryCaching` and `WithDistributedCaching` register independently). See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Section contents - [Memory Cache](./memory.mdx) — In-process memory caching setup, configuration options, eviction, and expression caching - [Redis Cache](./redis.mdx) — Redis setup, connection string configuration, distributed caching, and expression caching --- ## Redis Cache Source: https://rcommon.com/docs/caching/redis RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support. # Redis Cache `RCommon.RedisCache` implements `ICacheService` using StackExchange.Redis through the `Microsoft.Extensions.Caching.StackExchangeRedis` provider. It is the recommended caching backend for applications deployed across multiple instances or hosts. ## Installation ## Configuration Register Redis caching with `WithDistributedCaching`: ```csharp using RCommon; using RCommon.RedisCache; builder.Services.AddRCommon() .WithDistributedCaching(cache => { cache.Configure(options => { options.Configuration = "localhost:6379"; options.InstanceName = "MyApp:"; }); }); ``` `Configure` calls `AddStackExchangeRedisCache` on the underlying `IServiceCollection`, registering `IDistributedCache` with the Redis provider. `RedisCacheService` is then available as `ICacheService`. :::tip Modular composition `WithDistributedCaching` is a cache-aware sub-builder verb. When multiple modules call it, the cached `RedisCachingBuilder` is reused and each module's `Configure(...)` and `CacheDynamicallyCompiledExpressions()` calls accumulate on the same instance. Configuration delegates run in the order modules invoke them, so the last `Configure(...)` wins for `RedisCacheOptions` values that are simple scalar assignments — coordinate connection-string ownership in a single module. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ### Minimal setup ```csharp builder.Services.AddRCommon() .WithDistributedCaching(cache => { cache.Configure(options => { options.Configuration = "localhost:6379"; }); }); ``` ## Connection configuration The `RedisCacheOptions` object (from `Microsoft.Extensions.Caching.StackExchangeRedis`) exposes the following commonly used settings: | Option | Description | |--------|-------------| | `Configuration` | Redis connection string, e.g. `"localhost:6379"` or a full StackExchange.Redis connection string | | `InstanceName` | Optional key prefix applied to all cache keys, e.g. `"MyApp:"` | | `ConfigurationOptions` | Full `StackExchange.Redis.ConfigurationOptions` for advanced scenarios (SSL, auth, retry policy) | For production use with authentication and TLS: ```csharp cache.Configure(options => { options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions { EndPoints = { "my-redis.cache.windows.net:6380" }, Password = "your-access-key", Ssl = true, AbortOnConnectFail = false }; options.InstanceName = "MyApp:"; }); ``` ## Expression caching Enable RCommon's internal expression caching optimization by calling `CacheDynamicallyCompiledExpressions`: ```csharp builder.Services.AddRCommon() .WithDistributedCaching(cache => { cache.Configure(options => { options.Configuration = "localhost:6379"; }); cache.CacheDynamicallyCompiledExpressions(); }); ``` This registers: - `ICacheService` as `RedisCacheService` - `CachingOptions` with `CachingEnabled = true` and `CacheDynamicallyCompiledExpressions = true` - A `Func` factory for strategy-based resolution Note: the MassTransit documentation recommends using in-memory caching for expression caching because Redis introduces network overhead. Consider using `RCommon.MemoryCache` for expression caching alongside Redis for application-level caching if performance is critical. ## Using ICacheService Inject `ICacheService` and use the get-or-create pattern. `RedisCacheService` serializes values to JSON using `IJsonSerializer` before storing them in Redis: ```csharp public class ProductCatalogService { private readonly ICacheService _cache; private readonly IProductRepository _repository; public ProductCatalogService(ICacheService cache, IProductRepository repository) { _cache = cache; _repository = repository; } public async Task> GetActiveProductsAsync(CancellationToken ct) { var key = CacheKey.With(typeof(ProductCatalogService), "active-products"); return await _cache.GetOrCreateAsync(key, () => { // This runs only on a cache miss return _repository.FindAsync(p => p.IsActive).Result; }); } } ``` ## How RedisCacheService works `RedisCacheService` implements the get-or-create pattern against `IDistributedCache`: 1. The key is converted to a string via `.ToString()`. 2. `GetStringAsync(key)` is called. If the result is non-null, the JSON string is deserialized and returned. 3. On a cache miss, the factory delegate is invoked, the result is serialized to JSON via `IJsonSerializer`, stored with `SetStringAsync`, and returned. There is no built-in expiration set at the `ICacheService` level; all entries are stored without a TTL by default. For expiration control, wrap `IDistributedCache` directly and set `DistributedCacheEntryOptions`. ## JSON serialization dependency `RedisCacheService` depends on `IJsonSerializer` from `RCommon.Core`. Ensure a JSON serialization provider is registered before configuring Redis caching: ```csharp builder.Services.AddRCommon() .WithJsonSerialization() // or NewtonsoftJsonSerializer .WithDistributedCaching(cache => { cache.Configure(options => { options.Configuration = "localhost:6379"; }); }); ``` ## API summary | Type | Package | Description | |------|---------|-------------| | `RedisCachingBuilder` | `RCommon.RedisCache` | Concrete builder for Redis-backed distributed caching | | `IRedisCachingBuilder` | `RCommon.RedisCache` | Marker interface extending `IDistributedCachingBuilder` | | `RedisCacheService` | `RCommon.RedisCache` | `ICacheService` implementation backed by `IDistributedCache` (Redis) with JSON serialization | | `Configure(options)` | `RCommon.RedisCache` | Extension on `IRedisCachingBuilder`; configures `RedisCacheOptions` via `AddStackExchangeRedisCache` | | `CacheDynamicallyCompiledExpressions()` | `RCommon.RedisCache` | Extension enabling expression caching optimization using `RedisCacheService` | | `ICacheService` | `RCommon.Caching` | Core abstraction for get-or-create caching | | `CacheKey` | `RCommon.Caching` | Strongly-typed cache key with factory methods | | `WithDistributedCaching()` | `RCommon.Caching` | Extension method on `IRCommonBuilder` | --- ## Execution Results & Models Source: https://rcommon.com/docs/core-concepts/execution-results Return success or failure using RCommon's ExecutionResult pattern, and paginate data with PagedResult and PaginatedListModel — structured results without exceptions. # Execution Results & Models ## Overview The `RCommon.Models` package provides a set of shared model types used throughout the framework: execution results for conveying operation outcomes, paginated list models for returning paged data, and marker interfaces that create consistent type constraints across commands, queries, and events. Using these shared types instead of raw booleans or thrown exceptions gives your application a uniform, serializable contract between layers. ## Installation ## Execution Results ### Overview The `IExecutionResult` pattern replaces raw exception-based control flow for predictable failures (validation errors, business rule violations, etc.). Instead of throwing, a method returns an `IExecutionResult` that carries a success flag and, on failure, a list of error messages. `ExecutionResult` is an abstract record with static factory methods that return cached singleton instances for the common success and generic failure cases, avoiding unnecessary allocations. ### Basic usage ```csharp using RCommon.Models.ExecutionResults; // Returning success public IExecutionResult ActivateAccount(Account account) { if (account.IsAlreadyActive) return ExecutionResult.Failed("Account is already active."); account.Activate(); return ExecutionResult.Success(); } // Returning failure with multiple errors public IExecutionResult ValidateOrder(Order order) { var errors = new List(); if (order.Items.Count == 0) errors.Add("Order must contain at least one item."); if (order.CustomerId == Guid.Empty) errors.Add("Order must be assigned to a customer."); return errors.Count > 0 ? ExecutionResult.Failed(errors) : ExecutionResult.Success(); } ``` ### Consuming a result ```csharp var result = ActivateAccount(account); if (!result.IsSuccess) { // Cast to FailedExecutionResult to access error messages var failed = result as FailedExecutionResult; foreach (var error in failed?.Errors ?? []) { logger.LogWarning("Activation failed: {Error}", error); } return; } // proceed with successful path ``` ### Wrapping a result in a CommandResult When a command handler needs to return an execution result to the caller, wrap it in `CommandResult`: ```csharp using RCommon.Models.Commands; using RCommon.Models.ExecutionResults; public class ActivateAccountHandler { public ICommandResult Handle(ActivateAccountCommand command) { var result = ActivateAccount(command.AccountId); return new CommandResult(result); } } ``` --- ## Pagination Models ### Overview RCommon provides two complementary pagination abstractions: - `PagedResult` / `IPagedResult` — a lightweight, immutable result container. Construct it directly from a list of items and total count metadata. - `PaginatedListModel` / `PaginatedListModel` — abstract base records that apply pagination and optional projection to an `IQueryable` source. Derive from these when building response DTOs. ### `PagedResult` — simple paged container Use `PagedResult` when you already have a materialized page of items and just need to attach pagination metadata: ```csharp using RCommon.Models; public async Task> GetOrdersAsync(int pageNumber, int pageSize) { var query = _dbContext.Orders.Where(o => o.CustomerId == _currentUserId); var totalCount = await query.CountAsync(); var items = await query .OrderByDescending(o => o.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .Select(o => new OrderSummary(o.Id, o.CreatedAt, o.Total)) .ToListAsync(); return new PagedResult(items, totalCount, pageNumber, pageSize); } ``` `PagedResult` computes `TotalPages`, `HasNextPage`, and `HasPreviousPage` automatically. ### `PaginatedListRequest` — standardized request input Derive from `PaginatedListRequest` to build your own request types with a consistent paging interface: ```csharp using RCommon.Models; public record GetOrdersRequest : PaginatedListRequest { public string? CustomerName { get; init; } } ``` Defaults: page 1, page size 20, sort by "id", sort direction `None`. ### `PaginatedListModel` — queryable-based DTO Derive from `PaginatedListModel` when your response DTO should be built directly from an `IQueryable` source: ```csharp using RCommon.Models; public record OrderListModel : PaginatedListModel { public OrderListModel(IQueryable source, PaginatedListRequest request) : base(source, request) { } } // Usage in a query handler: var query = _dbContext.Orders.AsQueryable(); var model = new OrderListModel(query, request); // model.Items, model.TotalCount, model.TotalPages, model.HasNextPage, etc. are all set ``` ### `PaginatedListModel` — queryable with projection Use the two-type-parameter variant when you need to project source entities into a different output type. Implement the abstract `CastItems` method: ```csharp using RCommon.Models; public record OrderListModel : PaginatedListModel { public OrderListModel(IQueryable source, PaginatedListRequest request) : base(source, request) { } protected override IQueryable CastItems(IQueryable source) { return source.Select(o => new OrderSummaryDto { Id = o.Id, CreatedAt = o.CreatedAt, Total = o.Total }); } } ``` --- ## API Summary ### Execution result types | Type | Description | |---|---| | `IExecutionResult` | Contract: `bool IsSuccess`. Base for all execution results. | | `ExecutionResult` | Abstract record. Factory methods: `Success()`, `Failed()`, `Failed(IEnumerable)`, `Failed(params string[])`. | | `SuccessExecutionResult` | Concrete record; `IsSuccess = true`. | | `FailedExecutionResult` | Concrete record; `IsSuccess = false`; exposes `IReadOnlyCollection Errors`. | | `ICommandResult` | Wraps an `IExecutionResult` as a command handler return type. | | `CommandResult` | Default implementation of `ICommandResult`. Serialization-ready (`[DataContract]`). | ### Pagination types | Type | Description | |---|---| | `IPagedResult` | Interface: `Items`, `TotalCount`, `PageNumber`, `PageSize`, `TotalPages`, `HasNextPage`, `HasPreviousPage`. | | `PagedResult` | Concrete implementation of `IPagedResult`. Constructor: `(items, totalCount, pageNumber, pageSize)`. | | `IPaginatedListRequest` | Interface for paging/sorting parameters: `PageNumber`, `PageSize`, `SortBy`, `SortDirection`. | | `PaginatedListRequest` | Abstract base record. Defaults: page 1, page size 20, sort by "id", no direction. | | `PaginatedListModel` | Abstract base record that paginates an `IQueryable` in-constructor. | | `PaginatedListModel` | Same as above, plus abstract `CastItems` for projecting to a different output type. | | `SortDirectionEnum` | `Ascending = 1`, `Descending = 2`, `None = 3`. | ### Marker interface | Type | Description | |---|---| | `IModel` | Empty marker interface. Implemented by all model types to enable consistent generic constraints across the framework. | --- ## Fluent Configuration Source: https://rcommon.com/docs/core-concepts/fluent-configuration Deep dive into RCommon's fluent configuration API — IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection. # Fluent Configuration Builder ## Overview RCommon uses a fluent builder pattern to configure framework services during application startup. The entry point is the `AddRCommon()` extension method on `IServiceCollection`, which returns an `IRCommonBuilder` instance. You then chain `With*` methods on that builder to activate features such as GUID generation, system time, and common factories. This approach keeps all RCommon configuration in one place, makes dependencies explicit, and prevents double-registration of the same service. ## Installation ## Configuration Call `AddRCommon()` inside `Program.cs` (or `Startup.ConfigureServices` for older host models) and chain the features you need: ```csharp using RCommon; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRCommon() .WithSequentialGuidGenerator(options => { options.DefaultSequentialGuidType = SequentialGuidType.SequentialAtEnd; }) .WithDateTimeSystem(options => { options.Kind = DateTimeKind.Utc; }); ``` `AddRCommon()` registers core infrastructure immediately (caching options, event bus, event router). Each subsequent `With*` call registers the corresponding service into `IServiceCollection` and returns the same builder so calls can be chained. ### What AddRCommon registers automatically Calling `AddRCommon()` with no additional configuration registers: - `CachingOptions` (caching disabled by default) - `EventSubscriptionManager` (singleton) - `IEventBus` backed by `InMemoryEventBus` (singleton) - `IEventRouter` backed by `InMemoryTransactionalEventRouter` (scoped) ## Usage ### Basic setup — minimal configuration ```csharp builder.Services.AddRCommon(); ``` This is sufficient when you only need the built-in event infrastructure and do not require GUID generation or system time abstractions. ### Full configuration chain ```csharp builder.Services.AddRCommon() .WithSimpleGuidGenerator() .WithDateTimeSystem(options => options.Kind = DateTimeKind.Utc) .WithCommonFactory(); ``` ### Creating a custom builder extension If you are writing an RCommon add-on package you can extend `IRCommonBuilder` with your own `With*` method. Follow the same pattern used by the built-in methods: ```csharp using Microsoft.Extensions.DependencyInjection; using RCommon; public static class MyFeatureBuilderExtensions { public static IRCommonBuilder WithMyFeature( this IRCommonBuilder builder, Action configure) { builder.Services.Configure(configure); builder.Services.AddTransient(); return builder; } } ``` Consumers then call it like any other built-in method: ```csharp builder.Services.AddRCommon() .WithMyFeature(options => { options.Timeout = TimeSpan.FromSeconds(30); }); ``` ## API Summary ### `IServiceCollection` extension | Method | Description | |---|---| | `AddRCommon()` | Creates an `RCommonBuilder`, registers core services, and returns `IRCommonBuilder`. | ### `IRCommonBuilder` interface | Member | Description | |---|---| | `Services` | The underlying `IServiceCollection`. Useful inside extension methods to register additional services. | | `WithSequentialGuidGenerator(Action)` | Registers `SequentialGuidGenerator` as `IGuidGenerator`. | | `WithSimpleGuidGenerator()` | Registers `SimpleGuidGenerator` as `IGuidGenerator`. | | `WithDateTimeSystem(Action)` | Registers `SystemTime` as `ISystemTime`. | | `WithCommonFactory()` | Registers a service/implementation pair together with an `ICommonFactory`. | | `Configure()` | Finalizes configuration and returns the `IServiceCollection`. Called internally by `AddRCommon()`. | ### Behavior on repeated calls Calling the same `With*` method twice with the same `T` is now idempotent — the sub-builder is cached on the underlying `IServiceCollection` and any configuration action delegates accumulate against the cached instance. Singleton-style verbs (e.g., `WithSimpleGuidGenerator`, `WithDateTimeSystem`, `WithJsonSerialization`) are idempotent on same-type re-registration but throw `RCommonBuilderException` on different-type conflicts. See [Modular Composition](./modular-composition.mdx) for the full conflict matrix and modular usage patterns. --- ## Guards & Validation Source: https://rcommon.com/docs/core-concepts/guards Use RCommon Guard clauses for defensive programming — null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET. # Guards & Validation ## Overview RCommon ships a `Guard` utility class that implements the guard clause pattern. Guard clauses validate inputs at the top of a method and throw a descriptive exception immediately when a precondition is violated, rather than allowing invalid state to propagate further into the call stack. All methods are static, so no instantiation or dependency injection is required. You call them directly at the start of any method that needs to validate its arguments. ## Installation No additional configuration is required. The `Guard` class is part of the `RCommon` namespace and is available as soon as the package is referenced. ## Usage ### Null checks ```csharp using RCommon; public class OrderService { private readonly IOrderRepository _repository; public OrderService(IOrderRepository repository) { Guard.IsNotNull(repository, nameof(repository)); _repository = repository; } public async Task SubmitOrder(Order order) { Guard.IsNotNull(order, nameof(order)); await _repository.SaveAsync(order); } } ``` `Guard.IsNotNull` throws `ArgumentNullException` when the argument is `null`. ### String checks ```csharp public void SetCustomerName(string name) { Guard.IsNotEmpty(name, nameof(name)); // null, empty, or whitespace Guard.IsNotOutOfLength(name, 100, nameof(name)); // max length _name = name; } public void SetEmail(string email) { Guard.IsNotInvalidEmail(email, nameof(email)); _email = email; } public void SetWebsite(string url) { Guard.IsNotInvalidWebUrl(url, nameof(url)); _website = url; } ``` ### GUID checks ```csharp public async Task GetOrder(Guid orderId) { Guard.IsNotEmpty(orderId, nameof(orderId)); return await _repository.GetByIdAsync(orderId); } ``` `Guard.IsNotEmpty(Guid, string)` throws `ArgumentException` when the value is `Guid.Empty`. An overload that returns a `bool` instead of throwing is also available when you prefer a conditional check: ```csharp if (!Guard.IsNotEmpty(orderId, nameof(orderId), throwException: false)) { // handle the case gracefully return null; } ``` ### Numeric checks ```csharp public void SetPageSize(int pageSize) { Guard.IsNotNegative(pageSize, nameof(pageSize)); // < 0 throws Guard.IsNotNegativeOrZero(pageSize, nameof(pageSize)); // <= 0 throws } public void SetDiscountRate(decimal rate) { Guard.IsNotNegative(rate, nameof(rate)); Guard.IsNotOutOfRange((int)rate, 0, 100, nameof(rate)); // range check (int only) } ``` Numeric overloads exist for `int`, `long`, `float`, `decimal`, and `TimeSpan`. ### Date checks ```csharp public void SetDeliveryDate(DateTime deliveryDate) { Guard.IsNotInvalidDate(deliveryDate, nameof(deliveryDate)); } ``` ### Collection checks ```csharp public void ProcessItems(ICollection items) { Guard.IsNotEmpty(items, nameof(items)); // null or Count == 0 throws foreach (var item in items) { /* ... */ } } ``` ### Arbitrary condition checks Use `Guard.Against` when you need to assert a custom condition and throw a specific exception type: ```csharp Guard.Against( _isLocked, "Cannot modify a locked order."); // Lambda overload — evaluated lazily Guard.Against( () => order.Status == OrderStatus.Cancelled, "Cannot add items to a cancelled order."); ``` ### Type and interface checks ```csharp Guard.TypeOf(provider, "Provider must implement IPaymentProvider."); Guard.Implements(providerType, "Type does not implement IPaymentProvider."); Guard.InheritsFrom(entity, "Entity must inherit from BaseEntity."); ``` ### Equality checks ```csharp Guard.IsEqual(expectedVersion, actualVersion, "Version mismatch — optimistic concurrency violation."); ``` ## API Summary | Method | Throws | Condition | |---|---|---| | `IsNotNull(object, string)` | `ArgumentNullException` | Argument is `null` | | `IsNotEmpty(string, string)` | `ArgumentException` | String is null, empty, or whitespace | | `IsNotEmpty(string, string, bool)` | `ArgumentException` or `false` | String is null, empty, or whitespace | | `IsNotOutOfLength(string, int, string)` | `ArgumentException` | String length exceeds maximum | | `IsNotInvalidEmail(string, string)` | `ArgumentException` | String is not a valid email address | | `IsNotInvalidWebUrl(string, string)` | `ArgumentException` | String is not a valid web URL | | `IsNotEmpty(Guid, string)` | `ArgumentException` | GUID is `Guid.Empty` | | `IsNotEmpty(Guid, string, bool)` | `ArgumentException` or `false` | GUID is `Guid.Empty` | | `IsNotEmpty(ICollection, string)` | `ArgumentException` | Collection is null or empty | | `IsNotNegative(int/long/float/decimal/TimeSpan, string)` | `ArgumentOutOfRangeException` | Value is less than zero | | `IsNotNegativeOrZero(int/long/float/decimal/TimeSpan, string)` | `ArgumentOutOfRangeException` | Value is zero or less | | `IsNotOutOfRange(int, int, int, string)` | `ArgumentOutOfRangeException` | Value is outside [min, max] | | `IsNotInvalidDate(DateTime, string)` | `ArgumentOutOfRangeException` | Date is not valid | | `Against(bool, string)` | `TException` | Assertion is `true` | | `Against(Func, string)` | `TException` | Lambda returns `true` | | `TypeOf(object, string)` | `InvalidOperationException` | Object is not of expected type | | `Implements(object/Type, string)` | `InvalidOperationException` | Type does not implement interface | | `InheritsFrom(object/Type, string)` | `InvalidOperationException` | Type does not inherit from base | | `IsEqual(object, object, string)` | `TException` | Objects are not equal | --- ## GUID Generation Source: https://rcommon.com/docs/core-concepts/guid-generation RCommon's IGuidGenerator abstraction for testable GUID creation — sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation. # GUID Generation ## Overview RCommon abstracts GUID generation behind an `IGuidGenerator` interface for two reasons: 1. **Testability.** Code that calls `Guid.NewGuid()` directly cannot be controlled in unit tests. Injecting `IGuidGenerator` lets tests substitute a predictable implementation. 2. **Database performance.** Standard random GUIDs cause index fragmentation in clustered indexes because their values are not monotonically increasing. Sequential GUIDs embed a timestamp in the byte layout so new rows are always appended at the end of the index. RCommon ships two implementations out of the box: `SimpleGuidGenerator` and `SequentialGuidGenerator`. You pick one during application configuration; see [Behavior on repeated calls](#behavior-on-repeated-calls) below for what happens when more than one module configures GUID generation. ## Installation ## Configuration Configure GUID generation inside the `AddRCommon()` builder chain. ### Simple (random) GUIDs Uses `Guid.NewGuid()` internally. Suitable for non-database use cases or when the underlying database handles GUID ordering itself (e.g., `NEWSEQUENTIALID()` in SQL Server). ```csharp builder.Services.AddRCommon() .WithSimpleGuidGenerator(); ``` ### Sequential GUIDs Generates GUIDs with an embedded timestamp so that successive calls produce values that sort in the correct order for a given database platform. The byte layout varies by platform; choose the appropriate `SequentialGuidType` for your database: | `SequentialGuidType` | Database | |---|---| | `SequentialAsString` | MySQL, PostgreSQL | | `SequentialAsBinary` | Oracle | | `SequentialAtEnd` | SQL Server (default) | ```csharp builder.Services.AddRCommon() .WithSequentialGuidGenerator(options => { options.DefaultSequentialGuidType = SequentialGuidType.SequentialAtEnd; }); ``` When `DefaultSequentialGuidType` is left `null`, the generator defaults to `SequentialAtEnd`. ### Behavior on repeated calls GUID generator verbs (`WithSimpleGuidGenerator`, `WithSequentialGuidGenerator`) now follow singleton-style modular composition: calling the same generator twice is an idempotent no-op, while calling a different generator after one is already configured throws `RCommonBuilderException`. The exception message names both impl types and offers a remediation hint. See [Modular Composition](./modular-composition.mdx) for the full conflict matrix. ## Usage Inject `IGuidGenerator` wherever you need to create a new identifier: ```csharp using RCommon; public class OrderService { private readonly IGuidGenerator _guidGenerator; public OrderService(IGuidGenerator guidGenerator) { _guidGenerator = guidGenerator; } public Order CreateOrder(CustomerId customerId) { var orderId = _guidGenerator.Create(); return new Order(orderId, customerId); } } ``` ### Generating a specific sequential GUID type at runtime `SequentialGuidGenerator` exposes an overload that accepts a `SequentialGuidType` directly, which is useful when a single application works with multiple databases: ```csharp // Only available when you have a reference to SequentialGuidGenerator directly var generator = serviceProvider.GetRequiredService() as SequentialGuidGenerator; var guid = generator?.Create(SequentialGuidType.SequentialAsString); ``` For most scenarios, rely on the default configured via `WithSequentialGuidGenerator` and inject `IGuidGenerator` — the concrete type is an implementation detail. ### In unit tests Substitute `IGuidGenerator` with a test double that returns a known value: ```csharp public class FakeGuidGenerator : IGuidGenerator { private readonly Guid _value; public FakeGuidGenerator(Guid value) { _value = value; } public Guid Create() => _value; } // In a test var knownId = Guid.Parse("00000000-0000-0000-0000-000000000001"); var service = new OrderService(new FakeGuidGenerator(knownId)); var order = service.CreateOrder(customerId); Assert.Equal(knownId, order.Id); ``` ## API Summary ### `IGuidGenerator` | Member | Description | |---|---| | `Guid Create()` | Creates and returns a new `Guid`. | ### `SimpleGuidGenerator` Registered by `WithSimpleGuidGenerator()`. Calls `Guid.NewGuid()` on every invocation. Scoped lifetime. ### `SequentialGuidGenerator` Registered by `WithSequentialGuidGenerator(...)`. Transient lifetime. Generates a GUID composed of 10 bytes of cryptographically random data and a 6-byte millisecond-resolution timestamp. Byte layout is controlled by `SequentialGuidType`. ### `SequentialGuidGeneratorOptions` | Property | Type | Default | Description | |---|---|---|---| | `DefaultSequentialGuidType` | `SequentialGuidType?` | `null` (resolves to `SequentialAtEnd`) | The byte layout strategy used when `Create()` is called without an explicit type. | ### `SequentialGuidType` enum | Value | Description | |---|---| | `SequentialAsString` | Timestamp at start; sequential when formatted as a string. Use with MySQL, PostgreSQL. | | `SequentialAsBinary` | Timestamp at start; sequential when compared as a byte array. Use with Oracle. | | `SequentialAtEnd` | Timestamp at end of the Data4 block. Use with SQL Server. | --- ## modular-composition Source: https://rcommon.com/docs/core-concepts/modular-composition --- title: Modular Composition sidebar_position: 2 description: Compose AddRCommon across multiple modules in a single process — registrations merge, never duplicate or throw on agreement. --- # Modular Composition ## Overview RCommon's bootstrapper supports modular composition: any number of modules in the same process can independently call `services.AddRCommon().WithX(...)` and the registrations merge predictably. Sub-builders are cached by concrete type, singleton-style verbs are idempotent on same-type re-registration, and conflicts (different impls competing for the same slot) throw with a diagnostic message that names both types. This page documents the contract. For the underlying builder API itself, see [Fluent Configuration](./fluent-configuration.mdx). ## The problem Before modular composition, `AddRCommon()` assumed a single composition root. Applications that organized startup around feature-folders or per-module wiring could not safely invoke `AddRCommon()` from more than one place: - Calling `AddRCommon()` twice double-registered `IEventBus`, `EventSubscriptionManager`, and `IEventRouter`. - Internal guard flags on the builder reset on the second call, masking subsequent conflicts. - `DataStoreFactoryOptions` accepted duplicate `(name, base)` mappings and silently overwrote earlier ones. - Singleton-style verbs such as `WithSimpleGuidGenerator` and `WithDateTimeSystem` threw on every second call — even when the second call agreed with the first. The net effect: modular apps had to funnel all RCommon wiring through a single static method, which defeated the purpose of modular composition. ## The solution `AddRCommon()` is now idempotent. The first call constructs an `IRCommonBuilder` and stashes it on `IServiceCollection`; subsequent calls return the same cached instance. Sub-builders are cached by concrete builder type, so a second `WithPersistence(ef => ...)` reuses the same `EFCorePersistenceBuilder` and the new action delegate runs against it. Singleton-style verbs record the impl type they registered; same-type re-registration is a no-op, different-type calls throw `RCommonBuilderException` with both type names in the message. Conflict semantics are predictable across the entire surface — see the matrix below. ## A two-module example Define a minimal module contract and let each module configure RCommon independently: ```csharp public interface IServiceModule { void Configure(IServiceCollection services); } public sealed class OrderingModule : IServiceModule { public void Configure(IServiceCollection services) { services.AddRCommon() .WithSimpleGuidGenerator() .WithPersistence(ef => ef.AddDbContext( "Ordering", o => o.UseInMemoryDatabase("ordering"))) .WithEventHandling(eh => eh.AddProducer()); } } public sealed class InventoryModule : IServiceModule { public void Configure(IServiceCollection services) { services.AddRCommon() .WithSimpleGuidGenerator() // Same impl as Ordering — idempotent no-op. .WithPersistence(ef => ef.AddDbContext( "Inventory", o => o.UseInMemoryDatabase("inventory"))) .WithMemoryCaching(); } } ``` Compose them inside `Host.CreateDefaultBuilder`: ```csharp var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { IServiceModule[] modules = { new OrderingModule(), new InventoryModule(), }; foreach (var module in modules) { module.Configure(services); } }) .Build(); ``` What happens at runtime: - `AddRCommon()` runs twice but returns the same builder on the second call. - `WithSimpleGuidGenerator()` runs twice; the second call detects the impl is already registered and is a no-op. - `WithPersistence` runs twice; the same persistence sub-builder is reused and `AddDbContext` accumulates both `"Ordering"` and `"Inventory"` data stores. - Only the modules that opt in to event handling and caching pay the cost. A runnable three-module version of this example — including a producer registered twice and deduplicated — lives under `Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/`. ## Conflict semantics The table below documents every verb category and what happens when it is called more than once. | Verb category | Same impl re-registered | Different impl | |---|---|---| | `AddRCommon()` | Returns the cached `IRCommonBuilder`. | n/a | | Sub-builder verbs (`WithPersistence`, `WithMediator`, `WithEventHandling`, `WithCQRS`, `WithValidation`, `WithBlobStorage`, `WithMultiTenancy`, `WithUnitOfWork`, `WithMemoryCaching`, `WithDistributedCaching`) | Cached sub-builder is reused; the supplied `Action` runs against the same instance, so registrations accumulate. | Both sub-builders register side by side (e.g. `WithPersistence` + `WithPersistence`). | | Singleton-style verbs (`WithSimpleGuidGenerator`, `WithSequentialGuidGenerator`, `WithDateTimeSystem`, `WithJsonSerialization`, `WithSmtpEmailServices`, `WithSendGridEmailServices`) | Idempotent no-op. | Throws `RCommonBuilderException` with both impl type names and a remediation hint. | | `AddDbContext(name, ...)` | Idempotent for the same `(name, TDbContext)` pair. | Throws `UnsupportedDataStoreException` when the same `name` is re-bound to a different `TDbContext`. | | `AddProducer` | Same `T` is registered exactly once via descriptor scan. | Distinct `T` types coexist. | | Parameterless verbs (`WithStatelessStateMachine`, `WithMassTransitStateMachine`, `WithClaimsAndPrincipalAccessor`) | `TryAdd`-hardened — idempotent. | n/a | The "Same impl" column is what makes modular composition usable: modules can independently declare their requirements without coordinating with other modules. ## New helper APIs ### `IServiceCollection.IsRCommonInitialized()` Returns `true` if any module in the process has already called `AddRCommon()`. Useful when a module's behavior depends on whether RCommon is in play: ```csharp if (!services.IsRCommonInitialized()) { // First module to wire RCommon — set up shared infrastructure once. } services.AddRCommon().WithSimpleGuidGenerator(); ``` You rarely need to call this guard in normal usage because `AddRCommon()` is idempotent. It is most valuable in libraries that want to skip optional wiring entirely when RCommon is absent. ### `IRCommonBuilder.GetOrAddBuilder(Func)` The hook that third-party `WithX` extensions use to opt into sub-builder caching. The factory is invoked at most once per `TSubBuilder` per process: ```csharp public static IRCommonBuilder WithMyFeature( this IRCommonBuilder builder, Action configure) where TBuilder : class, IMyFeatureBuilder, new() { var sub = builder.GetOrAddBuilder(() => new TBuilder { Services = builder.Services }); configure(sub); return builder; } ``` When two modules call `WithMyFeature(...)`, only the first call constructs `MyBuilder`. The second call retrieves the cached instance and runs its `configure` delegate against it. ### `IRCommonBuilder.GetBootstrapDiagnostics()` After the host starts, an internal `IHostedService` scans the registered descriptors for soft duplicates and stashes a human-readable report on the builder. Retrieve it like this: ```csharp var host = builder.Build(); await host.StartAsync(); var rcommon = host.Services.GetRequiredService(); var report = rcommon.GetBootstrapDiagnostics(); if (!string.IsNullOrEmpty(report)) { Console.WriteLine("Bootstrap diagnostics:"); Console.WriteLine(report); } ``` When the descriptor scan finds nothing suspicious, `GetBootstrapDiagnostics()` returns `string.Empty`. The same report is also emitted as a single warning via `ILoggerFactory` during host startup. ## Diagnostics at startup The duplicate-descriptor scanner runs once when the host starts. It is registered automatically by `AddRCommon()` as a hosted service and: 1. Walks `IServiceCollection` after all modules have configured services. 2. Groups descriptors by `(ServiceType, ImplementationType, Lifetime)`. 3. Emits a single warning through `ILoggerFactory` when duplicates are present. 4. Stashes the same report on the builder so application code can read it via `GetBootstrapDiagnostics()`. "Soft duplicates" are descriptors that share `ServiceType` + `ImplementationType` but were not deduped automatically — typically a sign that a custom extension is bypassing the cache. Hardened built-in verbs do not produce soft duplicates. ## What's NOT thread-safe Bootstrap is single-threaded by contract. The cached builder and its sub-builders are not protected by locks. Modules must run their `Configure(IServiceCollection)` calls serially, exactly as `Host.CreateDefaultBuilder(args).ConfigureServices(...)` does. Once `host.Build()` returns, the service provider is fully constructed and standard DI thread-safety applies. If you parallelize module wiring you will see intermittent missing registrations and lost configuration actions. Don't. ## See also - `Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/` — a runnable three-module demonstration including producer deduplication and the diagnostic report. - [Fluent Configuration](./fluent-configuration.mdx) — the underlying builder API. - [GUID Generation](./guid-generation.mdx) — singleton-style verb that participates in the conflict matrix. - [System Time](./system-time.mdx) — same. --- ## System Time Abstraction Source: https://rcommon.com/docs/core-concepts/system-time Use ISystemTime to abstract DateTime.Now for testable time-dependent code — configure UTC or local time, normalize incoming dates, and substitute clocks in tests. # System Time Abstraction ## Overview Code that calls `DateTime.Now` or `DateTime.UtcNow` directly is difficult to test because the value changes with wall-clock time. RCommon provides an `ISystemTime` abstraction that wraps the system clock. Injecting `ISystemTime` instead of reading the clock directly lets you substitute a fixed time in unit tests without modifying production code. Beyond testability, `ISystemTime` also centralizes time zone handling. When `DateTimeKind.Utc` is configured, `ISystemTime.Now` always returns UTC, and the `Normalize` method converts any incoming `DateTime` to match that kind. ## Installation ## Configuration Configure the system time inside the `AddRCommon()` builder chain. ### UTC time (recommended for multi-timezone applications) ```csharp builder.Services.AddRCommon() .WithDateTimeSystem(options => { options.Kind = DateTimeKind.Utc; }); ``` ### Local server time ```csharp builder.Services.AddRCommon() .WithDateTimeSystem(options => { options.Kind = DateTimeKind.Local; }); ``` ### Unspecified (default) When `Kind` is `DateTimeKind.Unspecified`, `ISystemTime.Now` returns `DateTime.Now` and no normalization conversions are applied. ```csharp builder.Services.AddRCommon() .WithDateTimeSystem(options => { options.Kind = DateTimeKind.Unspecified; }); ``` `WithDateTimeSystem` is now idempotent — calling it multiple times is safe and any options actions you supply accumulate via the Options pattern. This lets multiple modules independently configure the date/time system without coordination. See [Modular Composition](./modular-composition.mdx) for the full conflict matrix across all `With*` verbs. ## Usage ### Injecting ISystemTime in production code ```csharp using RCommon; public class AuditService { private readonly ISystemTime _systemTime; private readonly IAuditRepository _repository; public AuditService(ISystemTime systemTime, IAuditRepository repository) { _systemTime = systemTime; _repository = repository; } public async Task RecordEvent(string description) { var entry = new AuditEntry { OccurredAt = _systemTime.Now, Description = description }; await _repository.AddAsync(entry); } } ``` ### Normalizing incoming DateTime values Use `Normalize` when accepting a `DateTime` from an external source (e.g., an API request) and you need it to conform to the application's configured `DateTimeKind`: ```csharp public void SetScheduledDate(DateTime scheduledDate) { // Converts from UTC to Local, or Local to UTC, depending on configured Kind. var normalized = _systemTime.Normalize(scheduledDate); _scheduledDate = normalized; } ``` `Normalize` rules: - If the configured `Kind` is `Unspecified`, the input is returned unchanged. - If the configured `Kind` matches the input's `Kind`, no conversion occurs. - `Local` + UTC input: converts to local time via `ToLocalTime()`. - `Utc` + Local input: converts to UTC via `ToUniversalTime()`. - `Unspecified` input: re-specifies the `Kind` without converting the value. ### In unit tests Substitute `ISystemTime` with a test double that returns a known timestamp: ```csharp public class FakeSystemTime : ISystemTime { private readonly DateTime _fixedTime; public FakeSystemTime(DateTime fixedTime) { _fixedTime = fixedTime; } public DateTime Now => _fixedTime; public DateTimeKind Kind => _fixedTime.Kind; public bool SupportsMultipleTimezone => _fixedTime.Kind == DateTimeKind.Utc; public DateTime Normalize(DateTime dateTime) => dateTime; } // In a test var fixedTime = new DateTime(2026, 1, 15, 9, 0, 0, DateTimeKind.Utc); var fakeTime = new FakeSystemTime(fixedTime); var service = new AuditService(fakeTime, repositoryMock.Object); await service.RecordEvent("order placed"); repositoryMock.Verify(r => r.AddAsync(It.Is(e => e.OccurredAt == fixedTime))); ``` ## API Summary ### `ISystemTime` | Member | Description | |---|---| | `DateTime Now { get; }` | Returns the current date and time according to the configured `Kind`. | | `DateTimeKind Kind { get; }` | The `DateTimeKind` this instance was configured with. | | `bool SupportsMultipleTimezone { get; }` | `true` when `Kind` is `Utc`, indicating the time is safe to store across time zones. | | `DateTime Normalize(DateTime dateTime)` | Converts the provided `DateTime` to match the configured `Kind`. | ### `SystemTime` (default implementation) Registered as `ISystemTime` by `WithDateTimeSystem(...)`. Transient lifetime. `Now` returns `DateTime.UtcNow` when `Kind` is `Utc`, otherwise `DateTime.Now`. ### `SystemTimeOptions` | Property | Type | Default | Description | |---|---|---|---| | `Kind` | `DateTimeKind` | `DateTimeKind.Unspecified` | Controls whether the system clock operates in UTC, Local, or Unspecified mode. | --- ## Command & Query Bus Source: https://rcommon.com/docs/cqrs-mediator/command-query-bus Learn how RCommon's ICommandBus and IQueryBus implement CQRS, resolve handlers from DI, support validation, and cache compiled dispatch delegates. # Command & Query Bus ## Overview RCommon implements the Command Query Responsibility Segregation (CQRS) pattern through two dedicated bus abstractions: `ICommandBus` and `IQueryBus`. These buses decouple the code that initiates an operation from the code that handles it. **Commands** represent intent to change state. A command is dispatched through `ICommandBus`, which resolves exactly one handler and returns an `IExecutionResult`. If no handler is registered or more than one is found, the bus throws an exception — enforcing the CQRS contract that a command has a single, authoritative handler. **Queries** represent requests for data without side effects. A query is dispatched through `IQueryBus`, which resolves exactly one handler and returns the typed result. Both buses resolve handlers from the .NET dependency injection container. They use dynamically compiled delegates to invoke `HandleAsync` on the resolved handler, avoiding per-call reflection overhead. These compiled delegates can optionally be cached by enabling expression caching in RCommon's caching configuration. Both buses also support optional pre-dispatch validation. When validation is enabled through `CqrsValidationOptions`, the bus calls `IValidationService.ValidateAsync` before passing the message to its handler. ## Installation ## Configuration Wire up the CQRS buses inside `AddRCommon()` using `WithCQRS`. The builder's configuration delegate is where you register your command and query handlers. ```csharp using RCommon; using RCommon.ApplicationServices; builder.Services.AddRCommon() .WithCQRS(cqrs => { // Register handlers individually (explicit, verbose) cqrs.AddCommandHandler(); cqrs.AddQueryHandler(); // Or scan an assembly for all handlers (concise) cqrs.AddCommandHandlers(typeof(Program).Assembly); cqrs.AddQueryHandlers(typeof(Program).Assembly); }); ``` ### Enabling validation Combine `WithCQRS` with `WithValidation` to run validators before dispatch: ```csharp using RCommon; using RCommon.ApplicationServices; using RCommon.FluentValidation; builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddCommandHandlers(typeof(Program).Assembly); cqrs.AddQueryHandlers(typeof(Program).Assembly); }) .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining(); validation.UseWithCqrs(options => { options.ValidateCommands = true; options.ValidateQueries = true; }); }); ``` ### Enabling expression caching Enable caching to avoid recompiling handler invocation delegates on every dispatch: ```csharp using RCommon.MemoryCache; builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddCommandHandlers(typeof(Program).Assembly); }) .WithMemoryCaching(cache => { cache.CacheDynamicallyCompiledExpressions(); }); ``` ### Enabling transactional command dispatch By default, `ICommandBus.DispatchCommandAsync` does not open a transaction — a multi-step command handler (e.g., create an order and reserve inventory) must manage its own `IUnitOfWork` explicitly. Call `AddUnitOfWorkToCommandBus()` to wrap every command dispatch in an `IUnitOfWork`, committed automatically after a successful dispatch: ```csharp builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddCommandHandlers(typeof(Program).Assembly); cqrs.AddUnitOfWorkToCommandBus(); }) .WithUnitOfWork(uow => { /* ... */ }); ``` This is opt-in and scoped to commands only — `IQueryBus` is never wrapped, since queries are read-only by CQRS convention. If the handler throws, the unit of work is disposed without committing, so the transaction rolls back. :::tip RCommon.Mediatr has its own, separate opt-in If you use MediatR instead of (or alongside) the native `ICommandBus`, the equivalent feature is `AddUnitOfWorkToRequestPipeline()` on `IMediatRBuilder` — see [MediatR](./mediatr#adding-pipeline-behaviors). The two are independent: enabling one does not enable the other, and each dispatcher gets the idiom that fits its shape (a bus-level decorator here; an `IPipelineBehavior<,>` there). ::: ## Usage Inject `ICommandBus` or `IQueryBus` into any service and call the dispatch methods: ```csharp using RCommon.ApplicationServices.Commands; using RCommon.ApplicationServices.Queries; using RCommon.Models.ExecutionResults; public class OrderApplicationService { private readonly ICommandBus _commandBus; private readonly IQueryBus _queryBus; public OrderApplicationService(ICommandBus commandBus, IQueryBus queryBus) { _commandBus = commandBus; _queryBus = queryBus; } public async Task PlaceOrder(PlaceOrderCommand command) { return await _commandBus.DispatchCommandAsync(command, CancellationToken.None); } public async Task GetOrder(GetOrderQuery query) { return await _queryBus.DispatchQueryAsync(query, CancellationToken.None); } } ``` ## API Summary ### `ICommandBus` | Member | Description | |---|---| | `DispatchCommandAsync(ICommand, CancellationToken)` | Resolves the single registered `ICommandHandler` and invokes it. Throws `NoCommandHandlersException` when no handler is found; throws `InvalidOperationException` when more than one is found. | ### `IQueryBus` | Member | Description | |---|---| | `DispatchQueryAsync(IQuery, CancellationToken)` | Resolves the single registered `IQueryHandler` and invokes it. | ### `ICqrsBuilder` extension methods | Method | Description | |---|---| | `AddCommandHandler()` | Registers a single command handler as a transient service. | | `AddCommand()` | Alias for `AddCommandHandler` with command-first type parameter order. | | `AddCommandHandlers(Assembly, Predicate?)` | Scans an assembly and registers all `ICommandHandler` implementations. Excludes decorator types. | | `AddCommandHandlers(params Type[])` | Registers the provided command handler types explicitly. | | `AddCommandHandlers(IEnumerable)` | Registers the provided command handler types explicitly. | | `AddQueryHandler()` | Registers a single query handler as a transient service. | | `AddQuery()` | Alias for `AddQueryHandler` with query-first type parameter order. | | `AddQueryHandlers(Assembly, Predicate?)` | Scans an assembly and registers all `IQueryHandler` implementations. Excludes decorator types. | | `AddQueryHandlers(params Type[])` | Registers the provided query handler types explicitly. | | `AddQueryHandlers(IEnumerable)` | Registers the provided query handler types explicitly. | | `AddUnitOfWorkToCommandBus()` | Opt-in. Wraps `ICommandBus.DispatchCommandAsync` in an `IUnitOfWork`, committed after a successful dispatch. Commands only; `IQueryBus` is untouched. | ### `CqrsValidationOptions` | Property | Default | Description | |---|---|---| | `ValidateCommands` | `false` | When `true`, the `CommandBus` calls `IValidationService.ValidateAsync` before dispatching. | | `ValidateQueries` | `false` | When `true`, the `QueryBus` calls `IValidationService.ValidateAsync` before dispatching. | --- ## Commands & Handlers Source: https://rcommon.com/docs/cqrs-mediator/commands-handlers Define ICommand classes, implement ICommandHandler, register handlers individually or by assembly scan, and add FluentValidation before dispatch. # Commands & Handlers ## Overview A **command** in RCommon is a message that expresses intent to change state. It carries the data needed to perform the operation and returns an `IExecutionResult` so the caller knows whether the operation succeeded. A **command handler** is a class that performs the state change. The `ICommandBus` resolves exactly one handler per command type and invokes it. Registering zero or more than one handler for the same command type is a configuration error. RCommon provides two command handler interfaces: - `ICommandHandler` — handles a command and returns a typed `IExecutionResult`. - `ICommandHandler` — handles a command with no return value (fire-and-forget style, resolved internally but not used with `ICommandBus.DispatchCommandAsync`). The `ICommand` and `ICommand` marker interfaces live in `RCommon.Models` and create the generic constraints that connect a command to its result type. ## Installation ## Defining a command Implement `ICommand` on your command class. The `TResult` type parameter must implement `IExecutionResult`. ```csharp using RCommon.Models.Commands; using RCommon.Models.ExecutionResults; public class CreateOrderCommand : ICommand { public CreateOrderCommand(Guid customerId, IReadOnlyList lines) { CustomerId = customerId; Lines = lines; } public Guid CustomerId { get; } public IReadOnlyList Lines { get; } } ``` The command is a plain data-carrying object. It should not reference any services or infrastructure — only the data required to perform the operation. ## Creating a handler Implement `ICommandHandler`. The type parameter order is result-first, then command. ```csharp using RCommon.ApplicationServices.Commands; using RCommon.Models.ExecutionResults; public class CreateOrderHandler : ICommandHandler { private readonly IOrderRepository _orders; public CreateOrderHandler(IOrderRepository orders) { _orders = orders; } public async Task HandleAsync( CreateOrderCommand command, CancellationToken cancellationToken) { var order = Order.Create(command.CustomerId, command.Lines); await _orders.AddAsync(order, cancellationToken); return new SuccessExecutionResult(); } } ``` The handler receives dependencies through constructor injection. It performs the state change and returns an execution result. ## Registration Register handlers during startup using the `ICqrsBuilder` extension methods inside `WithCQRS`. ### Individual registration ```csharp using RCommon; using RCommon.ApplicationServices; builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddCommandHandler(); }); ``` ### Assembly scan Scanning an assembly is the recommended approach for applications with many handlers. All non-abstract types implementing `ICommandHandler` are registered automatically. Types whose constructors accept a command handler (i.e., decorator types) are excluded. ```csharp using System.Reflection; builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddCommandHandlers(typeof(Program).Assembly); }); ``` Apply a predicate to restrict which handlers are included: ```csharp cqrs.AddCommandHandlers( typeof(Program).Assembly, t => t.Namespace?.StartsWith("MyApp.Orders") == true); ``` ## Adding validation When `ValidateCommands` is enabled, the `CommandBus` runs `IValidationService.ValidateAsync` before calling the handler. Define a FluentValidation validator for the command class: ```csharp using FluentValidation; public class CreateOrderCommandValidator : AbstractValidator { public CreateOrderCommandValidator() { RuleFor(c => c.CustomerId).NotEmpty(); RuleFor(c => c.Lines).NotEmpty().WithMessage("Order must contain at least one line."); } } ``` Register the validator and enable command validation: ```csharp using RCommon.FluentValidation; builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddCommandHandlers(typeof(Program).Assembly); }) .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining(); validation.UseWithCqrs(options => { options.ValidateCommands = true; }); }); ``` ## Dispatching a command Inject `ICommandBus` and call `DispatchCommandAsync`: ```csharp using RCommon.ApplicationServices.Commands; using RCommon.Models.ExecutionResults; public class OrderService { private readonly ICommandBus _commandBus; public OrderService(ICommandBus commandBus) { _commandBus = commandBus; } public async Task PlaceOrder( Guid customerId, IReadOnlyList lines, CancellationToken cancellationToken = default) { var command = new CreateOrderCommand(customerId, lines); return await _commandBus.DispatchCommandAsync(command, cancellationToken); } } ``` Inspect the result: ```csharp var result = await _commandBus.DispatchCommandAsync(command); if (!result.IsSuccess) { var failed = result as FailedExecutionResult; foreach (var error in failed?.Errors ?? []) logger.LogWarning("Command failed: {Error}", error); } ``` ## API Summary ### `ICommand` | Member | Description | |---|---| | (marker interface) | Implement on command classes. `TResult` must implement `IExecutionResult`. Consumed by `ICommandBus.DispatchCommandAsync`. | ### `ICommandHandler` | Member | Description | |---|---| | `HandleAsync(TCommand, CancellationToken)` | Executes the command and returns a `Task`. | ### `ICommandHandler` | Member | Description | |---|---| | `HandleAsync(TCommand, CancellationToken)` | Executes the command with no return value. Returns `Task`. | ### `ICqrsBuilder` command registration methods | Method | Description | |---|---| | `AddCommandHandler()` | Registers one handler for one command type. | | `AddCommand()` | Alias with command-first parameter order. | | `AddCommandHandlers(Assembly, Predicate?)` | Assembly scan registration. | | `AddCommandHandlers(IEnumerable)` | Explicit bulk registration. | --- ## MediatR Source: https://rcommon.com/docs/cqrs-mediator/mediatr Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors. # MediatR ## Overview `RCommon.MediatR` adapts the [MediatR](https://github.com/jbogard/MediatR) library to RCommon's mediator abstraction. It provides: - `MediatRAdapter` — an `IMediatorAdapter` implementation that delegates `Send` and `Publish` calls to MediatR's `IMediator`. - `MediatRBuilder` — an `IMediatRBuilder` that wires MediatR into the RCommon service container. - Pre-built pipeline behaviors for logging, validation, and unit-of-work. - Wrapper types (`MediatRRequest`, `MediatRNotification`) that bridge RCommon message contracts to MediatR's internal types. Application code depends only on `IMediatorService` from `RCommon.Mediator`. The MediatR implementation is an infrastructure detail registered at startup. ## Installation This package depends on `RCommon.Mediator`, `RCommon.ApplicationServices`, `RCommon.Core`, `RCommon.Persistence`, and the `MediatR` NuGet package. All are pulled in transitively. ## Configuration Register the MediatR provider inside `AddRCommon()` using `WithMediator`. The configuration delegate is where you register your requests, notifications, and pipeline behaviors. ```csharp using RCommon; using RCommon.Mediator.MediatR; builder.Services.AddRCommon() .WithMediator(mediator => { // Register a fire-and-forget request (no response) mediator.AddRequest(); // Register a request that returns a response mediator.AddRequest(); // Register a notification (fan-out to all subscribers) mediator.AddNotification(); }); ``` :::tip Modular composition `WithMediator` is cache-aware. When multiple modules call it, the cached `MediatRBuilder` is reused and each module's configuration delegate runs against the same instance — `AddRequest`, `AddNotification`, `AddLoggingToRequestPipeline`, `AddUnitOfWorkToRequestPipeline`, and `Configure(...)` registrations from every module accumulate on one builder. This lets each module register its own command/query handlers without coordinating with the others. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ### Advanced MediatR configuration Access the underlying `MediatRServiceConfiguration` to register handlers from additional assemblies or to set MediatR-specific options: ```csharp using System.Reflection; builder.Services.AddRCommon() .WithMediator(mediator => { mediator.AddRequest(); mediator.Configure(config => { config.RegisterServicesFromAssemblies(typeof(Program).Assembly); }); }); ``` ### Adding pipeline behaviors Pipeline behaviors run in registration order before and after every handler. RCommon ships three built-in behaviors: ```csharp builder.Services.AddRCommon() .WithMediator(mediator => { mediator.AddRequest(); // Adds LoggingRequestBehavior and LoggingRequestWithResponseBehavior mediator.AddLoggingToRequestPipeline(); // Adds ValidatorBehavior and ValidatorBehaviorForMediatR (requires IValidationService) mediator.AddValidationToRequestPipeline(); // Adds UnitOfWorkRequestBehavior and UnitOfWorkRequestWithResponseBehavior mediator.AddUnitOfWorkToRequestPipeline(); }); ``` :::tip Native ICommandBus has its own, separate opt-in `AddUnitOfWorkToRequestPipeline()` only wraps MediatR requests dispatched through `IMediator`. It has no effect on RCommon's native, non-MediatR `ICommandBus` — that dispatcher has its own opt-in, `AddUnitOfWorkToCommandBus()`, described in [Command & Query Bus](./command-query-bus#enabling-transactional-command-dispatch). If your application uses both dispatchers, enable each one independently; neither implies the other. ::: ## Usage ### Defining requests A fire-and-forget request implements `IAppRequest`: ```csharp using RCommon.Mediator.Subscribers; public class PlaceOrderRequest : IAppRequest { public PlaceOrderRequest(Guid customerId, IReadOnlyList lines) { CustomerId = customerId; Lines = lines; } public Guid CustomerId { get; } public IReadOnlyList Lines { get; } } ``` A request that returns a response implements `IAppRequest`: ```csharp using RCommon.Mediator.Subscribers; public class GetOrderRequest : IAppRequest { public GetOrderRequest(Guid orderId) { OrderId = orderId; } public Guid OrderId { get; } } ``` ### Defining request handlers Handle a fire-and-forget request by implementing `IAppRequestHandler`: ```csharp using RCommon.Mediator.Subscribers; public class PlaceOrderRequestHandler : IAppRequestHandler { private readonly IOrderRepository _orders; public PlaceOrderRequestHandler(IOrderRepository orders) { _orders = orders; } public async Task HandleAsync(PlaceOrderRequest request, CancellationToken cancellationToken = default) { var order = Order.Create(request.CustomerId, request.Lines); await _orders.AddAsync(order, cancellationToken); } } ``` Handle a request that returns a response by implementing `IAppRequestHandler`: ```csharp using RCommon.Mediator.Subscribers; public class GetOrderRequestHandler : IAppRequestHandler { private readonly IOrderRepository _orders; public GetOrderRequestHandler(IOrderRepository orders) { _orders = orders; } public async Task HandleAsync(GetOrderRequest request, CancellationToken cancellationToken = default) { var order = await _orders.GetByIdAsync(request.OrderId, cancellationToken); return new OrderDto { Id = order.Id, CustomerId = order.CustomerId }; } } ``` ### Defining notifications Notifications are broadcast to all registered subscribers. Implement `IAppNotification` on the notification class: ```csharp using RCommon.Mediator.Subscribers; public class OrderPlacedNotification : IAppNotification { public OrderPlacedNotification(Guid orderId, DateTime placedAt) { OrderId = orderId; PlacedAt = placedAt; } public Guid OrderId { get; } public DateTime PlacedAt { get; } } ``` Subscribe by implementing `ISubscriber` from `RCommon.EventHandling.Subscribers`: ```csharp using RCommon.EventHandling.Subscribers; public class OrderPlacedNotificationHandler : ISubscriber { public async Task HandleAsync(OrderPlacedNotification notification, CancellationToken cancellationToken = default) { // React to the notification — send email, update read model, etc. await Task.CompletedTask; } } ``` ### Sending and publishing Inject `IMediatorService` and use `Send` or `Publish`: ```csharp using RCommon.Mediator; public class OrderController : ControllerBase { private readonly IMediatorService _mediator; public OrderController(IMediatorService mediator) { _mediator = mediator; } [HttpPost] public async Task PlaceOrder(PlaceOrderRequest request) { // Fire-and-forget: dispatches to a single handler await _mediator.Send(request); return Accepted(); } [HttpGet("{id}")] public async Task GetOrder(Guid id) { // Request-response: dispatches to a single handler and returns a result return await _mediator.Send(new GetOrderRequest(id)); } [HttpPost("{id}/events")] public async Task PublishOrderPlaced(Guid id) { // Notification: delivers to all registered subscribers await _mediator.Publish(new OrderPlacedNotification(id, DateTime.UtcNow)); return NoContent(); } } ``` ## API Summary ### `IMediatorService` | Method | Description | |---|---| | `Send(TRequest, CancellationToken)` | Dispatches a fire-and-forget request to a single handler. Wraps the request in `MediatRRequest` internally. | | `Send(TRequest, CancellationToken)` | Dispatches a request to a single handler and returns its response. | | `Publish(TNotification, CancellationToken)` | Broadcasts a notification to all registered `ISubscriber` implementations. | ### `IMediatRBuilder` registration methods | Method | Description | |---|---| | `AddRequest()` | Registers a fire-and-forget request and its handler. | | `AddRequest()` | Registers a request-response pair and its handler. | | `AddNotification()` | Registers a notification and its subscriber. | | `AddLoggingToRequestPipeline()` | Registers `LoggingRequestBehavior` and `LoggingRequestWithResponseBehavior`. | | `AddValidationToRequestPipeline()` | Registers `ValidatorBehavior`, `ValidatorBehaviorForMediatR`, and `ValidationService`. | | `AddUnitOfWorkToRequestPipeline()` | Registers `UnitOfWorkRequestBehavior` and `UnitOfWorkRequestWithResponseBehavior`. | | `Configure(Action)` | Passes a configuration delegate directly to MediatR's `AddMediatR`. | | `Configure(MediatRServiceConfiguration)` | Passes a pre-built configuration instance to MediatR's `AddMediatR`. | ### Built-in pipeline behaviors | Behavior | Applies to | Description | |---|---|---| | `LoggingRequestBehavior` | `IRequest` | Logs request name and payload before and after handler execution. | | `LoggingRequestWithResponseBehavior` | `IRequest` | Same as above for requests that return a response. | | `ValidatorBehavior` | `IAppRequest` | Validates `IAppRequest` instances using `IValidationService`. Throws on failure. | | `ValidatorBehaviorForMediatR` | `IRequest` | Validates raw MediatR `IRequest` instances using `IValidationService`. | | `UnitOfWorkRequestBehavior` | `IRequest` | Wraps handler execution in a `TransactionMode.Default` unit of work. Commits on success. | | `UnitOfWorkRequestWithResponseBehavior` | `IRequest` | Same as above for requests that return a response. | ### Marker interfaces | Interface | Package | Description | |---|---|---| | `IAppRequest` | `RCommon.Mediator` | Marker for fire-and-forget requests dispatched via `Send`. | | `IAppRequest` | `RCommon.Mediator` | Marker for request-response requests dispatched via `Send`. | | `IAppNotification` | `RCommon.Mediator` | Marker for notifications dispatched via `Publish`. | | `IAppRequestHandler` | `RCommon.Mediator` | Handler for fire-and-forget requests. | | `IAppRequestHandler` | `RCommon.Mediator` | Handler for request-response requests. | --- ## Queries & Handlers Source: https://rcommon.com/docs/cqrs-mediator/queries-handlers Define read-only IQuery classes, implement IQueryHandler, register via assembly scan or explicit registration, and optionally validate queries before dispatch. # Queries & Handlers ## Overview A **query** in RCommon is a message that requests data without modifying state. It carries the parameters needed to fetch or compute results and returns a strongly-typed response. A **query handler** performs the data retrieval. The `IQueryBus` resolves exactly one handler per query type and invokes it. Queries are read-only by design — a handler should never cause side effects. The `IQuery` marker interface lives in `RCommon.Models` and binds a query class to its result type. `IQueryHandler` defines the handler contract. Registration follows the same fluent pattern used for commands. ## Installation ## Defining a query Implement `IQuery` on your query class. `TResult` can be any type — a DTO, a collection, a paginated result, or a primitive. ```csharp using RCommon.Models.Queries; public class GetOrderByIdQuery : IQuery { public GetOrderByIdQuery(Guid orderId) { OrderId = orderId; } public Guid OrderId { get; } } ``` For paginated results, derive the result type from `PagedResult` (from `RCommon.Models`): ```csharp using RCommon.Models.Queries; public class GetOrdersQuery : IQuery> { public GetOrdersQuery(int pageNumber, int pageSize) { PageNumber = pageNumber; PageSize = pageSize; } public int PageNumber { get; } public int PageSize { get; } } ``` ## Creating a handler Implement `IQueryHandler`. The type parameters match those declared on the query: query type first, result type second. ```csharp using RCommon.ApplicationServices.Queries; public class GetOrderByIdHandler : IQueryHandler { private readonly IOrderRepository _orders; public GetOrderByIdHandler(IOrderRepository orders) { _orders = orders; } public async Task HandleAsync( GetOrderByIdQuery query, CancellationToken cancellationToken) { var order = await _orders.GetByIdAsync(query.OrderId, cancellationToken); return new OrderDto { Id = order.Id, CustomerId = order.CustomerId, Status = order.Status, Lines = order.Lines.Select(l => new OrderLineDto(l.ProductId, l.Quantity, l.UnitPrice)).ToList() }; } } ``` Handlers receive dependencies through constructor injection. The handler should only read data, never write it. ## Registration Register handlers during startup inside `WithCQRS`. ### Individual registration ```csharp using RCommon; using RCommon.ApplicationServices; builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddQueryHandler(); }); ``` ### Assembly scan Scanning an assembly is the recommended approach for applications with many handlers. All non-abstract types implementing `IQueryHandler` are registered automatically. Decorator types (those accepting a query handler in their constructor) are excluded from the scan. ```csharp builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddQueryHandlers(typeof(Program).Assembly); }); ``` Apply a predicate to limit which namespaces or types are included: ```csharp cqrs.AddQueryHandlers( typeof(Program).Assembly, t => t.Namespace?.StartsWith("MyApp.Ordering") == true); ``` ## Adding validation When `ValidateQueries` is enabled, the `QueryBus` runs `IValidationService.ValidateAsync` before dispatching. Define a FluentValidation validator for the query class: ```csharp using FluentValidation; public class GetOrderByIdQueryValidator : AbstractValidator { public GetOrderByIdQueryValidator() { RuleFor(q => q.OrderId).NotEmpty(); } } ``` Register the validator and enable query validation: ```csharp using RCommon.FluentValidation; builder.Services.AddRCommon() .WithCQRS(cqrs => { cqrs.AddQueryHandlers(typeof(Program).Assembly); }) .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining(); validation.UseWithCqrs(options => { options.ValidateQueries = true; }); }); ``` ## Dispatching a query Inject `IQueryBus` and call `DispatchQueryAsync`: ```csharp using RCommon.ApplicationServices.Queries; public class OrderApplicationService { private readonly IQueryBus _queryBus; public OrderApplicationService(IQueryBus queryBus) { _queryBus = queryBus; } public async Task GetOrder(Guid orderId, CancellationToken cancellationToken = default) { var query = new GetOrderByIdQuery(orderId); return await _queryBus.DispatchQueryAsync(query, cancellationToken); } } ``` ## API Summary ### `IQuery` | Member | Description | |---|---| | (marker interface) | Implement on query classes. `TResult` is the return type produced by the handler. Consumed by `IQueryBus.DispatchQueryAsync`. | ### `IQueryHandler` | Member | Description | |---|---| | `HandleAsync(TQuery, CancellationToken)` | Handles the query and returns a `Task`. Should not cause side effects. | ### `ICqrsBuilder` query registration methods | Method | Description | |---|---| | `AddQueryHandler()` | Registers one handler for one query type as a transient service. | | `AddQuery()` | Alias with query-first parameter order. | | `AddQueryHandlers(Assembly, Predicate?)` | Assembly scan registration. Excludes decorator types. | | `AddQueryHandlers(IEnumerable)` | Explicit bulk registration. | --- ## Wolverine Source: https://rcommon.com/docs/cqrs-mediator/wolverine Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code. # Wolverine ## Overview `RCommon.Wolverine` integrates [WolverineFx](https://wolverinefx.net) as a distributed event handler for the RCommon event handling pipeline. It is focused on event production and subscription rather than the request-response mediator pattern. The package provides: - `WolverineEventHandlingBuilder` / `IWolverineEventHandlingBuilder` — registers Wolverine-based subscribers through the RCommon fluent builder. - `PublishWithWolverineEventProducer` — publishes events to all Wolverine handlers using `IMessageBus.PublishAsync` (fan-out). - `SendWithWolverineEventProducer` — sends events to a single Wolverine handler using `IMessageBus.SendAsync` (point-to-point). - `WolverineEventHandler` — bridges Wolverine's message dispatch to RCommon's `ISubscriber` abstraction. Events must implement `ISerializableEvent` (from `RCommon.Models`) to flow through the Wolverine producers. ## Installation This package depends on `RCommon.Core` and `WolverineFx`. Wolverine itself requires host-level setup via `UseWolverine()` on the application host builder — see the Wolverine documentation for transport and endpoint configuration. ## Configuration Wolverine is configured at the host level and within the RCommon builder. Register the Wolverine host extension on the application host builder, then wire up RCommon subscribers. ```csharp using RCommon; using RCommon.Wolverine; using Wolverine; var builder = WebApplication.CreateBuilder(args); // Configure Wolverine at the host level builder.Host.UseWolverine(opts => { // Configure transports, endpoints, and discovery here opts.Discovery.IncludeAssembly(typeof(Program).Assembly); }); // Configure RCommon with Wolverine event handling builder.Services.AddRCommon() .WithEventHandling(wolverine => { wolverine.AddProducer(); wolverine.AddSubscriber(); wolverine.AddSubscriber(); }); ``` :::warning Unlike InMemoryEventBusBuilder, AddSubscriber here does not auto-register a producer `InMemoryEventBusBuilder.AddSubscriber` automatically registers its matching producer (as of this version), but `WolverineEventHandlingBuilder.AddSubscriber` does not -- a subscriber registered here with no matching `AddProducer()` call is never invoked, with no error of any kind (the router simply has zero producers to route to). Always pair `AddSubscriber` with an `AddProducer()` call for one of the two producers below, as shown above. RCommon's startup diagnostics will warn once if it detects this builder has subscriptions but no registered producer. ::: :::tip Modular composition `WithEventHandling` is a cache-aware sub-builder verb. When multiple modules call it, the cached `WolverineEventHandlingBuilder` is reused and each module's `AddSubscriber` and `AddProducer` registrations accumulate on the same instance. `AddProducer` deduplicates by concrete producer type, so the same producer registered from two modules yields exactly one descriptor. Note that the host-level `builder.Host.UseWolverine(...)` call must happen exactly once in the application host; only the RCommon sub-builder participates in module-level composition. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Usage ### Defining an event Events dispatched through Wolverine must implement `ISerializableEvent`: ```csharp using RCommon.Models.Events; public class OrderPlacedEvent : ISerializableEvent { public OrderPlacedEvent(Guid orderId, Guid customerId, DateTime placedAt) { OrderId = orderId; CustomerId = customerId; PlacedAt = placedAt; } public Guid OrderId { get; } public Guid CustomerId { get; } public DateTime PlacedAt { get; } } ``` ### Creating a subscriber Implement `ISubscriber` from `RCommon.EventHandling.Subscribers`. This is the same interface used by MediatR subscribers — the Wolverine adapter bridges message dispatch to it automatically through `WolverineEventHandler`. ```csharp using RCommon.EventHandling.Subscribers; public class OrderPlacedEventHandler : ISubscriber { private readonly IEmailService _emailService; public OrderPlacedEventHandler(IEmailService emailService) { _emailService = emailService; } public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken = default) { await _emailService.SendOrderConfirmationAsync(@event.CustomerId, @event.OrderId, cancellationToken); } } ``` ### Registration Register subscribers using the `IWolverineEventHandlingBuilder` extension method. Each call records the event-to-producer subscription in the `EventSubscriptionManager` so the router only delivers the event to the producers registered for it. ```csharp builder.Services.AddRCommon() .WithEventHandling(wolverine => { wolverine.AddProducer(); wolverine.AddSubscriber(); }); ``` If you need to construct the subscriber with a factory delegate: ```csharp wolverine.AddSubscriber( sp => new OrderPlacedEventHandler(sp.GetRequiredService())); ``` ### Producing events Use `PublishWithWolverineEventProducer` for fan-out delivery (all subscribers receive the event) or `SendWithWolverineEventProducer` for point-to-point delivery (one handler). Register the appropriate producer with the builder's own `AddProducer()` -- not a raw `services.AddScoped()` call -- so the registration is tracked by `EventSubscriptionManager` and correctly associated with this builder: ```csharp using RCommon.Wolverine.Producers; builder.Services.AddRCommon() .WithEventHandling(wolverine => { // Fan-out: all subscribed handlers receive the event wolverine.AddProducer(); // Point-to-point: only one handler receives the event // wolverine.AddProducer(); }); ``` Produce events through RCommon's `IEventBus` or by raising domain events that flow through the event router — the producers are invoked by the routing infrastructure automatically. ## API Summary ### `IWolverineEventHandlingBuilder` extension methods | Method | Description | |---|---| | `AddSubscriber()` | Registers `TEventHandler` as a scoped `ISubscriber` and records the subscription in the `EventSubscriptionManager`. | | `AddSubscriber(Func)` | Same as above, but uses a factory delegate to construct the subscriber. | ### `PublishWithWolverineEventProducer` | Member | Description | |---|---| | `ProduceEventAsync(T, CancellationToken)` | Calls `IMessageBus.PublishAsync` to deliver the event to all Wolverine handlers subscribed to `T`. Skips delivery if the event type is not subscribed to this producer. | ### `SendWithWolverineEventProducer` | Member | Description | |---|---| | `ProduceEventAsync(T, CancellationToken)` | Calls `IMessageBus.SendAsync` to deliver the event to a single Wolverine handler. Skips delivery if the event type is not subscribed to this producer. | ### `WolverineEventHandler` | Member | Description | |---|---| | `HandleAsync(TEvent, CancellationToken)` | Called by Wolverine's message dispatch infrastructure. Delegates to the injected `ISubscriber`. | ### Key interfaces and base types | Type | Package | Description | |---|---|---| | `IWolverineEventHandlingBuilder` | `RCommon.Wolverine` | Builder interface for configuring Wolverine event handling. Extends `IEventHandlingBuilder`. | | `WolverineEventHandlingBuilder` | `RCommon.Wolverine` | Default implementation. Constructed by `WithEventHandling`. | | `ISerializableEvent` | `RCommon.Models` | Marker interface required on all events dispatched through Wolverine producers. | | `ISubscriber` | `RCommon.EventHandling` | Application-level handler interface. Implement this for your event handling logic. | | `IWolverineEventHandler` | `RCommon.Wolverine` | Wolverine-facing handler interface implemented by `WolverineEventHandler`. | --- ## Auditing Source: https://rcommon.com/docs/domain-driven-design/auditing Capture who created and last modified an entity using RCommon's IAuditedEntity interface and AuditedEntity base classes with generic user identifier types. # Auditing Audit tracking records who created an entity, when it was created, who last modified it, and when. RCommon provides `IAuditedEntity` and `AuditedEntity` to standardize these fields across your domain model, making it straightforward to capture audit information in any persistence layer. :::tip Related The [End-to-End DDD Recipe](../examples-recipes/domain-driven-design.mdx) doesn't use `IAuditedEntity` (its `Team` aggregate focuses on aggregates, domain events, value objects, soft delete, and multi-tenancy instead), but combines those building blocks the same way you would layer `IAuditedEntity` onto your own aggregate. ::: ## Installation ## The IAuditedEntity Interface `IAuditedEntity` defines four audit properties. The generic parameters let you choose the type used to represent a user — typically `string` (for a username or user ID) or a domain-specific type: ```csharp public interface IAuditedEntity : IBusinessEntity { TCreatedByUser? CreatedBy { get; set; } DateTime? DateCreated { get; set; } DateTime? DateLastModified { get; set; } TLastModifiedByUser? LastModifiedBy { get; set; } } ``` A second overload adds a strongly-typed primary key: ```csharp public interface IAuditedEntity : IAuditedEntity, IBusinessEntity { } ``` ## The AuditedEntity Base Classes Two abstract base classes implement `IAuditedEntity` and sit on top of the standard `BusinessEntity` hierarchy, so you get audit fields, key support, and transactional event tracking in a single type. ### Without an explicit key type Use `AuditedEntity` when your entity has a composite key or when you want to define the key separately: ```csharp public abstract class AuditedEntity : BusinessEntity, IAuditedEntity { public DateTime? DateCreated { get; set; } public TCreatedByUser? CreatedBy { get; set; } public DateTime? DateLastModified { get; set; } public TLastModifiedByUser? LastModifiedBy { get; set; } } ``` ### With a strongly-typed primary key Use `AuditedEntity` for entities with a single typed primary key — this is the form you will use most often: ```csharp public abstract class AuditedEntity : BusinessEntity, IAuditedEntity, IAuditedEntity where TKey : IEquatable { public DateTime? DateCreated { get; set; } public TCreatedByUser? CreatedBy { get; set; } public DateTime? DateLastModified { get; set; } public TLastModifiedByUser? LastModifiedBy { get; set; } } ``` ## Usage ### Basic audited entity The most common pattern uses `string` for both user types, storing a user identifier such as a username or a claim value: ```csharp public class Invoice : AuditedEntity { public string CustomerName { get; private set; } public decimal TotalAmount { get; private set; } public InvoiceStatus Status { get; private set; } protected Invoice() { } public Invoice(Guid id, string customerName, decimal totalAmount) { Id = id; CustomerName = customerName; TotalAmount = totalAmount; Status = InvoiceStatus.Draft; } } ``` ### Setting audit fields Audit fields are plain settable properties, so your application layer or persistence interceptor can populate them before saving: ```csharp public class AuditInterceptor { private readonly ICurrentUserService _currentUser; private readonly ISystemClock _clock; public AuditInterceptor(ICurrentUserService currentUser, ISystemClock clock) { _currentUser = currentUser; _clock = clock; } public void ApplyAudit(IAuditedEntity entity, bool isNew) { var now = _clock.UtcNow; var user = _currentUser.UserId; if (isNew) { entity.DateCreated = now; entity.CreatedBy = user; } entity.DateLastModified = now; entity.LastModifiedBy = user; } } ``` ### Combining auditing with aggregate root behavior You can combine audit tracking with aggregate root capabilities by having your entity extend `AuditedEntity` and separately managing domain events through `BusinessEntity`'s event tracking. If you need both audit fields and domain events on a single aggregate, you can compose the audit interface onto an `AggregateRoot`: ```csharp public class Project : AggregateRoot, IAuditedEntity { public string Name { get; private set; } public ProjectStatus Status { get; private set; } // Audit properties public string? CreatedBy { get; set; } public DateTime? DateCreated { get; set; } public DateTime? DateLastModified { get; set; } public string? LastModifiedBy { get; set; } protected Project() { } public Project(Guid id, string name) { Id = id; Name = name; Status = ProjectStatus.Active; IncrementVersion(); AddDomainEvent(new ProjectCreatedEvent(id, name)); } } ``` ### Using a typed user identifier If your system uses a custom type for users, substitute it for the generic parameters: ```csharp // Using an integer user ID public class Document : AuditedEntity { public string Title { get; private set; } public string Content { get; private set; } public Document(Guid id, string title, string content) { Id = id; Title = title; Content = content; } } ``` ## Integration with Persistence The audit properties are plain C# properties with no infrastructure coupling. You wire up population in whichever layer makes sense for your architecture: **Entity Framework Core SaveChanges override:** ```csharp public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var now = DateTime.UtcNow; var userId = _currentUserService.UserId; foreach (var entry in ChangeTracker.Entries()) { if (entry.Entity is IAuditedEntity audited) { if (entry.State == EntityState.Added) { audited.DateCreated = now; audited.CreatedBy = userId; } if (entry.State == EntityState.Added || entry.State == EntityState.Modified) { audited.DateLastModified = now; audited.LastModifiedBy = userId; } } } return await base.SaveChangesAsync(cancellationToken); } ``` **Repository base class:** ```csharp public class AuditedRepository : EFCoreRepository where TEntity : class { private readonly ICurrentUserService _currentUser; protected override async Task BeforeSaveAsync(TEntity entity, EntityState state) { if (entity is IAuditedEntity audited) { var now = DateTime.UtcNow; var userId = _currentUser.UserId; if (state == EntityState.Added) { audited.DateCreated = now; audited.CreatedBy = userId; } audited.DateLastModified = now; audited.LastModifiedBy = userId; } } } ``` ## API Summary | Type | Description | |------|-------------| | `IAuditedEntity` | Interface defining `CreatedBy`, `DateCreated`, `LastModifiedBy`, and `DateLastModified` | | `IAuditedEntity` | Extends the above with a typed primary key | | `AuditedEntity` | Abstract base class without explicit key type; extends `BusinessEntity` | | `AuditedEntity` | Abstract base class with typed primary key; extends `BusinessEntity` | --- ## Domain Events Source: https://rcommon.com/docs/domain-driven-design/domain-events Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline. # Domain Events Domain events represent something meaningful that happened within your domain. They are raised by aggregate roots when state changes occur, then dispatched after the unit of work commits. This keeps side effects (sending emails, updating read models, publishing to message brokers) decoupled from the core business logic. :::tip See it in a full example The [End-to-End DDD Recipe](../examples-recipes/domain-driven-design.mdx) raises `TeamMemberAddedEvent` from `Team.AddMember` and dispatches it to a real `ISubscriber` via `UnitOfWork.CommitAsync()`, in both the common single-scope case and a cross-scope mutation case. ::: ## Installation ## The IDomainEvent Interface All domain events must implement `IDomainEvent`, which extends `ISerializableEvent` to make events compatible with the existing RCommon event routing pipeline: ```csharp public interface IDomainEvent : ISerializableEvent { /// /// Unique identifier for this event instance. /// Guid EventId { get; } /// /// The date and time when this event occurred. /// DateTimeOffset OccurredOn { get; } } ``` ## The DomainEvent Base Record Rather than implementing `IDomainEvent` manually, extend the `DomainEvent` abstract record. It automatically assigns a unique `EventId` and captures `OccurredOn` at construction time: ```csharp public abstract record DomainEvent : IDomainEvent { public Guid EventId { get; init; } = Guid.NewGuid(); public DateTimeOffset OccurredOn { get; init; } = DateTimeOffset.UtcNow; } ``` Using a C# `record` gives you structural equality, `with`-expression support, and immutability without boilerplate. ## Defining Domain Events Create concrete domain events by extending `DomainEvent` and adding the properties that describe what happened: ```csharp public record OrderCreatedEvent(Guid OrderId, string CustomerId) : DomainEvent; public record OrderLineAddedEvent(Guid OrderId, string ProductId, int Quantity) : DomainEvent; public record OrderSubmittedEvent(Guid OrderId) : DomainEvent; public record CustomerEmailChangedEvent( Guid CustomerId, string OldEmail, string NewEmail) : DomainEvent; ``` Keep domain events in the past tense. They describe facts about what has already happened, not intentions about what should happen next. ## Raising Events from Aggregate Roots Domain events are raised inside aggregate root methods using `AddDomainEvent`. Events accumulate on the aggregate until the unit of work completes: ```csharp public class Order : AggregateRoot { public string CustomerId { get; private set; } public OrderStatus Status { get; private set; } public Order(Guid id, string customerId) { Id = id; CustomerId = customerId; Status = OrderStatus.Draft; IncrementVersion(); AddDomainEvent(new OrderCreatedEvent(id, customerId)); } public void Submit() { if (Status != OrderStatus.Draft) throw new InvalidOperationException("Only draft orders can be submitted."); Status = OrderStatus.Submitted; IncrementVersion(); AddDomainEvent(new OrderSubmittedEvent(Id)); } } ``` The `AddDomainEvent` method places the event in both: - `DomainEvents` — a typed `IReadOnlyCollection` specific to the aggregate root - `LocalEvents` — the inherited `BusinessEntity` collection consumed by the event tracking infrastructure Both collections stay in sync through `AddDomainEvent`, `RemoveDomainEvent`, and `ClearDomainEvents`. Do not call the inherited `AddLocalEvent` directly on an aggregate root, as this would break the dual-list invariant. ## How Events Flow Through the System After your aggregate is saved through a repository, the event tracking pipeline dispatches all accumulated events: ``` 1. Aggregate raises events via AddDomainEvent() | v 2. Repository saves the aggregate, then calls IEntityEventTracker.AddEntity(aggregate) | v 3. IEntityEventTracker.EmitTransactionalEventsAsync() — traverses the aggregate's object graph for IBusinessEntity instances — collects LocalEvents from the aggregate root (and any nested IBusinessEntity children) | v 4. IEventRouter.AddTransactionalEvents() + RouteEventsAsync() — routes events through registered IEventProducer implementations | v 5. IEventProducer dispatches events to subscribers — in-memory via IEventBus — or to external brokers (MassTransit, Wolverine, etc.) ``` This pipeline is provided by `RCommon.Core` and `RCommon.Entities` with no extra configuration needed when you use the standard repository implementations. ## Handling Domain Events Implement `ISubscriber` to handle a domain event. The framework resolves handlers from the DI container when events are published via `IEventBus`: ```csharp public class OrderSubmittedHandler : ISubscriber { private readonly IEmailService _emailService; private readonly IOrderReadModelRepository _readModels; public OrderSubmittedHandler(IEmailService emailService, IOrderReadModelRepository readModels) { _emailService = emailService; _readModels = readModels; } public async Task HandleAsync(OrderSubmittedEvent @event, CancellationToken cancellationToken = default) { // Update a read model await _readModels.MarkSubmittedAsync(@event.OrderId, cancellationToken); // Send a notification await _emailService.SendOrderConfirmationAsync(@event.OrderId, cancellationToken); } } ``` A handler can implement `ISubscriber` for multiple event types if needed: ```csharp public class OrderAuditHandler : ISubscriber, ISubscriber { public Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default) => LogAsync("created", @event.OrderId, cancellationToken); public Task HandleAsync(OrderSubmittedEvent @event, CancellationToken cancellationToken = default) => LogAsync("submitted", @event.OrderId, cancellationToken); private Task LogAsync(string action, Guid orderId, CancellationToken ct) { ... } } ``` ## Removing or Cancelling Events If business logic determines an event should not be dispatched (for example, a validation step fails after the event was raised), remove it before the unit of work completes: ```csharp public void CancelSubmission() { if (Status == OrderStatus.Submitted) { Status = OrderStatus.Draft; // Remove the previously raised event if still pending var pending = DomainEvents.OfType().FirstOrDefault(); if (pending is not null) RemoveDomainEvent(pending); AddDomainEvent(new OrderSubmissionCancelledEvent(Id)); } } ``` ## Inspecting Pending Events The `DomainEvents` property on an aggregate root exposes all events that have been raised but not yet dispatched. This is useful for testing: ```csharp var order = new Order(Guid.NewGuid(), "C42"); order.Submit(); Assert.Single(order.DomainEvents.OfType()); ``` After the repository dispatches events, call `ClearDomainEvents()` or allow the infrastructure to clear them automatically. The `InMemoryEntityEventTracker` does not clear events after dispatch — the repository or unit of work implementation is responsible for this depending on your persistence layer. ## API Summary | Type | Description | |------|-------------| | `IDomainEvent` | Interface for all domain events; extends `ISerializableEvent` with `EventId` and `OccurredOn` | | `DomainEvent` | Abstract base record; auto-generates `EventId` and `OccurredOn` | | `ISubscriber` | Interface for event handlers; resolved from DI on event publication | | `IEventBus` | In-process event bus for publishing and subscribing to events | | `IEventRouter` | Routes transactional events to registered `IEventProducer` implementations | | `IEventProducer` | Dispatches events to their destination (in-memory, broker, etc.) | | `IEntityEventTracker` | Collects entities and emits their transactional events after persistence | --- ## Entities & Aggregate Roots Source: https://rcommon.com/docs/domain-driven-design/entities-aggregates Learn how RCommon's BusinessEntity and AggregateRoot base classes give your domain model typed identity, domain event tracking, and optimistic concurrency out of the box. # Entities & Aggregate Roots In Domain-Driven Design, entities are objects with a distinct identity that persists over time. Aggregate roots are the primary entities that serve as the entry point for a cluster of related objects, enforcing consistency boundaries. RCommon provides a hierarchy of base classes that give you identity, equality, domain event tracking, and optimistic concurrency out of the box. :::tip See it in a full example The [End-to-End DDD Recipe](../examples-recipes/domain-driven-design.mdx) builds a `Team`/`TeamMembership` aggregate combining `AggregateRoot`, domain events, a value object, `ISoftDelete`, and `IMultiTenant` in one runnable project, including why the child entity is a `BusinessEntity` rather than a `DomainEntity`. ::: ## Installation ## Type Hierarchy RCommon provides several base types for building your domain model. Understanding when to use each is important: ``` ITrackedEntity IBusinessEntity BusinessEntity — composite keys, event tracking (no explicit key type) BusinessEntity — single typed key, event tracking IAggregateRoot : IBusinessEntity IAggregateRoot : IAggregateRoot, IBusinessEntity AggregateRoot : BusinessEntity, IAggregateRoot DomainEntity — child entities inside an aggregate (identity only, no event tracking) ``` ## BusinessEntity `BusinessEntity` is the foundational base class for all entities. It provides local (transactional) event tracking without requiring a specific key type — useful for entities with composite keys. `BusinessEntity` extends this with a strongly-typed `Id` property and is the type you will most commonly extend when building aggregate roots or tracked persistence entities. ```csharp // BusinessEntity gives you a typed Id and event tracking public abstract class BusinessEntity : BusinessEntity, IBusinessEntity where TKey : IEquatable { public virtual TKey Id { get; protected set; } public override object[] GetKeys() => new object[] { Id }; } ``` Key characteristics: - `GetKeys()` — returns the entity's key values as an object array, enabling generic repository operations - `LocalEvents` — a read-only collection of events accumulated during a unit of work, dispatched after persistence - `AllowEventTracking` — opt-out flag; set to `false` to suppress event dispatch for a given entity - `EntityEquals(IBusinessEntity other)` — identity-based equality via binary comparison ## AggregateRoot `AggregateRoot` extends `BusinessEntity` and is the entry point for a consistency boundary in your domain. It adds: - **Domain event management** — typed `AddDomainEvent`, `RemoveDomainEvent`, and `ClearDomainEvents` methods - **Optimistic concurrency** — a `Version` property decorated with `[ConcurrencyCheck]` ```csharp public abstract class AggregateRoot : BusinessEntity, IAggregateRoot where TKey : IEquatable { [ConcurrencyCheck] public virtual int Version { get; protected set; } [NotMapped] public IReadOnlyCollection DomainEvents { get; } protected void AddDomainEvent(IDomainEvent domainEvent) { ... } protected void RemoveDomainEvent(IDomainEvent domainEvent) { ... } public void ClearDomainEvents() { ... } protected void IncrementVersion() => Version++; } ``` ### Creating an Aggregate Root Extend `AggregateRoot` and raise domain events inside your business methods rather than from application services: ```csharp public class Order : AggregateRoot { public string CustomerId { get; private set; } public OrderStatus Status { get; private set; } private readonly List _lines = new(); public IReadOnlyCollection Lines => _lines.AsReadOnly(); protected Order() { } // required for ORM deserialization public Order(Guid id, string customerId) { Id = id; CustomerId = customerId; Status = OrderStatus.Draft; IncrementVersion(); AddDomainEvent(new OrderCreatedEvent(id, customerId)); } public void AddLine(string productId, int quantity, decimal unitPrice) { _lines.Add(new OrderLine(productId, quantity, unitPrice)); IncrementVersion(); AddDomainEvent(new OrderLineAddedEvent(Id, productId, quantity)); } public void Submit() { if (Status != OrderStatus.Draft) throw new InvalidOperationException("Only draft orders can be submitted."); Status = OrderStatus.Submitted; IncrementVersion(); AddDomainEvent(new OrderSubmittedEvent(Id)); } } ``` ## DomainEntity `DomainEntity` is for child entities that live inside an aggregate boundary. It provides identity-based equality but does **not** participate in event tracking. All domain events must be raised on the aggregate root. ```csharp public abstract class DomainEntity : IEquatable> where TKey : IEquatable { public virtual TKey Id { get; protected set; } public bool IsTransient() => Id is null || Id.Equals(default); // Equals, GetHashCode, == and != are implemented } ``` Use `DomainEntity` for objects within the aggregate that have their own identity but should not be accessed directly outside the aggregate root: ```csharp public class OrderLine : DomainEntity { public string ProductId { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; } protected OrderLine() { } public OrderLine(string productId, int quantity, decimal unitPrice) { Id = Guid.NewGuid(); ProductId = productId; Quantity = quantity; UnitPrice = unitPrice; } public decimal LineTotal => Quantity * UnitPrice; } ``` Note: Because `DomainEntity` does not implement `IBusinessEntity`, the `InMemoryEntityEventTracker`'s object graph walker will not traverse it. Events raised by child entities must be delegated up to the aggregate root. ## Identity and Equality Entities in RCommon use identity-based equality. Two entity instances are equal if and only if they are the same type and have the same key value. ```csharp var a = new Order(id: guid1, customerId: "C1"); var b = new Order(id: guid1, customerId: "C2"); bool same = a.EntityEquals(b); // true — same Id ``` Transient entities (those without an assigned persistent identity) are never equal to any other entity, including themselves by reference: ```csharp var transient = new Order(); // Id is default(Guid) bool isTransient = transient.IsTransient(); // true ``` ## IAggregateRoot Interfaces Two interfaces are provided for infrastructure scenarios: - `IAggregateRoot` — non-generic marker; useful for repository filtering and middleware - `IAggregateRoot` — generic version with `where TKey : IEquatable` Both expose `Version` and `DomainEvents` for infrastructure code that needs to dispatch events or enforce concurrency after persistence operations. ## API Summary | Type | Description | |------|-------------| | `BusinessEntity` | Abstract base with composite key support and transactional event tracking | | `BusinessEntity` | Abstract base with typed `Id` and event tracking | | `AggregateRoot` | Extends `BusinessEntity`; adds domain events and optimistic concurrency | | `DomainEntity` | Lightweight child entity with identity equality; no event tracking | | `IBusinessEntity` | Interface exposing key access, local events, and equality | | `IBusinessEntity` | Typed key interface extending `IBusinessEntity` | | `IAggregateRoot` | Non-generic marker interface for infrastructure use | | `IAggregateRoot` | Generic aggregate root interface | | `ITrackedEntity` | Minimal interface controlling whether an entity participates in event tracking | --- ## Soft Delete Source: https://rcommon.com/docs/domain-driven-design/soft-delete Mark records as deleted instead of removing them physically using RCommon's ISoftDelete interface, with automatic query filtering and explicit delete mode overrides. # Soft Delete Soft delete is the practice of marking a record as deleted rather than physically removing it from the database. The record remains in the data store with `IsDeleted = true`, allowing you to audit what was deleted, restore records if needed, and avoid referential integrity problems caused by physical deletes. :::tip See it in a full example The [End-to-End DDD Recipe](../examples-recipes/domain-driven-design.mdx) implements `ISoftDelete` on its `Team` aggregate and shows `ExistsAsync` excluding a soft-deleted row from reads. ::: ## Installation ## The ISoftDelete Interface Implement `ISoftDelete` on any entity that should support soft deletion: ```csharp public interface ISoftDelete { bool IsDeleted { get; set; } } ``` This is an opt-in interface. Entities that do not implement it continue to be physically deleted. If you call a soft-delete operation on an entity that does not implement `ISoftDelete`, an `InvalidOperationException` is thrown at runtime with a descriptive message. ## Marking an Entity as Soft-Deletable Add `ISoftDelete` to your entity class and add an `IsDeleted` property. The property should be backed by a column in your database: ```csharp public class Customer : BusinessEntity, ISoftDelete { public string Name { get; set; } public string Email { get; set; } public bool IsDeleted { get; set; } } ``` You can combine `ISoftDelete` with any base class in the RCommon entity hierarchy: ```csharp // With an aggregate root public class Order : AggregateRoot, ISoftDelete { public string CustomerId { get; private set; } public OrderStatus Status { get; private set; } public bool IsDeleted { get; set; } // ... domain logic } // With auditing public class Document : AuditedEntity, ISoftDelete { public string Title { get; set; } public string Content { get; set; } public bool IsDeleted { get; set; } } ``` ## Deleting with Auto-Detection The repository's `DeleteAsync` method automatically detects whether an entity implements `ISoftDelete`. If it does, an UPDATE is issued to set `IsDeleted = true` instead of a physical DELETE: ```csharp // Automatically uses soft delete if Customer implements ISoftDelete await repository.DeleteAsync(customer); // Automatically uses soft delete for all matching entities await repository.DeleteManyAsync(c => c.Name == "Acme Corp"); ``` ## Explicit Delete Mode Use the overloads that accept a `bool isSoftDelete` parameter when you need explicit control over the delete mode: ```csharp // Force soft delete (throws InvalidOperationException if entity does not implement ISoftDelete) await repository.DeleteAsync(customer, isSoftDelete: true); // Force physical delete even if the entity implements ISoftDelete await repository.DeleteAsync(customer, isSoftDelete: false); // Soft delete multiple entities matching a specification await repository.DeleteManyAsync(activeCustomersSpec, isSoftDelete: true); // Physical delete multiple entities matching an expression await repository.DeleteManyAsync( c => c.CreatedDate < cutoff, isSoftDelete: false); ``` ## Query Filtering RCommon repository implementations automatically exclude soft-deleted records from queries. When the entity type implements `ISoftDelete`, all standard find and query operations apply a `WHERE IsDeleted = false` filter: ```csharp // These queries automatically exclude soft-deleted records var customer = await repository.GetByIdAsync(customerId); // returns null if soft-deleted var all = await repository.FindAsync(c => c.Name == "Acme"); // excludes soft-deleted // The filter is applied at the IQueryable level, so it composes with your own predicates var recent = await repository.FindAsync( c => c.DateCreated > DateTime.UtcNow.AddDays(-30)); // only non-deleted, recent customers ``` The filter is built using `SoftDeleteHelper.GetNotDeletedFilter()`, which produces an expression equivalent to `e => !e.IsDeleted`. This filter is combined with any user-supplied predicate using a logical AND. ## EF Core: Global Query Filter For teams using Entity Framework Core directly, you can optionally configure a global query filter on the `DbContext` to ensure soft-deleted records are never returned by any query against that entity — including those executed outside the RCommon repository: ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { // Global query filter: automatically excludes soft-deleted customers from all queries modelBuilder.Entity() .HasQueryFilter(c => !c.IsDeleted); modelBuilder.Entity() .HasQueryFilter(o => !o.IsDeleted); } ``` With a global query filter in place, you can use `IgnoreQueryFilters()` when you explicitly need to include soft-deleted records: ```csharp // Include soft-deleted records for an admin audit report var allIncludingDeleted = await dbContext.Customers .IgnoreQueryFilters() .ToListAsync(); ``` Note: If you configure a global query filter in EF Core and also use the RCommon repository, the filter is applied at two layers. This is safe but redundant. Choose the approach that best fits your architecture. ## Restoring a Soft-Deleted Record To restore a soft-deleted record, set `IsDeleted = false` and call `UpdateAsync`: ```csharp // First, retrieve the soft-deleted record by including it manually // (standard repository queries exclude soft-deleted records automatically) var deletedCustomer = await dbContext.Customers .IgnoreQueryFilters() .FirstOrDefaultAsync(c => c.Id == customerId); if (deletedCustomer is not null) { deletedCustomer.IsDeleted = false; await repository.UpdateAsync(deletedCustomer); } ``` ## Implementation Notes The soft delete logic is encapsulated in `SoftDeleteHelper` in the `RCommon.Persistence` package: | Method | Purpose | |--------|---------| | `IsSoftDeletable()` | Returns `true` if `TEntity` implements `ISoftDelete` | | `EnsureSoftDeletable()` | Throws `InvalidOperationException` if `TEntity` does not implement `ISoftDelete` | | `MarkAsDeleted(entity)` | Sets `IsDeleted = true` on the entity (cast to `ISoftDelete`) | | `GetNotDeletedFilter()` | Returns an expression `e => !e.IsDeleted` for use in LINQ queries | | `CombineWithNotDeletedFilter(expression)` | ANDs the user expression with `!IsDeleted`; returns the original expression unchanged if the entity does not implement `ISoftDelete` | ## API Summary | Type | Description | |------|-------------| | `ISoftDelete` | Interface marking an entity as capable of soft deletion; exposes `IsDeleted` | | `IWriteOnlyRepository.DeleteAsync(entity)` | Soft deletes automatically if entity implements `ISoftDelete`; otherwise physically deletes | | `IWriteOnlyRepository.DeleteAsync(entity, isSoftDelete)` | Explicit delete mode, bypassing auto-detection | | `IWriteOnlyRepository.DeleteManyAsync(...)` | Bulk delete with the same auto-detection and explicit override overloads | | `SoftDeleteHelper` | Static utility class encapsulating soft delete detection, validation, and expression filtering | --- ## Value Objects Source: https://rcommon.com/docs/domain-driven-design/value-objects Model domain concepts by value rather than identity using RCommon's ValueObject and ValueObject<T> abstract records with built-in structural equality and immutability. # Value Objects Value objects are a DDD building block that model concepts by their value rather than by identity. Two value objects with the same data are considered equal — there is no separate identity field. Common examples include money amounts, addresses, email addresses, date ranges, and geographic coordinates. :::tip See it in a full example The [End-to-End DDD Recipe](../examples-recipes/domain-driven-design.mdx) uses an `EmailAddress : ValueObject` on `TeamMembership`, with a validating factory method and tests proving structural equality. ::: ## Installation ## When to Use Value Objects Use a value object when: - The concept is defined entirely by its attributes, not by a unique identifier - Instances should be immutable — changing a property means creating a new instance - Equality is structural: two instances with the same data represent the same thing Use an entity instead when: - The concept has a lifecycle and a persistent identity - Two instances with the same data can still be different things (for example, two orders for the same product) ## ValueObject Base Record RCommon provides `ValueObject` as an abstract C# `record`. Using a record leverages the language's built-in structural equality, `with`-expression support, and immutability without requiring manual `Equals` and `GetHashCode` implementations: ```csharp public abstract record ValueObject; ``` Derive your value objects from `ValueObject` using positional record syntax: ```csharp public record Money(decimal Amount, string Currency) : ValueObject; public record Address( string Street, string City, string State, string PostalCode, string Country) : ValueObject; public record DateRange(DateOnly Start, DateOnly End) : ValueObject; public record GeoCoordinate(double Latitude, double Longitude) : ValueObject; ``` ## ValueObject<T> for Single-Value Wrappers When a value object wraps a single primitive or simple type, use `ValueObject`. It adds a typed `Value` property and implicit conversions, reducing the noise of constantly unwrapping the underlying value: ```csharp public abstract record ValueObject(T Value) : ValueObject where T : notnull { public static implicit operator T(ValueObject valueObject) => valueObject.Value; public override string ToString() => Value.ToString() ?? string.Empty; } ``` Concrete single-value wrappers: ```csharp public record EmailAddress(string Value) : ValueObject(Value); public record CustomerId(Guid Value) : ValueObject(Value); public record ProductCode(string Value) : ValueObject(Value); public record Percentage(decimal Value) : ValueObject(Value); ``` The implicit conversions mean you can use value objects without constantly unwrapping them: ```csharp EmailAddress email = new EmailAddress("user@example.com"); // Implicit conversion to the underlying type string raw = email; // "user@example.com" // Or use the Value property explicitly Console.WriteLine(email.Value); ``` ## Structural Equality Because value objects are C# records, equality is structural by default. Two instances with the same property values are equal: ```csharp var a = new Money(10.00m, "USD"); var b = new Money(10.00m, "USD"); var c = new Money(15.00m, "USD"); bool equal = a == b; // true bool notEqual = a == c; // false ``` This also applies to complex value objects: ```csharp var addr1 = new Address("123 Main St", "Springfield", "IL", "62701", "US"); var addr2 = new Address("123 Main St", "Springfield", "IL", "62701", "US"); bool same = addr1 == addr2; // true ``` ## Immutability and the `with` Expression Value objects should be treated as immutable. To represent a changed value, create a new instance using the `with` expression: ```csharp var original = new Money(10.00m, "USD"); // Create a new instance rather than mutating the original var doubled = original with { Amount = original.Amount * 2 }; Console.WriteLine(original.Amount); // 10.00 Console.WriteLine(doubled.Amount); // 20.00 ``` ## Adding Domain Logic to Value Objects Value objects can encapsulate domain logic and validation. Add factory methods, computed properties, and domain operations directly on the record: ```csharp public record Money(decimal Amount, string Currency) : ValueObject { // Validation in the constructor public Money { if (Amount < 0) throw new ArgumentException("Money amount cannot be negative.", nameof(Amount)); if (string.IsNullOrWhiteSpace(Currency)) throw new ArgumentException("Currency is required.", nameof(Currency)); Currency = Currency.ToUpperInvariant(); } public Money Add(Money other) { if (Currency != other.Currency) throw new InvalidOperationException($"Cannot add {Currency} and {other.Currency}."); return new Money(Amount + other.Amount, Currency); } public Money Subtract(Money other) { if (Currency != other.Currency) throw new InvalidOperationException($"Cannot subtract {Currency} and {other.Currency}."); return new Money(Amount - other.Amount, Currency); } public bool IsZero => Amount == 0m; public static Money Zero(string currency) => new(0m, currency); } ``` ```csharp public record EmailAddress(string Value) : ValueObject(Value) { public EmailAddress { if (string.IsNullOrWhiteSpace(Value)) throw new ArgumentException("Email address cannot be empty.", nameof(Value)); if (!Value.Contains('@')) throw new ArgumentException("Email address must contain '@'.", nameof(Value)); Value = Value.ToLowerInvariant().Trim(); } public string Domain => Value.Split('@')[1]; } ``` ## Using Value Objects in Entities Value objects compose naturally into entities and aggregate roots: ```csharp public class Customer : AggregateRoot { public EmailAddress Email { get; private set; } public Address ShippingAddress { get; private set; } public Customer(Guid id, EmailAddress email, Address shippingAddress) { Id = id; Email = email; ShippingAddress = shippingAddress; AddDomainEvent(new CustomerRegisteredEvent(id, email.Value)); } public void ChangeEmail(EmailAddress newEmail) { var old = Email; Email = newEmail; AddDomainEvent(new CustomerEmailChangedEvent(Id, old.Value, newEmail.Value)); } } ``` ## Persisting Value Objects When using Entity Framework Core, value objects are typically mapped as owned entity types so they are stored in the parent entity's table without a separate identity column: ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(customer => { customer.OwnsOne(c => c.ShippingAddress, address => { address.Property(a => a.Street).HasColumnName("ShippingStreet"); address.Property(a => a.City).HasColumnName("ShippingCity"); address.Property(a => a.PostalCode).HasColumnName("ShippingPostalCode"); address.Property(a => a.Country).HasColumnName("ShippingCountry"); }); // Single-value wrappers: map through the Value property customer.Property(c => c.Email) .HasConversion( email => email.Value, raw => new EmailAddress(raw)); }); } ``` ## API Summary | Type | Description | |------|-------------| | `ValueObject` | Abstract base record; structural equality via C# record semantics | | `ValueObject` | Abstract base record for single-value wrappers; adds typed `Value` property and implicit conversions to/from `T` | --- ## Overview Source: https://rcommon.com/docs/email/overview Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event. # Email Overview ## Overview RCommon provides a provider-agnostic email abstraction so that application code does not depend on a specific mail delivery library. Your services depend only on `IEmailService`; the underlying transport — SMTP or a cloud API — is a startup configuration concern. The `RCommon.Emailing` package delivers a built-in SMTP implementation backed by `System.Net.Mail.SmtpClient`. Third-party providers such as SendGrid are available through separate packages. ### Core abstractions `IEmailService` exposes two methods and one event: - `SendEmail(MailMessage)` — synchronous send. - `SendEmailAsync(MailMessage, CancellationToken)` — asynchronous send. - `EmailSent` event — raised after a message is sent successfully, with `EmailEventArgs` carrying the original `MailMessage`. Both methods accept a standard `System.Net.Mail.MailMessage`, which means you can use the full .NET mail model (multiple recipients, CC, BCC, attachments, HTML body, inline images) regardless of which provider is configured. ## Installation ## Configuration Call `WithSmtpEmailServices` inside your `AddRCommon()` block and provide a delegate that configures `SmtpEmailSettings`. ```csharp using RCommon; builder.Services.AddRCommon() .WithSmtpEmailServices(smtp => { smtp.Host = "smtp.example.com"; smtp.Port = 587; smtp.EnableSsl = true; smtp.UserName = builder.Configuration["Email:UserName"]; smtp.Password = builder.Configuration["Email:Password"]; smtp.FromEmailDefault = "noreply@example.com"; smtp.FromNameDefault = "My Application"; }); ``` Bind settings from `appsettings.json` to avoid hard-coding values: ```json { "Email": { "Host": "smtp.example.com", "Port": 587, "EnableSsl": true, "UserName": "", "Password": "", "FromEmailDefault": "noreply@example.com", "FromNameDefault": "My Application" } } ``` ```csharp builder.Services.AddRCommon() .WithSmtpEmailServices(smtp => builder.Configuration.GetSection("Email").Bind(smtp)); ``` ### Modular composition Both `WithSmtpEmailServices` and `WithSendGridEmailServices` are singleton-style verbs: only one `IEmailService` implementation makes sense per process, so they enforce the choice at startup. | Re-registration shape | Behaviour | |---|---| | Same verb called twice (e.g. `WithSmtpEmailServices` in two modules) | Idempotent no-op — the second call detects the impl is already registered and skips the second registration. The latest configuration delegate still runs against `SmtpEmailSettings`, so option values follow last-write-wins. | | Different email impls in the same process (e.g. one module calls `WithSmtpEmailServices`, another calls `WithSendGridEmailServices`) | Throws `RCommonBuilderException` naming both impl types. Pick exactly one transport. | See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ## Usage Inject `IEmailService` wherever you need to send mail. ### Sending a plain-text email ```csharp using System.Net.Mail; public class WelcomeEmailService { private readonly IEmailService _emailService; public WelcomeEmailService(IEmailService emailService) { _emailService = emailService; } public async Task SendWelcomeAsync(string recipientEmail, string recipientName, CancellationToken ct = default) { using var message = new MailMessage( from: "noreply@example.com", to: recipientEmail, subject: "Welcome to the platform", body: $"Hi {recipientName}, your account is ready."); await _emailService.SendEmailAsync(message, ct); } } ``` ### Sending an HTML email ```csharp using var message = new MailMessage { From = new MailAddress("noreply@example.com", "My App"), Subject = "Your order has shipped", Body = "

Your order is on its way!

", IsBodyHtml = true }; message.To.Add(new MailAddress("customer@example.com", "Jane Doe")); await _emailService.SendEmailAsync(message, ct); ``` ### Sending with an attachment ```csharp using var message = new MailMessage("noreply@example.com", "user@example.com") { Subject = "Monthly report", Body = "Please find this month's report attached." }; await using var stream = File.OpenRead("/tmp/report.pdf"); message.Attachments.Add(new Attachment(stream, "report.pdf", "application/pdf")); await _emailService.SendEmailAsync(message, ct); ``` ### Subscribing to the EmailSent event ```csharp _emailService.EmailSent += (sender, args) => { Console.WriteLine($"Email sent to {args.Message.To.First().Address}"); }; ``` ## API Summary ### `IEmailService` | Member | Description | |--------|-------------| | `SendEmail(MailMessage)` | Sends the message synchronously. | | `SendEmailAsync(MailMessage, CancellationToken)` | Sends the message asynchronously. | | `EmailSent` | Event raised after successful delivery. | ### `SmtpEmailSettings` | Property | Description | |----------|-------------| | `Host` | SMTP server hostname or IP address. | | `Port` | SMTP port (commonly 587 for STARTTLS, 465 for SSL, 25 for unencrypted). | | `EnableSsl` | When `true`, the connection is encrypted. | | `UserName` | SMTP authentication username. | | `Password` | SMTP authentication password. | | `FromEmailDefault` | Default sender email address. | | `FromNameDefault` | Default sender display name. | ### `IRCommonBuilder` extension | Method | Description | |--------|-------------| | `WithSmtpEmailServices(Action)` | Registers `SmtpEmailService` as `IEmailService` and configures SMTP settings. | --- ## SendGrid Source: https://rcommon.com/docs/email/sendgrid RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support. # SendGrid ## Overview `RCommon.SendGrid` provides a `SendGridEmailService` that implements `IEmailService` using the official SendGrid .NET SDK. Your application code continues to work with standard `System.Net.Mail.MailMessage` objects; the implementation converts them to SendGrid API calls transparently. Switching from SMTP to SendGrid is a startup configuration change — no application code needs to be modified. ### How it works `SendGridEmailService` accepts a `MailMessage` and converts it to a `SendGridMessage` using `MailHelper.CreateSingleEmailToMultipleRecipients`. The body is sent as HTML when `MailMessage.IsBodyHtml` is `true`, otherwise as plain text. Attachments are streamed directly into the SendGrid message before the API call is made. The synchronous `SendEmail` method wraps the async implementation using `AsyncHelper.RunSync`. ## Installation ## Configuration Call `WithSendGridEmailServices` inside your `AddRCommon()` block and provide a delegate that configures `SendGridEmailSettings`. ```csharp using RCommon; builder.Services.AddRCommon() .WithSendGridEmailServices(sg => { sg.SendGridApiKey = builder.Configuration["SendGrid:ApiKey"]; sg.FromEmailDefault = "noreply@example.com"; sg.FromNameDefault = "My Application"; }); ``` Store the API key in a secrets manager or environment variable, never in source-controlled configuration files: ```json { "SendGrid": { "ApiKey": "", "FromEmailDefault": "noreply@example.com", "FromNameDefault": "My Application" } } ``` Set `SendGrid:ApiKey` via: - `dotnet user-secrets set "SendGrid:ApiKey" "SG.your_key_here"` during development - An environment variable `SendGrid__ApiKey` in production containers - Azure Key Vault, AWS Secrets Manager, or your preferred secrets service ### Binding from configuration ```csharp builder.Services.AddRCommon() .WithSendGridEmailServices(sg => builder.Configuration.GetSection("SendGrid").Bind(sg)); ``` :::tip Modular composition `WithSendGridEmailServices` is a singleton-style verb: only one `IEmailService` implementation makes sense per process. Calling `WithSendGridEmailServices` from two modules is an idempotent no-op — the second registration is skipped, and `SendGridEmailSettings` follows last-write-wins for any options the second module's delegate assigns. Mixing `WithSendGridEmailServices` and `WithSmtpEmailServices` in the same process throws `RCommonBuilderException` naming both impl types; pick exactly one transport. See [Modular Composition](../core-concepts/modular-composition.mdx) and the [Email overview](./overview.mdx#modular-composition) for the full conflict matrix. ::: ## Usage Inject `IEmailService` exactly as you would with SMTP. No SendGrid-specific types appear in application code. ### Sending a plain-text email ```csharp using System.Net.Mail; public class NotificationService { private readonly IEmailService _emailService; public NotificationService(IEmailService emailService) { _emailService = emailService; } public async Task SendPasswordResetAsync(string email, string resetLink, CancellationToken ct = default) { using var message = new MailMessage( from: "noreply@example.com", to: email, subject: "Reset your password", body: $"Follow this link to reset your password: {resetLink}"); await _emailService.SendEmailAsync(message, ct); } } ``` ### Sending an HTML email ```csharp using var message = new MailMessage { From = new MailAddress("noreply@example.com", "My App"), Subject = "Your invoice is ready", Body = "

Your invoice for this month is now available.

", IsBodyHtml = true }; message.To.Add(new MailAddress("customer@example.com", "Jane Doe")); await _emailService.SendEmailAsync(message, ct); ``` ### Sending to multiple recipients ```csharp using var message = new MailMessage { From = new MailAddress("alerts@example.com"), Subject = "Scheduled maintenance", Body = "The system will be offline on Saturday from 02:00 to 04:00 UTC.", IsBodyHtml = false }; message.To.Add(new MailAddress("alice@example.com")); message.To.Add(new MailAddress("bob@example.com")); await _emailService.SendEmailAsync(message, ct); ``` ### Sending with an attachment ```csharp using var message = new MailMessage( "reports@example.com", "finance@example.com") { Subject = "Q1 Report", Body = "

Please find the Q1 report attached.

", IsBodyHtml = true }; await using var pdf = File.OpenRead("/tmp/q1-report.pdf"); message.Attachments.Add(new Attachment(pdf, "q1-report.pdf", "application/pdf")); await _emailService.SendEmailAsync(message, ct); ``` ## API Summary ### `SendGridEmailService` Registered as `IEmailService`. Accepts `IOptions` via constructor injection and constructs a `SendGridClient` internally. | Member | Description | |--------|-------------| | `SendEmail(MailMessage)` | Synchronous wrapper around `SendEmailAsync`. | | `SendEmailAsync(MailMessage, CancellationToken)` | Converts the message and sends it via the SendGrid API. No-op when `message.To` is empty. | | `EmailSent` | Raised after the SendGrid API call completes successfully. | ### `SendGridEmailSettings` | Property | Description | |----------|-------------| | `SendGridApiKey` | The SendGrid API key used to authenticate all API calls. | | `FromEmailDefault` | Default sender email address when no `From` address is set on the `MailMessage`. | | `FromNameDefault` | Default sender display name. | ### `IRCommonBuilder` extension | Method | Description | |--------|-------------| | `WithSendGridEmailServices(Action)` | Registers `SendGridEmailService` as `IEmailService` and configures SendGrid settings. | --- ## Distributed Events Source: https://rcommon.com/docs/event-handling/distributed Extend RCommon event handling to message brokers with MassTransit or Wolverine, mixing transports in one app while keeping ISubscriber handler code broker-agnostic. # Distributed Events Distributed event handling extends the in-process model to message brokers, enabling events to cross service boundaries. RCommon integrates with MassTransit and Wolverine as transport backends while keeping the application handler code identical to the in-memory case: every handler implements the same `ISubscriber` interface. ## How distributed events work When a distributed transport is configured, RCommon registers a broker-specific `IEventProducer` (for example `PublishWithMassTransitEventProducer` or `PublishWithWolverineEventProducer`). When your application calls `IEventRouter.RouteEventsAsync`, the router forwards each event to the producer bound to that event type. The producer then calls the broker's publish or send API. On the receiving side, the broker delivers the message to a consumer that is registered by RCommon. That consumer resolves the matching `ISubscriber` from the DI container and calls `HandleAsync`. The result is: - **Producer code** calls `IEventRouter` or `IEventProducer` — no broker-specific API in application code. - **Handler code** implements `ISubscriber` — no broker-specific base class or attribute. - **Routing** is managed by the internal `EventSubscriptionManager`, which tracks which events go to which producers. ## Publish vs Send Both MassTransit and Wolverine support two dispatch patterns: | Producer | Pattern | Description | |----------|---------|-------------| | `PublishWithMassTransitEventProducer` | Fan-out | Delivered to all consumers subscribed to the event type | | `SendWithMassTransitEventProducer` | Point-to-point | Delivered to a single consumer endpoint | | `PublishWithWolverineEventProducer` | Fan-out | Delivered to all Wolverine handlers for the message type | | `SendWithWolverineEventProducer` | Point-to-point | Delivered to a single Wolverine handler endpoint | Use publish (fan-out) for events where multiple downstream services should react. Use send (point-to-point) for command-style messages where exactly one consumer should act. ## Event contract requirements Events that travel across a message broker must implement `ISerializableEvent`. They should also have a parameterless constructor so the broker can deserialize incoming messages: ```csharp using RCommon.Models.Events; public class OrderShipped : ISyncEvent { public OrderShipped(Guid orderId, DateTime shippedAt) { OrderId = orderId; ShippedAt = shippedAt; } public OrderShipped() { } public Guid OrderId { get; } public DateTime ShippedAt { get; } } ``` ## Mixing transports A single application can register multiple event handling builders simultaneously. For example, you can route one event type to the in-memory bus (for local side-effects) and another to MassTransit (for cross-service delivery): ```csharp builder.Services.AddRCommon() .WithEventHandling(local => { local.AddProducer(); local.AddSubscriber(); }) .WithEventHandling(mt => { mt.AddProducer(); mt.AddSubscriber(); // ... MassTransit transport configuration ... }); ``` The `EventSubscriptionManager` ensures that `AuditEvent` goes only to the in-memory producer and `OrderShipped` goes only to MassTransit. Each handler still implements `ISubscriber` — the transport is invisible to handler code. ## Guaranteed delivery For reliable at-least-once delivery guarantees, pair MassTransit or Wolverine with the transactional outbox pattern. See [Transactional Outbox](./transactional-outbox.mdx) for details. ## See also - [MassTransit](./masstransit.mdx) — MassTransit-specific configuration and consumers - [Wolverine](./wolverine.mdx) — Wolverine-specific configuration and handlers - [Transactional Outbox](./transactional-outbox.mdx) — guaranteeing delivery with an outbox --- ## In-Memory Events Source: https://rcommon.com/docs/event-handling/in-memory Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly. # In-Memory Events The in-memory event bus provides local publish/subscribe within a single process. Events are dispatched synchronously to all registered `ISubscriber` handlers within the same DI scope. No external infrastructure is required. ## Installation The in-memory event bus is part of `RCommon.Core`, which is included when you install the main package: ## Configuration Register the in-memory event bus using `WithEventHandling` on the RCommon builder, then call `AddSubscriber` to wire each handler to its event type: ```csharp using RCommon; using RCommon.EventHandling; using RCommon.EventHandling.Producers; builder.Services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }); ``` `AddSubscriber` automatically ensures `PublishWithEventBusEventProducer` — the built-in producer that delegates to `IEventBus.PublishAsync` — is registered; a subscriber with no matching producer is never invoked, so there is no scenario where you'd want one wired without the other. Calling `AddProducer()` explicitly still works and is harmless (idempotent), but is no longer required for the common case: ```csharp builder.Services.AddRCommon() .WithEventHandling(eventHandling => { // Explicit and still supported, but redundant as of this version -- AddSubscriber // below registers this automatically. eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` Additional subscribers can be registered for the same event type; all of them will be called. ### Additional AddSubscriber / AddProducer overloads If your handler needs constructor arguments that DI can't resolve on its own, use the factory-delegate overload of `AddSubscriber` instead of the parameterless one: ```csharp eventHandling.AddSubscriber( sp => new OrderPlacedHandler(sp.GetRequiredService>(), extraArg: 42)); ``` `AddProducer` has two additional overloads beyond the parameterless `AddProducer()` shown above: ```csharp // Factory delegate -- same idea as AddSubscriber's factory overload. eventHandling.AddProducer(sp => new MyCustomProducer(sp.GetRequiredService())); // Pre-built instance. var producer = new MyCustomProducer(preconfiguredClient); eventHandling.AddProducer(producer); ``` The instance overload (`AddProducer(T producer)`) has a behaviorally significant side effect: if the instance you pass also implements `IHostedService`, it is additionally registered as a hosted service, so it participates in the host's start/stop lifecycle alongside being resolvable as `IEventProducer`. The parameterless and factory-delegate overloads do not do this. If you're registering a *different* producer (not `PublishWithEventBusEventProducer`) alongside subscribers on a custom `IEventHandlingBuilder`, RCommon's startup diagnostics will warn (once) if it ever finds a builder type with recorded subscriptions but zero registered producers — since that combination means the subscribers will silently never fire. :::tip Modular composition `WithEventHandling` is cache-aware. When multiple modules call it, the cached `InMemoryEventBusBuilder` is reused and each configuration delegate runs against the same instance — so subscriber and producer registrations from every module accumulate. `AddProducer` deduplicates by concrete producer type: if `OrderingModule` and `NotificationsModule` both call `eventHandling.AddProducer()`, exactly one `AuditProducer` descriptor is registered. `AddSubscriber` accumulates normally, so multiple modules can register handlers for the same event. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Defining an event Events must implement `ISerializableEvent`. Use `ISyncEvent` for sequential dispatch or `IAsyncEvent` for concurrent dispatch: :::note `ISyncEvent`/`IAsyncEvent` control dispatch ordering, not transport Despite the "Sync" name, `ISyncEvent` does **not** mean "in-memory only" and `IAsyncEvent` does not mean "out-of-process." Both extend `ISerializableEvent` and are fully serializable — an `ISyncEvent` is persisted to and drained from the [transactional outbox](./outbox-producer-processor-topology.mdx) just like any other event. The only thing these markers select is how the router dispatches a batch: `ISyncEvent` events are dispatched **sequentially** (awaited one at a time, preserving order), `IAsyncEvent` events **concurrently**. Choose based on whether ordering between events matters, not on which transport you expect. ::: ```csharp using RCommon.Models.Events; public class OrderPlaced : ISyncEvent { public OrderPlaced(Guid orderId, DateTime placedAt) { OrderId = orderId; PlacedAt = placedAt; } public OrderPlaced() { } public Guid OrderId { get; } public DateTime PlacedAt { get; } } ``` ## Implementing a subscriber Implement `ISubscriber` and register the class in the DI container via `AddSubscriber`: ```csharp using RCommon.EventHandling.Subscribers; public class OrderPlacedHandler : ISubscriber { private readonly ILogger _logger; public OrderPlacedHandler(ILogger logger) { _logger = logger; } public async Task HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default) { _logger.LogInformation("Order {OrderId} placed at {PlacedAt}", @event.OrderId, @event.PlacedAt); await Task.CompletedTask; } } ``` ### Two ways to register a subscriber (and why they're equivalent) A handler can be wired either through the event-handling builder or directly into DI: ```csharp // 1. Through the builder (recommended — also records the event→producer subscription // and auto-registers the in-memory producer): eventHandling.AddSubscriber(); // 2. Directly into the container: services.AddScoped, OrderPlacedHandler>(); ``` At dispatch time these resolve identically. The in-memory bus (`InMemoryEventBus`) resolves **all** `ISubscriber` registrations from the DI container for the event's runtime type, regardless of how they were registered — so a handler added with the raw `AddScoped` call is invoked just like one added via `AddSubscriber`. This holds for the [outbox poller](./outbox-producer-processor-topology.mdx) too, since it dispatches through the same in-memory producer. Prefer `AddSubscriber()`: besides registering the handler, it records the event→producer subscription used for routing and auto-registers `PublishWithEventBusEventProducer`. Use the raw `services.AddScoped, THandler>()` form only when you must register a handler outside a builder context (for example, from a module that does not have the event-handling builder in scope) — in that case ensure a producer is registered so the subscriber can actually be reached. ## Publishing events ### Via IEventProducer (recommended with unit-of-work) Resolve `IEventProducer` or `IEnumerable` and call `ProduceEventAsync`. The router ensures the event reaches only the producers subscribed to it: ```csharp public class OrderService { private readonly IEventRouter _eventRouter; private readonly IUnitOfWorkFactory _uowFactory; public OrderService(IEventRouter eventRouter, IUnitOfWorkFactory uowFactory) { _eventRouter = eventRouter; _uowFactory = uowFactory; } public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken) { using var uow = _uowFactory.CreateUnitOfWork(); // ... persist the order ... _eventRouter.AddTransactionalEvent(new OrderPlaced(order.Id, DateTime.UtcNow)); await uow.SaveChangesAsync(cancellationToken); await _eventRouter.RouteEventsAsync(cancellationToken); } } ``` ### Via IEventBus (direct, no routing layer) You can also publish directly through `IEventBus` without going through the router: ```csharp public class NotificationService { private readonly IEventBus _eventBus; public NotificationService(IEventBus eventBus) { _eventBus = eventBus; } public async Task NotifyAsync(OrderPlaced @event, CancellationToken cancellationToken) { await _eventBus.PublishAsync(@event, cancellationToken); } } ``` ## Dynamic subscriptions `IEventBus` supports runtime subscriptions that do not require DI registration. These are resolved via `ActivatorUtilities` at publish time: ```csharp eventBus.Subscribe(); // or auto-discover all ISubscriber interfaces on a handler type eventBus.SubscribeAllHandledEvents(); ``` ## API summary | Type | Description | |------|-------------| | `InMemoryEventBusBuilder` | Builder used with `WithEventHandling` to configure the in-memory bus | | `IEventBus` | In-process event bus; `PublishAsync`, `Subscribe`, `SubscribeAllHandledEvents` | | `InMemoryEventBus` | Default `IEventBus` implementation; resolves handlers from a DI scope | | `ISubscriber` | Handler interface; implement `HandleAsync` | | `PublishWithEventBusEventProducer` | `IEventProducer` that delegates to `IEventBus.PublishAsync` | | `IEventRouter` | Queues transactional events and routes them to registered producers | | `InMemoryTransactionalEventRouter` | Default `IEventRouter`; registered automatically by `AddRCommon` | | `ISyncEvent` | Marker; selects **sequential** dispatch ordering (not a transport choice — still fully serializable / outbox-eligible) | | `IAsyncEvent` | Marker; selects **concurrent** dispatch ordering (not a transport choice) | ### `AddSubscriber` / `AddProducer` overloads | Method | Description | |---|---| | `AddSubscriber()` | Registers `TEventHandler` as a scoped `ISubscriber`; auto-registers `PublishWithEventBusEventProducer`. | | `AddSubscriber(Func)` | Same as above, constructing the handler via a factory delegate. | | `AddProducer()` | Registers `T` as a singleton `IEventProducer`, deduplicated by concrete type. | | `AddProducer(Func)` | Same as above, constructing the producer via a factory delegate. | | `AddProducer(T producer)` | Registers a pre-built producer instance. Also registers it as `IHostedService` if `producer` implements that interface. | --- ## MassTransit Source: https://rcommon.com/docs/event-handling/masstransit Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers. # MassTransit RCommon integrates with MassTransit to deliver events to consumers across service boundaries. The integration uses `MassTransitEventHandler` as a MassTransit `IConsumer` that delegates to the application's `ISubscriber` implementation. Application handler code has no dependency on MassTransit types. ## Installation ## Configuration Use `WithEventHandling` on the RCommon builder. The builder inherits from MassTransit's `ServiceCollectionBusConfigurator`, so all standard MassTransit configuration APIs are available directly on it: ```csharp using RCommon; using RCommon.MassTransit; using RCommon.MassTransit.Producers; builder.Services.AddRCommon() .WithEventHandling(mt => { // Register the producer that publishes events to the broker mt.AddProducer(); // Register subscribers; each call also adds a MassTransit consumer mt.AddSubscriber(); mt.AddSubscriber(); // Standard MassTransit transport configuration mt.UsingRabbitMq((ctx, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(ctx); }); }); ``` `AddSubscriber` performs three registrations: 1. Registers `ISubscriber` in the DI container as transient. 2. Calls `AddConsumer>` so MassTransit creates a consumer for the endpoint. 3. Records the event-to-producer association in the `EventSubscriptionManager` so the router routes this event only to the MassTransit producer. :::tip Modular composition `WithEventHandling` participates in the cache-aware sub-builder contract: a second call with the same builder type reuses the cached `MassTransitEventHandlingBuilder`, and subscriber/producer registrations from each module accumulate against the one instance. `AddProducer` deduplicates by concrete producer type — exactly one descriptor per producer. **Known limitation — cross-module composition.** MassTransit's pre-existing `IBus` registration guard means a second module that calls `WithEventHandling` will throw `MassTransit.ConfigurationException`. Single-module usage is unchanged. Tracked as a follow-up; for now, funnel MassTransit wiring through a single module and let other modules contribute subscribers via the shared cached builder accessed through `IRCommonBuilder.GetOrAddBuilder()`. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Defining an event Events must implement `ISerializableEvent` and must have a parameterless constructor for broker deserialization: ```csharp using RCommon.Models.Events; public class OrderShipped : ISyncEvent { public OrderShipped(Guid orderId, string trackingNumber) { OrderId = orderId; TrackingNumber = trackingNumber; } public OrderShipped() { } public Guid OrderId { get; } public string TrackingNumber { get; } = string.Empty; } ``` ## Implementing a subscriber Implement `ISubscriber`. No MassTransit types appear in handler code: ```csharp using RCommon.EventHandling.Subscribers; public class OrderShippedHandler : ISubscriber { private readonly ILogger _logger; public OrderShippedHandler(ILogger logger) { _logger = logger; } public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default) { _logger.LogInformation( "Order {OrderId} shipped with tracking {TrackingNumber}", @event.OrderId, @event.TrackingNumber); await Task.CompletedTask; } } ``` ## Publishing events Call `IEventRouter.RouteEventsAsync` after adding transactional events. The router forwards each event to `PublishWithMassTransitEventProducer`, which calls `IBus.Publish`: ```csharp public class ShippingService { private readonly IEventRouter _eventRouter; public ShippingService(IEventRouter eventRouter) { _eventRouter = eventRouter; } public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken) { // ... update order state ... _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber)); await _eventRouter.RouteEventsAsync(cancellationToken); } } ``` ## Publish vs Send Register `SendWithMassTransitEventProducer` instead of (or in addition to) `PublishWithMassTransitEventProducer` when you want point-to-point delivery to a single consumer endpoint: ```csharp mt.AddProducer(); ``` `SendWithMassTransitEventProducer` calls `IBus.Send` internally. It is appropriate for command-style messages where only one consumer should process the event. ## Transactional outbox Pair MassTransit with the outbox pattern to guarantee at-least-once delivery. See [Transactional Outbox](./transactional-outbox.mdx#masstransit-outbox). ## How the consumer bridge works `MassTransitEventHandler` implements both `IMassTransitEventHandler` and MassTransit's `IConsumer`. When MassTransit delivers a message, it calls `Consume(ConsumeContext)`, which resolves `ISubscriber` from DI and calls `HandleAsync`. This keeps application handler code free of any MassTransit API surface. ```csharp // Framework code — you do not write this yourself public class MassTransitEventHandler : IConsumer where TEvent : class, ISerializableEvent { public async Task Consume(ConsumeContext context) { await _subscriber.HandleAsync(context.Message, context.CancellationToken); } } ``` ## API summary | Type | Description | |------|-------------| | `MassTransitEventHandlingBuilder` | Builder used with `WithEventHandling`; inherits `ServiceCollectionBusConfigurator` | | `IMassTransitEventHandlingBuilder` | Interface for the MassTransit event handling builder | | `PublishWithMassTransitEventProducer` | `IEventProducer` that calls `IBus.Publish` (fan-out) | | `SendWithMassTransitEventProducer` | `IEventProducer` that calls `IBus.Send` (point-to-point) | | `MassTransitEventHandler` | Internal `IConsumer` that bridges MassTransit to `ISubscriber` | | `IMassTransitEventHandler` | Marker interface for MassTransit event handlers | --- ## MediatR Source: https://rcommon.com/docs/event-handling/mediatr Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals. # MediatR RCommon integrates with MediatR to route events through the MediatR notification pipeline. Each event is wrapped in a `MediatRNotification` and published to all registered `INotificationHandler` instances. From the application's perspective, handlers still implement `ISubscriber` — the MediatR plumbing is hidden behind the abstraction. ## Installation ## Configuration Use `WithEventHandling` on the RCommon builder. Register a producer and one subscriber per event type: ```csharp using RCommon; using RCommon.MediatR; using RCommon.MediatR.Producers; builder.Services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }); ``` :::tip Modular composition `WithEventHandling` is cache-aware. When multiple modules call it, the cached `MediatREventHandlingBuilder` is reused and each configuration delegate runs against the same instance — subscriber and producer registrations from every module accumulate. `AddProducer` deduplicates by concrete producer type: registering `PublishWithMediatREventProducer` from both `OrderingModule` and `NotificationsModule` results in a single descriptor. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: `AddSubscriber` does three things internally: 1. Registers `ISubscriber` with the DI container. 2. Registers a `MediatREventHandler>` as an `INotificationHandler` so MediatR can dispatch the notification. 3. Records the event-to-producer association in the `EventSubscriptionManager` so the router routes this event only to the MediatR producer. ### Custom MediatR service configuration If you need to control which assemblies MediatR scans for handlers, use the three-parameter overload: ```csharp builder.Services.AddRCommon() .WithEventHandling( eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }, mediatR => { mediatR.RegisterServicesFromAssembly(typeof(Program).Assembly); }); ``` ## Defining an event Events must implement `ISerializableEvent`. The `ISyncEvent` and `IAsyncEvent` markers control dispatch ordering through the `IEventRouter`: ```csharp using RCommon.Models.Events; public class OrderPlaced : ISyncEvent { public OrderPlaced(Guid orderId, DateTime placedAt) { OrderId = orderId; PlacedAt = placedAt; } public OrderPlaced() { } public Guid OrderId { get; } public DateTime PlacedAt { get; } } ``` ## Implementing a subscriber Implement `ISubscriber`. MediatR delivery is transparent: ```csharp using RCommon.EventHandling.Subscribers; public class OrderPlacedHandler : ISubscriber { private readonly ILogger _logger; public OrderPlacedHandler(ILogger logger) { _logger = logger; } public async Task HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default) { _logger.LogInformation("Order {OrderId} placed at {PlacedAt}", @event.OrderId, @event.PlacedAt); await Task.CompletedTask; } } ``` ## Publishing events Produce events through `IEventRouter` or `IEventProducer`. The producer wraps each event in a `MediatRNotification` and calls `IMediatorService.Publish`, which dispatches to all registered notification handlers: ```csharp public class OrderService { private readonly IEventRouter _eventRouter; public OrderService(IEventRouter eventRouter) { _eventRouter = eventRouter; } public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken) { // ... persist the order ... _eventRouter.AddTransactionalEvent(new OrderPlaced(order.Id, DateTime.UtcNow)); await _eventRouter.RouteEventsAsync(cancellationToken); } } ``` ## How MediatR is wired Internally, `PublishWithMediatREventProducer` calls `IMediatorService.Publish(@event, cancellationToken)`. The mediator service wraps the event in a `MediatRNotification` and dispatches it through MediatR's notification pipeline. `MediatREventHandler` receives the notification and resolves `ISubscriber` from the DI container to call `HandleAsync`. This means MediatR pipeline behaviours (logging, validation, unit-of-work) apply to event handlers just as they apply to command and query handlers, giving you a consistent cross-cutting-concerns story. ## API summary | Type | Description | |------|-------------| | `MediatREventHandlingBuilder` | Builder used with `WithEventHandling` to configure MediatR event handling | | `IMediatREventHandlingBuilder` | Interface for the MediatR event handling builder | | `PublishWithMediatREventProducer` | `IEventProducer` that publishes via `IMediatorService.Publish` (fan-out) | | `SendWithMediatREventProducer` | `IEventProducer` that sends via `IMediatorService.Send` (point-to-point) | | `MediatREventHandler` | Internal `INotificationHandler` that bridges MediatR to `ISubscriber` | | `MediatRNotification` | MediatR `INotification` wrapper used to carry events through the pipeline | --- ## outbox-producer-processor-topology Source: https://rcommon.com/docs/event-handling/outbox-producer-processor-topology --- title: Outbox Producer/Processor Topology sidebar_position: 8 description: How RCommon's built-in persistence outbox dispatches and marks events across multiple hosts, and why producer-only hosts must set OutboxOptions.ImmediateDispatch = false for reliable cross-host delivery. --- # Outbox Producer/Processor Topology This guide covers RCommon's **built-in persistence outbox** — the one you register with `AddOutbox` on a persistence builder (EF Core, Dapper, or LINQ to DB). It is distinct from the [transport-integrated outbox](./transactional-outbox.mdx) provided by MassTransit and Wolverine. The built-in outbox stores events in a database table (`__OutboxMessages`) and drains them with an in-process background poller (`OutboxProcessingService`). If you run the producer and every subscriber in a **single process**, the defaults are correct and you can skip most of this page. If events produced on one host are consumed on **another** host, read the [cross-host invariant](#the-cross-host-invariant) carefully — the default behaviour will silently fail to deliver. :::tip Runnable example See `Examples/EventHandling/Examples.EventHandling.Outbox/` for a complete console app exercising the single-host, default-`ImmediateDispatch` happy path end to end: committing a `UnitOfWork` persists the outbox row, dispatches it to a subscriber, and marks it processed. ::: ## How commit dispatches events (three phases) When you commit a `UnitOfWork` that has domain events, `CommitAsync` runs three phases: 1. **Persist (inside the transaction).** Each event is serialized and written as a row to the outbox table in the same transaction as your business data. If the transaction rolls back, the outbox row rolls back with it. 2. **Commit.** The transaction commits, making both the business data and the outbox rows durable together. 3. **Immediate dispatch (best-effort, post-commit).** The `OutboxEventRouter` dispatches the just-persisted events to any in-process `IEventProducer` instances and, on success, marks each row processed (`ProcessedAtUtc` is set). Failures here are swallowed and logged — the row stays unprocessed for the poller to retry. Separately, the background poller (`OutboxProcessingService`) claims unprocessed rows — `ClaimAsync` filters `WHERE ProcessedAtUtc IS NULL` — dispatches them, and marks them processed. Phase 3 and the poller are two paths to the same outcome; the poller is the durable one. ## The cross-host invariant Phase 3 runs **on the host that committed the transaction**. It marks rows processed whenever the in-process dispatch loop does not throw — it is *not* gated on any subscriber actually consuming the event. The in-memory event bus is subscription-filtered and silently does nothing when no matching subscriber lives in that process. So on a host that produces an event but hosts none of its subscribers, Phase 3 still completes, still marks the row processed, and the poller — which only claims `ProcessedAtUtc IS NULL` rows — **never sees it**. Cross-host delivery is silently defeated. Note that "only mark the row processed if a subscriber handled it" does **not** fix this. No single host knows the full cross-process subscriber set. If a logging subscriber lives on the producer host and the business subscriber lives on the poller host, the producer would see "a subscriber ran," mark the row processed, and still starve the remote subscriber. The correct model: **when a durable poller is the delivery mechanism, the poller must be the sole dispatcher-and-marker.** Producer-only hosts must persist without running Phase 3. > **Invariant:** If any subscriber for an event type runs on a different process than the producer, that producer must run with `OutboxOptions.ImmediateDispatch = false`, or the poller will never see the row. ## `OutboxOptions.ImmediateDispatch` `ImmediateDispatch` (default `true`) controls whether Phase 3 runs: | Value | Behaviour | Use on | |-------|-----------|--------| | `true` (default) | Phase 3 runs: immediate in-process dispatch + mark processed after commit. Preserves single-host, low-latency delivery. | Single-process apps, and the host that runs the poller. | | `false` | Phase 3 is skipped entirely: no in-process dispatch, no `MarkProcessedAsync`. Rows stay `ProcessedAtUtc IS NULL` for the poller to drain. | Producer-only hosts in a multi-host topology. | ```csharp // Producer-only host: persist to the outbox, but let the poller host dispatch and mark. builder.Services.AddRCommon() .WithPersistence(db => { // ... data store configuration ... db.AddOutbox(outbox => { outbox.ImmediateDispatch = false; }); }); ``` ```csharp // Poller host: leave ImmediateDispatch at its default (true). It dispatches and marks // rows as its OutboxProcessingService drains them — including rows written by producer hosts. db.AddOutbox(); ``` Same-host subscribers are still delivered when `ImmediateDispatch = false`: the poller shares the database and dispatches every unprocessed row exactly once, so a subscriber co-located with the poller fires normally. ## Secondary hardening: no in-process producer As defense-in-depth, Phase 3 also skips `MarkProcessedAsync` for a message when there is **no matching in-process producer at all** — the row is left unprocessed for the poller rather than being marked processed after being dispatched to nobody. This is a narrow guard, **not** the cross-host fix. An in-process producer that no-ops when no subscriber matches (like the in-memory bus) still counts as a producer here, so the row would still be marked processed. Rely on `ImmediateDispatch = false` for cross-host correctness; this guard only helps when a host has literally no producer registered for the event. ## Who marks a row processed? | Scenario | Marked processed by | When | |----------|---------------------|------| | Single host, `ImmediateDispatch = true` (default) | Phase 3, on the committing host | Immediately after commit | | Poller host, `ImmediateDispatch = true` | Phase 3 for its own writes; the poller for anything still unprocessed | After commit / on the next poll | | Producer-only host, `ImmediateDispatch = false` | The poller on the processor host | On the next poll | ## How the poller resolves subscribers The poller dispatches through the in-process producer, which publishes to the in-memory bus. The bus resolves **all** `ISubscriber` registrations from the DI container for the event's runtime type. That means a subscriber registered directly with `services.AddScoped, THandler>()` is resolved by the poller just as one registered through an event-handling builder's `AddSubscriber()`. Because the poller is the terminal dispatcher, **every** event type that flows through the outbox must have its subscriber registered on the poller (processor) host. A common mistake with `ImmediateDispatch = false` is registering a subscriber only on the producer host: the poller drains the row, finds no matching subscriber, marks it processed, and the event is silently dropped. The poller now logs a **Warning once per event type** in this case: > Outbox poller drained event type `{EventType}` but no matching subscriber/producer is registered on this host; the message will be marked processed and not delivered. Ensure the subscriber for this event type is registered on the poller (processor) host. ## Outbox routing must not be overridden `AddOutbox` binds `IEventRouter` to the outbox router. Because DI is last-registration-wins, an event-handling registration applied *after* `AddOutbox` can silently rebind `IEventRouter` to the in-memory router, which defeats the outbox entirely — events would fire in-process and never be persisted. To catch this, `AddOutbox` registers a startup diagnostic that logs a **Warning** when the effective `IEventRouter` is the in-memory router: > RCommon outbox is configured (AddOutbox) but the effective IEventRouter is the in-memory router; a later registration overrode the outbox router. Domain events will be dispatched in-memory and NOT persisted to the outbox. Register AddOutbox after any event-handling configuration that binds IEventRouter, or re-assert the outbox router last. If you see this warning, ensure `AddOutbox(...)` runs **after** any `WithEventHandling<...>` configuration that binds `IEventRouter`. ## Related - [Transactional Outbox](./transactional-outbox.mdx) — the MassTransit/Wolverine transport-integrated outbox (different mechanism) - [Distributed Events](./distributed.mdx) — routing events across brokers --- ## Overview Source: https://rcommon.com/docs/event-handling/overview Understand RCommon's unified event handling pipeline — IEventBus, IEventProducer, IEventRouter, and ISubscriber — and how to choose between in-process and broker transports. # Event Handling Overview RCommon provides a unified event handling abstraction that works across multiple transport backends. The same `ISubscriber` interface is used whether events are dispatched in-process via the built-in event bus, fanned out through MediatR notifications, or routed over a message broker using MassTransit or Wolverine. ## Core abstractions ### IEventBus `IEventBus` is the in-process event bus. It publishes events to all registered `ISubscriber` implementations within the current process. ```csharp public interface IEventBus { Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default); IEventBus Subscribe() where TEvent : class where TEventHandler : class, ISubscriber; IEventBus SubscribeAllHandledEvents() where TEventHandler : class; } ``` ### ISubscriber<TEvent> Every event handler in RCommon implements this single interface regardless of transport: ```csharp public interface ISubscriber { Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); } ``` ### IEventProducer An `IEventProducer` is responsible for dispatching a serializable event to its destination. Multiple producers can be registered simultaneously; the `EventSubscriptionManager` routes each event only to the producers that have a matching subscription. ```csharp public interface IEventProducer { Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : ISerializableEvent; } ``` ### IEventRouter `IEventRouter` acts as a coordination layer between application code and `IEventProducer` instances. It holds a transactional queue of events and dispatches them when the application signals it is ready: ```csharp public interface IEventRouter { void AddTransactionalEvent(ISerializableEvent serializableEvent); void AddTransactionalEvents(IEnumerable serializableEvents); Task RouteEventsAsync(CancellationToken cancellationToken = default); Task RouteEventsAsync(IEnumerable transactionalEvents, CancellationToken cancellationToken = default); } ``` ## Event type markers Events that flow through `IEventProducer` must implement `ISerializableEvent`. Two sub-markers control dispatch ordering: | Interface | Behaviour | |-----------|-----------| | `ISyncEvent` | Events are produced sequentially, one after the other. | | `IAsyncEvent` | Events are produced concurrently via `Task.WhenAll`. | Events that implement neither marker are treated as synchronous by default. ## Choosing an approach | Scenario | Recommended approach | |----------|---------------------| | In-process publish/subscribe within a single application | In-memory event bus (`InMemoryEventBusBuilder`) | | In-process fan-out where MediatR is already the mediator | MediatR (`MediatREventHandlingBuilder`) | | Cross-service messaging with a message broker | MassTransit (`MassTransitEventHandlingBuilder`) or Wolverine (`WolverineEventHandlingBuilder`) | | Guaranteed at-least-once delivery with a broker | MassTransit + outbox or Wolverine + outbox | ## How the pipeline fits together 1. Application code raises an event by calling `IEventRouter.AddTransactionalEvent` or by using `IEventBus.PublishAsync` directly. 2. At an appropriate commit boundary (e.g. after a database transaction completes), `IEventRouter.RouteEventsAsync` is called. 3. The router inspects each event and forwards it only to the `IEventProducer` instances that are subscribed to that event type, as tracked by the internal `EventSubscriptionManager`. 4. Each producer dispatches the event to its transport: an in-process bus, MediatR pipeline, or an external message broker. 5. The transport delivers the event to all registered `ISubscriber` handlers. This layered design lets you mix transports in a single application. For example, one event type can go to the in-memory bus for local side-effects while another goes to MassTransit for cross-service delivery, all sharing the same handler interface. --- ## Transactional Outbox Source: https://rcommon.com/docs/event-handling/transactional-outbox Guarantee at-least-once event delivery using the transactional outbox pattern with MassTransit EF Core outbox or Wolverine's EF Core transaction integration. # Transactional Outbox The transactional outbox pattern guarantees that events are published to a message broker if and only if the database transaction that triggered them commits. RCommon provides outbox integration for both MassTransit and Wolverine. ## The problem the outbox solves Without an outbox, there is a window between committing a database change and publishing an event where a crash can cause the event to be lost. Conversely, publishing an event before committing risks publishing for a transaction that is later rolled back. The outbox eliminates both failure modes by writing the event to the same database transaction as the business data, then relaying it to the broker after the transaction commits. ## MassTransit outbox ### Installation MassTransit's Entity Framework Core outbox stores pending messages in the same `DbContext` as your application data. The outbox tables are added to any `DbContext` that you supply. ### Configuration Call `AddOutbox` on the `IMassTransitEventHandlingBuilder` returned by `WithEventHandling`: ```csharp using RCommon; using RCommon.MassTransit; builder.Services.AddRCommon() .WithEventHandling(mt => { mt.AddProducer(); mt.AddSubscriber(); // Configure the transactional outbox backed by AppDbContext mt.AddOutbox(outbox => { outbox.UseSqlServer(); // or outbox.UsePostgres() outbox.UseBusOutbox(); }); mt.UsingRabbitMq((ctx, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(ctx); }); }); ``` ### IMassTransitOutboxBuilder members | Method | Description | |--------|-------------| | `UseSqlServer()` | Configures the outbox to use SQL Server dialect | | `UsePostgres()` | Configures the outbox to use PostgreSQL dialect | | `UseBusOutbox(configure?)` | Enables the bus outbox relay; accepts an optional `IBusOutboxConfigurator` action | ### Applying outbox migrations The MassTransit outbox adds its own tables to the `DbContext`. Add a migration after configuring the outbox: ```bash dotnet ef migrations add AddMassTransitOutbox dotnet ef database update ``` ## Wolverine outbox ### Installation Wolverine's outbox integrates with Entity Framework Core transactions, persisting messages alongside application data in the same transaction scope. ### Configuration Call `AddOutbox` on the `IWolverineEventHandlingBuilder`: ```csharp using RCommon; using RCommon.Wolverine; builder.Host.UseWolverine(); builder.Services.AddRCommon() .WithEventHandling(wlv => { wlv.AddProducer(); wlv.AddSubscriber(); wlv.AddOutbox(outbox => { outbox.UseEntityFrameworkCoreTransactions(); }); }); ``` ### IWolverineOutboxBuilder members | Method | Description | |--------|-------------| | `UseEntityFrameworkCoreTransactions()` | Integrates Wolverine's outbox with EF Core transaction management | ## How delivery works Once the outbox is configured: 1. When `IEventRouter.RouteEventsAsync` is called, the producer writes the message to the outbox table inside the current database transaction rather than sending it directly to the broker. 2. After the transaction commits, a background relay process (managed by MassTransit or Wolverine) reads pending messages from the outbox table and forwards them to the broker. 3. Each message is deleted from the outbox only after the broker acknowledges receipt. This gives at-least-once delivery semantics. Consumers must be idempotent to handle the rare case of duplicate delivery after a relay failure. ## API summary ### MassTransit outbox | Type | Package | Description | |------|---------|-------------| | `MassTransitOutboxBuilderExtensions.AddOutbox` | `RCommon.MassTransit.Outbox` | Registers the EF Core outbox for the given `DbContext` | | `IMassTransitOutboxBuilder` | `RCommon.MassTransit.Outbox` | Builder for outbox dialect and bus relay options | ### Wolverine outbox | Type | Package | Description | |------|---------|-------------| | `WolverineOutboxBuilderExtensions.AddOutbox` | `RCommon.Wolverine.Outbox` | Registers Wolverine's outbox via `ConfigureWolverine` | | `IWolverineOutboxBuilder` | `RCommon.Wolverine.Outbox` | Builder for EF Core transaction integration | --- ## Wolverine Source: https://rcommon.com/docs/event-handling/wolverine Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send. # Wolverine RCommon integrates with Wolverine to deliver events both in-process and across service boundaries. The integration registers `WolverineEventHandler` as a Wolverine `IWolverineHandler` that delegates to the application's `ISubscriber`. Application handler code has no dependency on Wolverine types. ## Installation Wolverine requires host-level registration via `UseWolverine()` in addition to the RCommon service configuration: ```csharp builder.Host.UseWolverine(opts => { // Wolverine transport and endpoint options }); ``` ## Configuration Use `WithEventHandling` on the RCommon builder: ```csharp using RCommon; using RCommon.Wolverine; using RCommon.Wolverine.Producers; builder.Services.AddRCommon() .WithEventHandling(wlv => { // Register the producer that publishes events via Wolverine wlv.AddProducer(); // Register subscribers and record routing information wlv.AddSubscriber(); wlv.AddSubscriber(); }); ``` `AddSubscriber` registers `ISubscriber` as scoped in the DI container and records the event-to-producer association in the `EventSubscriptionManager` so the router routes this event only to Wolverine producers. :::tip Modular composition `WithEventHandling` is a cache-aware sub-builder verb. When multiple modules call it, the cached `WolverineEventHandlingBuilder` is reused and each module's `AddSubscriber` and `AddProducer` calls accumulate on the same instance. `AddProducer` deduplicates by concrete producer type — registering `PublishWithWolverineEventProducer` from two modules yields one descriptor. The host-level `builder.Host.UseWolverine(...)` registration must remain in the application host's composition root; only the RCommon sub-builder participates in module-level composition. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Defining an event Events must implement `ISerializableEvent`. Include a parameterless constructor for Wolverine deserialization: ```csharp using RCommon.Models.Events; public class OrderShipped : ISyncEvent { public OrderShipped(Guid orderId, string trackingNumber) { OrderId = orderId; TrackingNumber = trackingNumber; } public OrderShipped() { } public Guid OrderId { get; } public string TrackingNumber { get; } = string.Empty; } ``` ## Implementing a subscriber Implement `ISubscriber`. No Wolverine types appear in handler code: ```csharp using RCommon.EventHandling.Subscribers; public class OrderShippedHandler : ISubscriber { private readonly ILogger _logger; public OrderShippedHandler(ILogger logger) { _logger = logger; } public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default) { _logger.LogInformation( "Order {OrderId} shipped with tracking {TrackingNumber}", @event.OrderId, @event.TrackingNumber); await Task.CompletedTask; } } ``` ## Publishing events Call `IEventRouter.RouteEventsAsync` after queuing events. The router forwards each event to `PublishWithWolverineEventProducer`, which calls `IMessageBus.PublishAsync`: ```csharp public class ShippingService { private readonly IEventRouter _eventRouter; public ShippingService(IEventRouter eventRouter) { _eventRouter = eventRouter; } public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken) { // ... update order state ... _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber)); await _eventRouter.RouteEventsAsync(cancellationToken); } } ``` ## Publish vs Send Register `SendWithWolverineEventProducer` instead of (or in addition to) `PublishWithWolverineEventProducer` when you want point-to-point delivery to a single handler endpoint: ```csharp wlv.AddProducer(); ``` `SendWithWolverineEventProducer` calls `IMessageBus.SendAsync` internally. ## Transactional outbox Pair Wolverine with the outbox pattern to guarantee at-least-once delivery. See [Transactional Outbox](./transactional-outbox.mdx#wolverine-outbox). ## How the handler bridge works `WolverineEventHandler` implements both `IWolverineEventHandler` and Wolverine's `IWolverineHandler`. When Wolverine delivers a message it calls `HandleAsync(TEvent, CancellationToken)`, which delegates to the registered `ISubscriber`: ```csharp // Framework code — you do not write this yourself public class WolverineEventHandler : IWolverineHandler where TEvent : class, ISerializableEvent { public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default) { await _subscriber.HandleAsync(@event, cancellationToken); } } ``` This keeps application handler code free of any Wolverine API surface. ## API summary | Type | Description | |------|-------------| | `WolverineEventHandlingBuilder` | Builder used with `WithEventHandling` to configure Wolverine event handling | | `IWolverineEventHandlingBuilder` | Interface for the Wolverine event handling builder | | `PublishWithWolverineEventProducer` | `IEventProducer` that calls `IMessageBus.PublishAsync` (fan-out) | | `SendWithWolverineEventProducer` | `IEventProducer` that calls `IMessageBus.SendAsync` (point-to-point) | | `WolverineEventHandler` | Internal `IWolverineHandler` that bridges Wolverine to `ISubscriber` | | `IWolverineEventHandler` | Marker interface for Wolverine event handlers | --- ## caching Source: https://rcommon.com/docs/examples-recipes/caching --- title: Caching Examples sidebar_position: 3 description: Practical RCommon caching examples covering IMemoryCache, distributed memory cache, Redis, and ICachingGraphRepository for caching EF Core repository query results. --- # Caching Examples This page walks through the three caching examples included with RCommon: in-process memory cache, distributed memory cache, Redis distributed cache, and the persistence caching layer that caches repository query results. ## Example Projects ``` Examples/Caching/ Examples.Caching.MemoryCaching/ — IMemoryCache + IDistributedCache (in-memory) Examples.Caching.RedisCaching/ — IDistributedCache backed by Redis Examples.Caching.PersistenceCaching/ — ICachingGraphRepository with EF Core ``` ## Example 1: In-Memory and Distributed Memory Caching ### Installation ### Configuration ```csharp services.AddRCommon() // Serialization is required for distributed cache (values are serialized to bytes) .WithJsonSerialization() .WithMemoryCaching(cache => { cache.Configure(x => { x.ExpirationScanFrequency = TimeSpan.FromMinutes(1); }); // Optionally cache dynamically compiled LINQ expression trees cache.CacheDynamicallyCompiledExpressions(); }) .WithDistributedCaching(cache => { cache.Configure(x => { x.ExpirationScanFrequency = TimeSpan.FromMinutes(1); }); }); ``` `WithMemoryCaching` registers `IMemoryCache` backed by `Microsoft.Extensions.Caching.Memory`. `WithDistributedCaching` registers `IDistributedCache` backed by an in-process distributed cache (useful for development and single-node deployments). ### Application Service Inject `IMemoryCache` and `IDistributedCache` as you would in any .NET application. RCommon's `IJsonSerializer` handles serialization for the distributed cache: ```csharp public class TestApplicationService : ITestApplicationService { private readonly IMemoryCache _memoryCache; private readonly IDistributedCache _distributedCache; private readonly IJsonSerializer _serializer; public TestApplicationService( IMemoryCache memoryCache, IDistributedCache distributedCache, IJsonSerializer serializer) { _memoryCache = memoryCache; _distributedCache = distributedCache; _serializer = serializer; } // IMemoryCache — objects stored by reference, in-process only public void SetMemoryCache(string key, TestDto data) => _memoryCache.Set(key, data); public TestDto GetMemoryCache(string key) => _memoryCache.Get(key); // IDistributedCache — objects serialized to bytes, transport-agnostic public void SetDistributedMemoryCache(string key, Type type, object data) => _distributedCache.Set(key, Encoding.UTF8.GetBytes(_serializer.Serialize(data, type))); public TestDto GetDistributedMemoryCache(string key) { var cache = _distributedCache.Get(key); return _serializer.Deserialize(Encoding.UTF8.GetString(cache)); } } ``` ### Usage ```csharp var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddRCommon() .WithJsonSerialization() .WithMemoryCaching(cache => { cache.Configure(x => { x.ExpirationScanFrequency = TimeSpan.FromMinutes(1); }); }) .WithDistributedCaching(cache => { cache.Configure(x => { x.ExpirationScanFrequency = TimeSpan.FromMinutes(1); }); }); services.AddTransient(); }).Build(); var appService = host.Services.GetRequiredService(); // Store and retrieve from IMemoryCache appService.SetMemoryCache("test-key", new TestDto("test data 1")); var testData1 = appService.GetMemoryCache("test-key"); // Store and retrieve from IDistributedCache appService.SetDistributedMemoryCache("test-key", typeof(TestDto), new TestDto("test data 2")); var testData2 = appService.GetDistributedMemoryCache("test-key"); Console.WriteLine(testData1.Message); // test data 1 Console.WriteLine(testData2.Message); // test data 2 ``` ## Example 2: Redis Distributed Caching ### Installation ### Configuration Swap `DistributedMemoryCacheBuilder` for `RedisCachingBuilder`. The application service code does not change: ```csharp services.AddRCommon() .WithJsonSerialization() .WithDistributedCaching(cache => { cache.Configure(redis => { // StackExchange.Redis ConfigurationOptions redis.ConfigurationOptions = new ConfigurationOptions { EndPoints = { "localhost:6379" }, AbortOnConnectFail = false }; }); }); ``` ### Application Service (unchanged) The application service injects `IDistributedCache` and `IJsonSerializer`. The same code that worked against the in-memory distributed cache works against Redis: ```csharp public class TestApplicationService : ITestApplicationService { private readonly IDistributedCache _distributedCache; private readonly IJsonSerializer _serializer; public TestApplicationService( IDistributedCache distributedCache, IJsonSerializer serializer) { _distributedCache = distributedCache; _serializer = serializer; } public void SetDistributedMemoryCache(string key, Type type, object data) => _distributedCache.Set(key, Encoding.UTF8.GetBytes(_serializer.Serialize(data, type))); public TestDto GetDistributedMemoryCache(string key) { var cache = _distributedCache.Get(key); return _serializer.Deserialize(Encoding.UTF8.GetString(cache)); } } ``` ### Usage ```csharp var appService = host.Services.GetRequiredService(); appService.SetDistributedMemoryCache("test-key", typeof(TestDto), new TestDto("test data 1")); var testData1 = appService.GetDistributedMemoryCache("test-key"); Console.WriteLine(testData1.Message); // test data 1 ``` ## Example 3: Persistence Caching Persistence caching wraps `IGraphRepository` with a caching layer so that repeated queries return cached results instead of hitting the database. ### Installation ### Configuration Add `AddInMemoryPersistenceCaching()` to the EF Core persistence builder. This registers `ICachingGraphRepository` implementations backed by `IMemoryCache`: ```csharp services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext("TestDbContext", options => { options.UseSqlServer(config.GetConnectionString("TestDbContext")); }); ef.SetDefaultDataStore(dataStore => dataStore.DefaultDataStoreName = "TestDbContext"); // Registers ICachingGraphRepository ef.AddInMemoryPersistenceCaching(); }) .WithMemoryCaching(cache => { cache.Configure(x => { x.ExpirationScanFrequency = TimeSpan.FromMinutes(1); }); }); ``` ### Application Service Instead of `IGraphRepository`, inject `ICachingGraphRepository`. The `FindAsync` overload accepts a cache key as the first argument: ```csharp public class TestApplicationService : ITestApplicationService { private readonly ICachingGraphRepository _customerRepository; public TestApplicationService(ICachingGraphRepository customerRepository) { _customerRepository = customerRepository; _customerRepository.DataStoreName = "TestDbContext"; } public async Task> GetCustomers(object cacheKey) { // First call: hits the database and stores result under cacheKey // Subsequent calls with the same cacheKey: returns cached result return await _customerRepository.FindAsync(cacheKey, x => x.LastName == "Potter"); } } ``` ### Usage ```csharp var appService = host.Services.GetRequiredService(); Console.WriteLine("Hitting the database w/ a query"); var customers = await appService.GetCustomers("my-test-key"); Console.WriteLine(customers); Console.WriteLine("Hitting the cache"); customers = await appService.GetCustomers("my-test-key"); Console.WriteLine(customers); // Second call returns from cache, no database roundtrip ``` ## Provider Comparison | Provider | Package | Scope | Serialization Required | Best For | |---------|---------|-------|----------------------|---------| | `InMemoryCachingBuilder` | `RCommon.MemoryCache` | Single process | No | Fast in-process caching | | `DistributedMemoryCacheBuilder` | `RCommon.MemoryCache` | Single process | Yes | Development, single-node | | `RedisCachingBuilder` | `RCommon.RedisCache` | Multi-process / distributed | Yes | Production distributed cache | | `AddInMemoryPersistenceCaching()` | `RCommon.Persistence.EFCore` | Single process | No | Caching repository query results | ## Key Design Points **Serialization for distributed caches.** `IDistributedCache` stores `byte[]`. RCommon uses `IJsonSerializer` to serialize and deserialize objects. Register a serializer with `WithJsonSerialization<>` before calling `WithDistributedCaching<>`. **Cache keys for persistence caching.** The cache key is a plain `object`. Use a meaningful, deterministic key that encodes the query parameters — for example, `"customers:potter"` rather than a GUID. The same key must be passed on subsequent calls to receive the cached result. **`CacheDynamicallyCompiledExpressions()`** caches compiled LINQ expression trees, which avoids recompilation overhead for frequently evaluated predicates. **Swapping providers.** The application service code depends only on `IMemoryCache`, `IDistributedCache`, `IJsonSerializer`, or `ICachingGraphRepository`. Switching from in-memory to Redis requires a one-line change in `Program.cs`. ## API Reference | Type | Package | Purpose | |------|---------|---------| | `InMemoryCachingBuilder` | `RCommon.MemoryCache` | Registers `IMemoryCache` | | `DistributedMemoryCacheBuilder` | `RCommon.MemoryCache` | Registers in-process `IDistributedCache` | | `RedisCachingBuilder` | `RCommon.RedisCache` | Registers Redis-backed `IDistributedCache` | | `ICachingGraphRepository` | `RCommon.Persistence` | Caching repository interface | | `IJsonSerializer` | `RCommon.Json` | Serialization abstraction | | `JsonNetBuilder` | `RCommon.Json.JsonNet` | Json.NET serializer registration | --- ## End-to-End DDD Recipe Source: https://rcommon.com/docs/examples-recipes/domain-driven-design A single runnable project walking through AggregateRoot, domain events, value objects, soft delete, and IMultiTenant together on one coherent Team/TeamMembership aggregate. # End-to-End DDD Recipe The five [Domain-Driven Design](../domain-driven-design/entities-aggregates.mdx) conceptual pages each introduce one building block in isolation, with its own small, disposable example. This recipe ties all of them together on a single, coherent aggregate — a `Team` that members can join — so you can see how the pieces actually compose in one running project rather than five unrelated snippets. **Source:** `Examples/DomainDrivenDesign/Examples.DomainDrivenDesign/` (project) and `Examples.DomainDrivenDesign.Tests/` (unit tests). ## The aggregate `Team` is the aggregate root. It brings together every building block this recipe covers: ```csharp public class Team : AggregateRoot, ISoftDelete, IMultiTenant { private readonly List _memberships = new(); public Team(string name) : base(Guid.NewGuid()) { Name = name; } public string Name { get; private set; } public ICollection Memberships => _memberships; public bool IsDeleted { get; set; } public string? TenantId { get; set; } public void AddMember(Guid userId, EmailAddress email, TeamRole role) { var membership = new TeamMembership(Id, userId, email, role); _memberships.Add(membership); AddDomainEvent(new TeamMemberAddedEvent(Id, userId, email)); } } ``` ### Aggregate roots and child entities — [Entities & Aggregate Roots](../domain-driven-design/entities-aggregates.mdx) `Team` derives from `AggregateRoot`, giving it identity, domain event tracking, and optimistic concurrency. `TeamMembership`, the child inside the aggregate boundary, derives from `BusinessEntity` rather than the lighter-weight `DomainEntity` you'll see in that page's own `OrderLine` example — deliberately, because this recipe's Pattern 2 (below) persists a membership through its own repository, which requires it to be a full `IBusinessEntity`, something `DomainEntity` does not implement. ### Raising and dispatching a domain event — [Domain Events](../domain-driven-design/domain-events.mdx) `AddMember` raises `TeamMemberAddedEvent` via `AddDomainEvent` — a state change and its side effect recorded together, not scattered across an application service. The event is only *dispatched* once the aggregate is persisted and a `UnitOfWork` is committed: ```csharp var team = new Team("Platform Engineering"); team.AddMember(userId, EmailAddress.Create("ada@example.com"), TeamRole.Lead); using var uow = unitOfWorkFactory.Create(); await teams.AddAsync(team); await uow.CommitAsync(); // TeamMemberAddedEvent is dispatched here, not before ``` A subscriber (`TeamMemberAddedEventHandler : ISubscriber`) registered via `WithEventHandling` receives it — simulating a welcome-email side effect kept entirely out of `Team` itself. ### A value object on the child entity — [Value Objects](../domain-driven-design/value-objects.mdx) `TeamMembership.Email` is an `EmailAddress`, a single-value wrapper value object with a validating factory and structural equality for free: ```csharp public record EmailAddress(string Value) : ValueObject(Value) { public static EmailAddress Create(string value) { if (string.IsNullOrWhiteSpace(value) || !value.Contains('@')) throw new ArgumentException($"'{value}' is not a valid email address.", nameof(value)); return new EmailAddress(value); } } ``` Two `EmailAddress` instances with the same underlying string are equal without any `Equals`/`GetHashCode` override — `ValueObject` is a `record`, so equality is structural by default. ## Persisting the aggregate — [Aggregate Repository](../persistence/aggregate-repository.mdx) The recipe persists `Team` through `IAggregateRepository`, demonstrating both fully-supported add/update-child patterns from that page, not just the common case: **Pattern 1 — load, mutate, update, in one scope (recommended):** ```csharp var team = await teams.Include(t => t.Memberships).GetByIdAsync(teamId); team!.AddMember(userId, EmailAddress.Create("grace@example.com"), TeamRole.Member); using var uow = unitOfWorkFactory.Create(); await teams.UpdateAsync(team); // new membership inserted; event queued await uow.CommitAsync(); // event dispatched ``` **Pattern 2 — cross-scope mutation** (a background worker or message handler operating on a `Team` it didn't load in this `DbContext` scope): ```csharp var team = await teams.GetByIdAsync(teamId); team!.AddMember(userId, EmailAddress.Create("alan@example.com"), TeamRole.Member); using var uow = unitOfWorkFactory.Create(); // UpdateAsync is deliberately not called -- persist the new child through its own repository. await writableMemberships.AddAsync(team.Memberships.Last()); // Required: nothing else registers `team` for event dispatch without UpdateAsync. eventTracker.AddEntity(team); await uow.CommitAsync(); ``` ## Soft delete — [Soft Delete](../domain-driven-design/soft-delete.mdx) `Team` implements `ISoftDelete`. Calling `DeleteAsync` sets `IsDeleted = true` and issues an `UPDATE` rather than physically removing the row — and `ExistsAsync` (like every other read path) filters soft-deleted rows out automatically: ```csharp await teams.DeleteAsync(team); await uow.CommitAsync(); team.IsDeleted; // true await teams.ExistsAsync(team.Id); // false -- soft-deleted rows are excluded from reads ``` ## Multi-tenancy, kept minimal — [Multi-Tenancy Overview](../multi-tenancy/overview.mdx) `Team` implements `IMultiTenant`, so `AddAsync` stamps `TenantId` automatically from whatever `ITenantIdAccessor` is registered. This recipe registers a trivial fixed-tenant accessor purely to show the stamping happen — it does not wire up `TenantScope.Bypass()` or Finbuckle. For the full tenant-resolution story (real request-scoped tenant resolution, bypassing tenant filtering for admin/bootstrap scenarios), see [Examples.MultiTenancy.Finbuckle](../multi-tenancy/finbuckle.mdx) instead of duplicating that wiring here. ## Running it ```shell dotnet run --project Examples/DomainDrivenDesign/Examples.DomainDrivenDesign ``` The console output walks through aggregate creation, both add/update-child patterns, soft delete, and the resulting subscriber invocation count. `Examples.DomainDrivenDesign.Tests` covers the pieces most worth unit-testing in isolation: the domain event raised on the right method call, `EmailAddress` equality and validation, and the `IsDeleted` flag's default/settable behavior. --- ## event-handling Source: https://rcommon.com/docs/examples-recipes/event-handling --- title: Event Handling Examples sidebar_position: 2 description: Step-by-step event handling examples for RCommon using InMemoryEventBus and MediatR providers with ISyncEvent, IEventProducer, and ISubscriber patterns. --- # Event Handling Examples This page walks through the event handling examples included with RCommon. Each example is a self-contained console application showing a different event handling provider. ## Example Projects ``` Examples/EventHandling/ Examples.EventHandling.InMemoryEventBus/ Examples.EventHandling.MediatR/ ``` ## Step 1: Define an Event An event is a plain class that implements `ISyncEvent`. It is immutable after construction: ```csharp using RCommon.Models.Events; public class TestEvent : ISyncEvent { public TestEvent() { } public TestEvent(DateTime dateTime, Guid guid) { DateTime = dateTime; Guid = guid; } public DateTime DateTime { get; } public Guid Guid { get; } } ``` `ISyncEvent` is the marker interface that RCommon's routing infrastructure uses to identify events. No base class is required. ## Step 2: Define a Subscriber A subscriber implements `ISubscriber` and contains the handling logic: ```csharp using RCommon.EventHandling.Subscribers; public class TestEventHandler : ISubscriber { public async Task HandleAsync(TestEvent notification, CancellationToken cancellationToken = default) { Console.WriteLine("Handled event: {0}", notification.ToString()); await Task.CompletedTask; } } ``` Subscribers are registered with a specific builder, which scopes their routing. ## Example 1: In-Memory Event Bus ### Installation ### Configuration ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` `WithEventHandling` registers an in-process bus. Events are dispatched synchronously within the same process with no external broker dependency. ### Publishing Resolve all registered `IEventProducer` instances and call `ProduceEventAsync`: ```csharp var eventProducers = host.Services.GetServices(); var testEvent = new TestEvent(DateTime.Now, Guid.NewGuid()); foreach (var producer in eventProducers) { await producer.ProduceEventAsync(testEvent); } ``` ### Full Program.cs ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using RCommon; using RCommon.EventHandling; using RCommon.EventHandling.Producers; var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); }).Build(); Console.WriteLine("Example Starting"); var eventProducers = host.Services.GetServices(); var testEvent = new TestEvent(DateTime.Now, Guid.NewGuid()); foreach (var producer in eventProducers) { await producer.ProduceEventAsync(testEvent); } Console.WriteLine("Example Complete"); ``` ### Expected Output ``` Example Starting Handled event: Examples.EventHandling.InMemoryEventBus.TestEvent Example Complete ``` ## Example 2: MediatR Event Bus ### Installation ### Configuration ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` `WithEventHandling` routes events through MediatR's `IPublisher`. This integrates with existing MediatR pipeline behaviors. ### Full Program.cs ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using RCommon; using RCommon.EventHandling.Producers; using RCommon.MediatR; using RCommon.MediatR.Producers; var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); }).Build(); Console.WriteLine("Example Starting"); var eventProducers = host.Services.GetServices(); var testEvent = new TestEvent(DateTime.Now, Guid.NewGuid()); foreach (var producer in eventProducers) { await producer.ProduceEventAsync(testEvent); } Console.WriteLine("Example Complete"); ``` ## Multiple Subscribers for the Same Event Register the same event type with multiple handlers. All handlers are invoked when the event is published: ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }); ``` ## Event Handling in an ASP.NET Core Application Wire event handling alongside the rest of your RCommon configuration: ```csharp // Program.cs — ASP.NET Core Web API builder.Services.AddRCommon() .WithMediator(mediator => { mediator.AddRequest(); mediator.AddUnitOfWorkToRequestPipeline(); }) .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }) .WithPersistence(ef => { ef.AddDbContext("AppDb", options => { options.UseSqlServer( builder.Configuration.GetConnectionString("AppDb")); }); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "AppDb"); }); ``` In a command handler, inject and use `IEnumerable`: ```csharp public class PlaceOrderCommandHandler : IAppRequestHandler { private readonly IGraphRepository _orderRepository; private readonly IEnumerable _eventProducers; public PlaceOrderCommandHandler( IGraphRepository orderRepository, IEnumerable eventProducers) { _orderRepository = orderRepository; _eventProducers = eventProducers; } public async Task HandleAsync( PlaceOrderCommand request, CancellationToken cancellationToken) { var order = new Order(request.CustomerId, request.Items); await _orderRepository.AddAsync(order); var @event = new OrderPlacedEvent(order.Id, order.CustomerId); foreach (var producer in _eventProducers) { await producer.ProduceEventAsync(@event); } return new BaseCommandResponse { Success = true, Id = order.Id }; } } ``` ## Testing an Event Handler Event handlers are plain classes and test without a running host: ```csharp [Test] public async Task Handler_Writes_To_Console_On_Event() { var handler = new TestEventHandler(); var @event = new TestEvent(DateTime.UtcNow, Guid.NewGuid()); // Should not throw await handler.HandleAsync(@event, CancellationToken.None); } ``` For handlers with dependencies, use mocks: ```csharp [Test] public async Task SendConfirmationEmailHandler_Calls_Email_Service() { var emailMock = new Mock(); var handler = new SendConfirmationEmailHandler(emailMock.Object); await handler.HandleAsync( new OrderPlacedEvent(Guid.NewGuid(), "customer-1"), CancellationToken.None); emailMock.Verify( x => x.SendAsync(It.Is(r => r.To == "customer-1")), Times.Once); } ``` ## Choosing Between In-Memory and MediatR | Aspect | InMemoryEventBusBuilder | MediatREventHandlingBuilder | |--------|-------------------------|-----------------------------| | External dependency | None beyond RCommon | MediatR NuGet package | | Pipeline behaviors | Not supported | Supported via MediatR behaviors | | Multi-publisher isolation | Yes | Yes | | Distributed messaging | No | No | | Best for | Simple in-process events | Projects already using MediatR | For distributed messaging across services, see the [Messaging Examples](./messaging) page. ## API Reference | Type | Package | Purpose | |------|---------|---------| | `ISyncEvent` | `RCommon.Models` | Base interface for all events | | `IEventProducer` | `RCommon.EventHandling` | Publishes events | | `ISubscriber` | `RCommon.EventHandling` | Handles events | | `InMemoryEventBusBuilder` | `RCommon.EventHandling` | Registers the in-process event bus | | `PublishWithEventBusEventProducer` | `RCommon.EventHandling` | In-memory producer | | `MediatREventHandlingBuilder` | `RCommon.MediatR` | Registers MediatR-backed event handling | | `PublishWithMediatREventProducer` | `RCommon.MediatR` | MediatR-backed producer | --- ## hr-leave-management Source: https://rcommon.com/docs/examples-recipes/hr-leave-management --- title: HR Leave Management Sample sidebar_position: 1 description: Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration. --- # HR Leave Management Sample The HR Leave Management sample is the reference application included with RCommon. It demonstrates Clean Architecture, CQRS with MediatR, EF Core persistence, FluentValidation, JWT authentication, and SendGrid email — all wired together through RCommon's fluent builder. This walkthrough explains how each piece fits together. ## Sample Location ``` Examples/CleanWithCQRS/ HR.LeaveManagement.Domain/ HR.LeaveManagement.Application/ HR.LeaveManagement.Persistence/ HR.LeaveManagement.Identity/ HR.LeaveManagement.API/ HR.LeaveManagement.MVC/ HR.LeaveManagement.Application.UnitTests/ ``` ## What the Sample Covers - A leave management system where employees request time off - Administrators create leave types (e.g. Annual, Sick, Bereavement) and allocate days to employees - Employees submit leave requests against their allocations - Requests can be approved or rejected by administrators - Email confirmation is sent on successful request submission ## Domain Model ### Entities All entities inherit from `BaseDomainEntity`, which inherits from RCommon's `AuditedEntity`. This provides `Id`, `CreatedBy`, `ModifiedBy`, `CreateDate`, and `ModifyDate` automatically: ```csharp public abstract class BaseDomainEntity : AuditedEntity { } public class LeaveType : BaseDomainEntity { public string Name { get; set; } // e.g. "Annual Leave" public int DefaultDays { get; set; } // e.g. 14 } public class LeaveAllocation : BaseDomainEntity { public int NumberOfDays { get; set; } public LeaveType LeaveType { get; set; } public int LeaveTypeId { get; set; } public int Period { get; set; } // year public string EmployeeId { get; set; } } public class LeaveRequest : BaseDomainEntity { public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public LeaveType LeaveType { get; set; } public int LeaveTypeId { get; set; } public DateTime DateRequested { get; set; } public string RequestComments { get; set; } public bool? Approved { get; set; } public bool Cancelled { get; set; } public string RequestingEmployeeId { get; set; } } ``` ### Domain Specifications A specification encapsulates a reusable query predicate. The `AllocationExistsSpec` checks whether an employee already has an allocation for a given leave type in a given year: ```csharp public class AllocationExistsSpec : Specification { public AllocationExistsSpec(string userId, int leaveTypeId, int period) : base(q => q.EmployeeId == userId && q.LeaveTypeId == leaveTypeId && q.Period == period) { } } ``` Usage in a command handler: ```csharp var allocationCount = await _leaveAllocationRepository .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period)); if (allocationCount > 0) continue; // already allocated for this period ``` ## Application Layer ### Commands and Queries CQRS is expressed through RCommon's `IAppRequest` and `IAppRequestHandler` interfaces. ```csharp // Command — modifies state public class CreateLeaveTypeCommand : IAppRequest { public CreateLeaveTypeDto LeaveTypeDto { get; set; } } // Query — reads state, no side effects public class GetLeaveTypeListRequest : IAppRequest> { } ``` ### Command Handler with Validation The `CreateLeaveTypeCommandHandler` validates the incoming DTO via `IValidationService` before persisting: ```csharp public class CreateLeaveTypeCommandHandler : IAppRequestHandler { private readonly IGraphRepository _leaveTypeRepository; private readonly IValidationService _validationService; public CreateLeaveTypeCommandHandler( IGraphRepository leaveTypeRepository, IValidationService validationService) { _leaveTypeRepository = leaveTypeRepository; _validationService = validationService; _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement; } public async Task HandleAsync( CreateLeaveTypeCommand request, CancellationToken cancellationToken) { var response = new BaseCommandResponse(); var validationResult = await _validationService.ValidateAsync(request.LeaveTypeDto); if (!validationResult.IsValid) { response.Success = false; response.Message = "Creation Failed"; response.Errors = validationResult.Errors .Select(q => q.ErrorMessage).ToList(); } else { var leaveType = request.LeaveTypeDto.ToLeaveType(); await _leaveTypeRepository.AddAsync(leaveType); response.Success = true; response.Message = "Creation Successful"; response.Id = leaveType.Id; } return response; } } ``` ### Command Handler with Multiple Repositories and Email The `CreateLeaveRequestCommandHandler` coordinates multiple repositories, the current user identity, and email delivery. The handler remains unit-testable because all dependencies are injected as interfaces: ```csharp public class CreateLeaveRequestCommandHandler : IAppRequestHandler { private readonly IReadOnlyRepository _leaveTypeRepository; private readonly IGraphRepository _leaveAllocationRepository; private readonly IGraphRepository _leaveRequestRepository; private readonly IEmailService _emailSender; private readonly ICurrentUser _currentUser; private readonly IValidationService _validationService; public async Task HandleAsync( CreateLeaveRequestCommand request, CancellationToken cancellationToken) { var response = new BaseCommandResponse(); var validationResult = await _validationService.ValidateAsync(request.LeaveRequestDto); var userId = _currentUser.FindClaimValue(CustomClaimTypes.Uid); // Check allocation exists var allocation = _leaveAllocationRepository.FirstOrDefault( x => x.EmployeeId == userId && x.LeaveTypeId == request.LeaveRequestDto.LeaveTypeId); if (allocation is null) { validationResult.Errors.Add(new ValidationFault( nameof(request.LeaveRequestDto.LeaveTypeId), "You do not have any allocations for this leave type.")); } else { int daysRequested = (int)(request.LeaveRequestDto.EndDate - request.LeaveRequestDto.StartDate).TotalDays; if (daysRequested > allocation.NumberOfDays) { validationResult.Errors.Add(new ValidationFault( nameof(request.LeaveRequestDto.EndDate), "You do not have enough days for this request")); } } if (!validationResult.IsValid) { response.Success = false; response.Message = "Request Failed"; response.Errors = validationResult.Errors .Select(q => q.ErrorMessage).ToList(); } else { var leaveRequest = request.LeaveRequestDto.ToLeaveRequest(); leaveRequest.RequestingEmployeeId = userId; await _leaveRequestRepository.AddAsync(leaveRequest); response.Success = true; response.Message = "Request Created Successfully"; response.Id = leaveRequest.Id; // Send confirmation email — failure does not roll back the request try { var emailAddress = _currentUser.FindClaimValue(ClaimTypes.Email); var email = new MailMessage( new MailAddress(fromEmail, fromName), new MailAddress(emailAddress)) { Subject = "Leave Request Submitted", Body = $"Your leave request for {request.LeaveRequestDto.StartDate:D} " + $"to {request.LeaveRequestDto.EndDate:D} has been submitted." }; await _emailSender.SendEmailAsync(email); } catch (Exception) { // Log but do not propagate — email is non-critical } } return response; } } ``` ### Allocation Command Handler The `CreateLeaveAllocationCommandHandler` demonstrates using a specification to avoid duplicate allocations. It retrieves all employees via the `IUserService` contract and allocates days from the leave type's `DefaultDays`: ```csharp public async Task HandleAsync( CreateLeaveAllocationCommand request, CancellationToken cancellationToken) { var leaveType = await _leaveTypeRepository .FindAsync(request.LeaveAllocationDto.LeaveTypeId); var employees = await _userService.GetEmployees(); var period = DateTime.Now.Year; var allocations = new List(); foreach (var emp in employees) { var allocationCount = await _leaveAllocationRepository .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period)); if (allocationCount > 0) continue; allocations.Add(new LeaveAllocation { EmployeeId = emp.Id, LeaveTypeId = leaveType.Id, NumberOfDays = leaveType.DefaultDays, Period = period }); } foreach (var item in allocations) { await _leaveAllocationRepository.AddAsync(item); } return new BaseCommandResponse { Success = true, Message = "Allocations Successful" }; } ``` ### Validation Validators use FluentValidation. They are discovered automatically via `AddValidatorsFromAssemblyContaining`: ```csharp public class ILeaveTypeDtoValidator : AbstractValidator { public ILeaveTypeDtoValidator() { RuleFor(p => p.Name) .NotEmpty().WithMessage("{PropertyName} is required.") .NotNull() .MaximumLength(50) .WithMessage("{PropertyName} must not exceed {ComparisonValue} characters."); RuleFor(p => p.DefaultDays) .NotEmpty().WithMessage("{PropertyName} is required.") .GreaterThan(0).WithMessage("{PropertyName} must be at least 1.") .LessThan(100) .WithMessage("{PropertyName} must be less than {ComparisonValue}."); } } public class CreateLeaveTypeDtoValidator : AbstractValidator { public CreateLeaveTypeDtoValidator() { Include(new ILeaveTypeDtoValidator()); } } ``` ## Persistence Layer `LeaveManagementDbContext` inherits from `AuditableDbContext`. RCommon injects `ICurrentUser` and `ISystemTime` to stamp audit fields on every `SaveChanges`: ```csharp public class LeaveManagementDbContext : AuditableDbContext { public LeaveManagementDbContext( DbContextOptions options, ICurrentUser currentUser, ISystemTime systemTime) : base(options, currentUser, systemTime) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly( typeof(LeaveManagementDbContext).Assembly); } public DbSet LeaveRequests { get; set; } public DbSet LeaveTypes { get; set; } public DbSet LeaveAllocations { get; set; } } ``` The data store name constant keeps the name in one place: ```csharp public static class DataStoreNamesConst { public const string LeaveManagement = "LeaveManagement"; } ``` ## API Controllers Controllers use `IMediatorService` exclusively — no direct handler references: ```csharp [Route("api/[controller]")] [ApiController] [Authorize] public class LeaveTypesController : ControllerBase { private readonly IMediatorService _mediator; public LeaveTypesController(IMediatorService mediator) => _mediator = mediator; [HttpGet] public async Task>> Get() { var leaveTypes = await _mediator.Send>( new GetLeaveTypeListRequest()); return Ok(leaveTypes); } [HttpPost] [Authorize(Roles = "Administrator")] public async Task> Post( [FromBody] CreateLeaveTypeDto leaveType) { var command = new CreateLeaveTypeCommand { LeaveTypeDto = leaveType }; var response = await _mediator.Send(command); return Ok(response); } [HttpPut("{id}")] [Authorize(Roles = "Administrator")] public async Task Put([FromBody] LeaveTypeDto leaveType) { var command = new UpdateLeaveTypeCommand { LeaveTypeDto = leaveType }; await _mediator.Send(command); return NoContent(); } [HttpDelete("{id}")] [Authorize(Roles = "Administrator")] public async Task Delete(int id) { var command = new DeleteLeaveTypeCommand { Id = id }; await _mediator.Send(command); return NoContent(); } } ``` ## Composition Root Everything is wired in `Program.cs`. This is the only place in the system where concrete implementations are referenced by name: :::tip Modular composition The HR Leave Management sample keeps all wiring in a single `Program.cs` for clarity, but RCommon also supports splitting this across modules. For example, an `IdentityServicesRegistration.AddIdentityServices(services)` extension could call `services.AddRCommon().WithClaimsAndPrincipalAccessor()` while a separate `PersistenceServicesRegistration` calls `services.AddRCommon().WithPersistence(...)`. The bootstrapper merges both into the same builder. See [Modular Composition](../core-concepts/modular-composition.mdx) for the contract. ::: ```csharp builder.Services.AddRCommon() .WithClaimsAndPrincipalAccessor() .WithSendGridEmailServices(x => { var settings = builder.Configuration.Get(); x.SendGridApiKey = settings.SendGridApiKey; x.FromNameDefault = settings.FromNameDefault; x.FromEmailDefault = settings.FromEmailDefault; }) .WithDateTimeSystem(dateTime => dateTime.Kind = DateTimeKind.Utc) .WithSequentialGuidGenerator(guid => guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString) .WithMediator(mediator => { mediator.AddRequest(); mediator.AddRequest, GetLeaveTypeListRequestHandler>(); // ... all other request/handler pairs mediator.Configure(config => { config.RegisterServicesFromAssemblies( typeof(ApplicationServicesRegistration).GetTypeInfo().Assembly); }); mediator.AddLoggingToRequestPipeline(); mediator.AddUnitOfWorkToRequestPipeline(); }) .WithPersistence(ef => { ef.AddDbContext( DataStoreNamesConst.LeaveManagement, options => options.UseSqlServer( builder.Configuration.GetConnectionString( DataStoreNamesConst.LeaveManagement))); ef.SetDefaultDataStore(dataStore => dataStore.DefaultDataStoreName = DataStoreNamesConst.LeaveManagement); }) .WithUnitOfWork(unitOfWork => { unitOfWork.SetOptions(options => { options.AutoCompleteScope = true; options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }) .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining( typeof(ApplicationServicesRegistration)); }); ``` ## Unit Testing Handlers Because every handler depends on interfaces, tests use mocks and require no database: ```csharp [TestFixture] public class CreateLeaveTypeCommandHandlerTests { private readonly CreateLeaveTypeDto _leaveTypeDto; private readonly CreateLeaveTypeCommandHandler _handler; public CreateLeaveTypeCommandHandlerTests() { var repositoryMock = new Mock>(); var validationMock = new Mock(); _leaveTypeDto = new CreateLeaveTypeDto { DefaultDays = 15, Name = "Test DTO" }; validationMock .Setup(x => x.ValidateAsync(_leaveTypeDto, false, CancellationToken.None)) .Returns(() => Task.FromResult(new ValidationOutcome())); _handler = new CreateLeaveTypeCommandHandler( repositoryMock.Object, validationMock.Object); } [Test] public async Task Valid_LeaveType_Added() { var result = await _handler.HandleAsync( new CreateLeaveTypeCommand { LeaveTypeDto = _leaveTypeDto }, CancellationToken.None); result.ShouldBeOfType(); result.Success.ShouldBeTrue(); } } ``` ## Key Takeaways - **`IGraphRepository`** decouples handlers from the ORM. Switch from EF Core to NHibernate without touching a single handler. - **`IValidationService`** decouples handlers from FluentValidation. The validator registration is a composition-root concern. - **`ICurrentUser`** provides the authenticated user identity without importing ASP.NET Core into the domain or application layers. - **`AuditableDbContext`** eliminates boilerplate audit stamping across every save. - **Pipeline behaviors** (`AddLoggingToRequestPipeline`, `AddUnitOfWorkToRequestPipeline`) apply cross-cutting concerns to every handler without modifying handler code. - **`DataStoreName`** enables routing to the correct `DbContext` when multiple databases are registered. --- ## Messaging Examples Source: https://rcommon.com/docs/examples-recipes/messaging Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing. # Messaging Examples This page walks through the messaging examples included with RCommon. These examples show how to use MassTransit and Wolverine as the event transport, and how to isolate event subscriptions between multiple publishers. ## Example Projects ``` Examples/Messaging/ Examples.Messaging.MassTransit/ — MassTransit with in-memory transport Examples.Messaging.Wolverine/ — Wolverine with local queue Examples.Messaging.SubscriptionIsolation/ — Two builders with isolated subscriptions ``` ## Step 1: Define an Event Events are plain classes that implement `ISyncEvent`. They must have a default constructor so MassTransit or Wolverine can deserialize them off the wire: ```csharp using RCommon.Models.Events; public class TestEvent : ISyncEvent { public TestEvent() { } public TestEvent(DateTime dateTime, Guid guid) { DateTime = dateTime; Guid = guid; } public DateTime DateTime { get; } public Guid Guid { get; } } ``` ## Step 2: Define a Subscriber ```csharp using RCommon.EventHandling.Subscribers; public class TestEventHandler : ISubscriber { public async Task HandleAsync(TestEvent notification, CancellationToken cancellationToken = default) { Console.WriteLine("I just handled this event {0}", notification.ToString()); Console.WriteLine("Example Complete"); await Task.CompletedTask; } } ``` ## Example 1: MassTransit ### Installation ### Configuration ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { // Use the in-memory transport for local development and testing eventHandling.UsingInMemory((context, cfg) => { cfg.ConfigureEndpoints(context); }); eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); ``` For production, replace `UsingInMemory` with `UsingRabbitMq`, `UsingAzureServiceBus`, or another MassTransit transport: ```csharp eventHandling.UsingRabbitMq((context, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(context); }); ``` ### Publishing via a Background Worker ```csharp public class Worker : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly IHostApplicationLifetime _lifetime; public Worker(IServiceProvider serviceProvider, IHostApplicationLifetime lifetime) { _serviceProvider = serviceProvider; _lifetime = lifetime; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { Console.WriteLine("Example Starting"); var eventProducers = _serviceProvider.GetServices(); var testEvent = new TestEvent(DateTime.Now, Guid.NewGuid()); foreach (var producer in eventProducers) { Console.WriteLine($"Producer: {producer}"); await producer.ProduceEventAsync(testEvent); } Console.WriteLine("Example Complete"); _lifetime.StopApplication(); } } ``` ### Full Program.cs ```csharp using MassTransit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using RCommon; using RCommon.EventHandling.Producers; using RCommon.MassTransit; using RCommon.MassTransit.Producers; var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.UsingInMemory((context, cfg) => { cfg.ConfigureEndpoints(context); }); eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); services.AddHostedService(); }).Build(); await host.RunAsync(); ``` ## Example 2: Wolverine ### Installation Wolverine requires calling `UseWolverine` on the host builder before configuring services: ### Configuration ```csharp using Wolverine; using RCommon; using RCommon.Wolverine; using RCommon.Wolverine.Producers; var host = Host.CreateDefaultBuilder(args) .UseWolverine(options => { // Define a local queue for event processing options.LocalQueue("test"); }) .ConfigureServices(services => { services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); }); }).Build(); ``` ### Publishing ```csharp await host.StartAsync(); var eventProducers = host.Services.GetServices(); var testEvent = new TestEvent(DateTime.Now, Guid.NewGuid()); foreach (var producer in eventProducers) { await producer.ProduceEventAsync(testEvent); } ``` The pattern is identical to MassTransit. Only the builder type and the registered producer change. ## Example 3: Subscription Isolation The subscription isolation example demonstrates that events are only routed to the producer/handler pair that subscribed them. This is critical in services with mixed internal and external messaging. :::tip Modular composition The two-builder pattern shown below works whether you wire it in one `Program.cs` or split it across modules. Each module can call `services.AddRCommon().WithEventHandling(...)` independently — the cached builder is reused and subscriber registrations accumulate. The MassTransit builder has a known cross-module limitation documented on the [MassTransit event handling page](../event-handling/masstransit.mdx#configuration); see [Modular Composition](../core-concepts/modular-composition.mdx) for the full contract. ::: ### Scenario - `InMemoryOnlyEvent` — subscribed only in `InMemoryEventBusBuilder` - `MassTransitOnlyEvent` — subscribed only in `MassTransitEventHandlingBuilder` - `SharedEvent` — subscribed in both builders, handled by both producer types ### Event Definitions ```csharp public class InMemoryOnlyEvent : ISyncEvent { public InMemoryOnlyEvent() { } public InMemoryOnlyEvent(DateTime dateTime, Guid guid) { DateTime = dateTime; Guid = guid; } public DateTime DateTime { get; } public Guid Guid { get; } } public class MassTransitOnlyEvent : ISyncEvent { public MassTransitOnlyEvent() { } public MassTransitOnlyEvent(DateTime dateTime, Guid guid) { DateTime = dateTime; Guid = guid; } public DateTime DateTime { get; } public Guid Guid { get; } } public class SharedEvent : ISyncEvent { public SharedEvent() { } public SharedEvent(DateTime dateTime, Guid guid) { DateTime = dateTime; Guid = guid; } public DateTime DateTime { get; } public Guid Guid { get; } } ``` ### Configuration ```csharp services.AddRCommon() .WithEventHandling(eventHandling => { eventHandling.AddProducer(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }) .WithEventHandling(eventHandling => { eventHandling.UsingInMemory((context, cfg) => { cfg.ConfigureEndpoints(context); }); eventHandling.AddProducer(); eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }); ``` ### Publishing All Three Event Types ```csharp protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var eventProducers = _serviceProvider.GetServices(); // Only InMemoryEventBus handles this — MassTransit producers ignore it var inMemoryEvent = new InMemoryOnlyEvent(DateTime.Now, Guid.NewGuid()); Console.WriteLine("Publishing InMemoryOnlyEvent to all producers..."); foreach (var producer in eventProducers) { Console.WriteLine($" -> Producer: {producer.GetType().Name}"); await producer.ProduceEventAsync(inMemoryEvent); } // Only MassTransit handles this — in-memory producer ignores it var massTransitEvent = new MassTransitOnlyEvent(DateTime.Now, Guid.NewGuid()); Console.WriteLine("Publishing MassTransitOnlyEvent to all producers..."); foreach (var producer in eventProducers) { Console.WriteLine($" -> Producer: {producer.GetType().Name}"); await producer.ProduceEventAsync(massTransitEvent); } // Both builders subscribed this — both producers handle it var sharedEvent = new SharedEvent(DateTime.Now, Guid.NewGuid()); Console.WriteLine("Publishing SharedEvent to all producers (subscribed to both)..."); foreach (var producer in eventProducers) { Console.WriteLine($" -> Producer: {producer.GetType().Name}"); await producer.ProduceEventAsync(sharedEvent); } _lifetime.StopApplication(); } ``` ### SharedEventHandler ```csharp public class SharedEventHandler : ISubscriber { public async Task HandleAsync(SharedEvent notification, CancellationToken cancellationToken = default) { Console.WriteLine("[SharedHandler] Handled SharedEvent: {0} | {1}", notification.DateTime, notification.Guid); await Task.CompletedTask; } } ``` ### Expected Behavior When `InMemoryOnlyEvent` is published through all producers: - `PublishWithEventBusEventProducer` routes it to `InMemoryOnlyEventHandler` (subscribed) - `PublishWithMassTransitEventProducer` ignores it (not subscribed in that builder) When `SharedEvent` is published through all producers: - `PublishWithEventBusEventProducer` routes it to `SharedEventHandler` - `PublishWithMassTransitEventProducer` also routes it to `SharedEventHandler` Both handlers fire independently. ## Comparing MassTransit and Wolverine | Aspect | MassTransit | Wolverine | |--------|------------|-----------| | Installation | `RCommon.MassTransit` | `RCommon.Wolverine` | | Host setup | Standard `ConfigureServices` | Requires `Host.UseWolverine(...)` | | Transport options | RabbitMQ, Azure Service Bus, SQS, in-memory, and more | RabbitMQ, Azure Service Bus, in-memory local queues | | Saga / workflow support | Yes (via MassTransit state machines) | Yes (via Wolverine's saga support) | | Outbox support | Yes (via MassTransit Outbox) | Yes (built-in) | | Local queue config | Automatic endpoint configuration | Named local queues via `options.LocalQueue(...)` | | RCommon builder | `MassTransitEventHandlingBuilder` | `WolverineEventHandlingBuilder` | | RCommon producer | `PublishWithMassTransitEventProducer` | `PublishWithWolverineEventProducer` | ## Testing Messaging Code Event handlers are plain classes and do not require a running broker to test: ```csharp [Test] public async Task TestEventHandler_Completes_Without_Error() { var handler = new TestEventHandler(); var @event = new TestEvent(DateTime.UtcNow, Guid.NewGuid()); await handler.HandleAsync(@event, CancellationToken.None); // No exception = pass } ``` For integration tests, use the MassTransit in-memory transport or Wolverine's test harness, which both run entirely in process. ## API Reference | Type | Package | Purpose | |------|---------|---------| | `ISyncEvent` | `RCommon.Models` | Base interface for all events | | `IEventProducer` | `RCommon.EventHandling` | Publishes events | | `ISubscriber` | `RCommon.EventHandling` | Handles events | | `MassTransitEventHandlingBuilder` | `RCommon.MassTransit` | MassTransit event handling configuration | | `PublishWithMassTransitEventProducer` | `RCommon.MassTransit` | MassTransit-backed producer | | `WolverineEventHandlingBuilder` | `RCommon.Wolverine` | Wolverine event handling configuration | | `PublishWithWolverineEventProducer` | `RCommon.Wolverine` | Wolverine-backed producer | | `InMemoryEventBusBuilder` | `RCommon.EventHandling` | In-process event bus for isolation scenarios | | `PublishWithEventBusEventProducer` | `RCommon.EventHandling` | In-memory producer | --- ## configuration Source: https://rcommon.com/docs/getting-started/configuration --- title: Configuration & Bootstrapping sidebar_position: 4 description: "Configure RCommon with AddRCommon() — fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling." --- # Configuration & Bootstrapping RCommon uses a single fluent builder entry point. All configuration happens at application startup before the DI container is built. ## The AddRCommon() extension method `AddRCommon()` is an extension method on `IServiceCollection` defined in `RCommon.Core`. Calling it creates an `RCommonBuilder`, registers the core framework services, and returns an `IRCommonBuilder` for further configuration. ```csharp using RCommon; // In Program.cs or a DI registration method builder.Services.AddRCommon(); ``` Calling `AddRCommon()` always registers these services regardless of what else is configured: - `EventSubscriptionManager` (singleton) — tracks event-to-producer subscriptions - `IEventBus` backed by `InMemoryEventBus` (singleton) — in-process publish/subscribe - `IEventRouter` backed by `InMemoryTransactionalEventRouter` (scoped) — coordinates domain events with the unit of work - `CachingOptions` configured with caching disabled by default ## Builder chain The return value of `AddRCommon()` is `IRCommonBuilder`. Every `With*` method on the builder returns the same `IRCommonBuilder` instance so calls can be chained: ```csharp builder.Services.AddRCommon() .WithSequentialGuidGenerator(...) .WithDateTimeSystem(...) .WithPersistence(...) .WithUnitOfWork(...) .WithMediator(...) .WithEventHandling(...); ``` Every `With*` call is independent and opt-in. Omitting a call means those services are not registered. ## Core builder methods ### WithSequentialGuidGenerator Registers `IGuidGenerator` as a `SequentialGuidGenerator`. Sequential GUIDs are ordered in a way that reduces index fragmentation in SQL databases. ```csharp .WithSequentialGuidGenerator(options => { options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString; }) ``` `SequentialGuidType` values: | Value | Description | |---|---| | `SequentialAsString` | Sequential in string representation — compatible with MySQL and PostgreSQL | | `SequentialAsBinary` | Sequential in binary representation — compatible with Oracle | | `SequentialAtEnd` | Sequential at the end — compatible with SQL Server | Only one GUID generator may be configured. Calling `WithSequentialGuidGenerator` or `WithSimpleGuidGenerator` a second time throws `RCommonBuilderException`. ### WithSimpleGuidGenerator Registers `IGuidGenerator` as a `SimpleGuidGenerator` that wraps `Guid.NewGuid()`. ```csharp .WithSimpleGuidGenerator() ``` ### WithDateTimeSystem Registers `ISystemTime` as `SystemTime`. Inject `ISystemTime` wherever you need the current time so that tests can substitute a known value. ```csharp .WithDateTimeSystem(options => { options.Kind = DateTimeKind.Utc; }) ``` Only one date/time system may be configured per builder instance. ### WithCommonFactory Registers a service type alongside a DI-aware `ICommonFactory` that can create instances through the container. ```csharp .WithCommonFactory() ``` ## Persistence configuration `WithPersistence` accepts any type that implements `IPersistenceBuilder`. The built-in options are `EFCorePersistenceBuilder`, `DapperPersistenceBuilder`, and `Linq2DbPersistenceBuilder`. ```csharp .WithPersistence(ef => { ef.AddDbContext("AppDb", options => { options.UseSqlServer(connectionString); }); ef.SetDefaultDataStore(ds => { ds.DefaultDataStoreName = "AppDb"; }); }) ``` Multiple DbContexts can be registered with different names. The name is used to resolve the correct context at runtime via `IDataStoreFactory`. Use `SetDefaultDataStore` to declare which context a repository uses when no explicit data store name is specified on the repository instance. ## Unit of work configuration `WithUnitOfWork` accepts any type that implements `IUnitOfWorkBuilder`. Use `DefaultUnitOfWorkBuilder` for the standard `System.Transactions`-based implementation. ```csharp .WithUnitOfWork(uow => { uow.SetOptions(options => { options.AutoCompleteScope = true; options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }) ``` `AutoCompleteScope` causes the unit of work scope to commit automatically when the outermost scope completes without an exception. Set it to `false` if you need explicit `Complete()` calls. ## Mediator configuration `WithMediator` accepts any type that implements `IMediatorBuilder`. Use `MediatRBuilder` for MediatR integration. ```csharp .WithMediator(mediator => { mediator.AddRequest(); mediator.AddRequest(); mediator.Configure(config => { config.RegisterServicesFromAssemblies(typeof(ApplicationAssemblyMarker).Assembly); }); mediator.AddLoggingToRequestPipeline(); mediator.AddUnitOfWorkToRequestPipeline(); }) ``` ## Event handling configuration `WithEventHandling` accepts any type that implements `IEventHandlingBuilder`. Use `InMemoryEventBusBuilder` for in-process events. ```csharp .WithEventHandling(eventHandling => { eventHandling.AddSubscriber(); eventHandling.AddSubscriber(); }) ``` Each `AddSubscriber` call registers the handler as a scoped `ISubscriber` and records the event-to-producer subscription in the `EventSubscriptionManager` so the `IEventRouter` knows which producers handle which events. ## Complete example The following shows a realistic `Program.cs` configuration for a web API that uses EF Core persistence, MediatR, FluentValidation, and in-process events: ```csharp using RCommon; using RCommon.Persistence.EFCore; using RCommon.Persistence.Transactions; using RCommon.Mediator.MediatR; using RCommon.EventHandling; using RCommon.FluentValidation; using Microsoft.EntityFrameworkCore; using System.Transactions; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRCommon() .WithSequentialGuidGenerator(guid => guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString) .WithDateTimeSystem(dt => dt.Kind = DateTimeKind.Utc) .WithPersistence(ef => { ef.AddDbContext("AppDb", options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection"))); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "AppDb"); }) .WithUnitOfWork(uow => { uow.SetOptions(options => { options.AutoCompleteScope = true; options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }) .WithMediator(mediator => { mediator.Configure(config => config.RegisterServicesFromAssemblies(typeof(ApplicationAssemblyMarker).Assembly)); mediator.AddLoggingToRequestPipeline(); mediator.AddUnitOfWorkToRequestPipeline(); }) .WithEventHandling(events => { events.AddSubscriber(); }) .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining(); }); ``` ## Modular Composition `AddRCommon()` is idempotent across modules. The first call constructs the `IRCommonBuilder` and caches it on `IServiceCollection`; every subsequent call returns the same instance. Sub-builder verbs (`WithPersistence`, `WithMediator`, `WithEventHandling`, `WithUnitOfWork`, `WithCQRS`, `WithValidation`, `WithBlobStorage`, `WithMultiTenancy`, `WithMemoryCaching`, `WithDistributedCaching`) are cache-aware: a second call with the same `T` reuses the cached sub-builder, so the supplied `Action` runs against the same instance and registrations accumulate. Singleton-style verbs (`WithSimpleGuidGenerator`, `WithSequentialGuidGenerator`, `WithDateTimeSystem`, `WithJsonSerialization`, `WithSmtpEmailServices`, `WithSendGridEmailServices`) are idempotent on identical impls and throw `RCommonBuilderException` only when a different impl competes for the same slot. The typical pattern: each module exposes a `Configure(IServiceCollection services)` method that builds its slice of RCommon independently. ```csharp public interface IServiceModule { void Configure(IServiceCollection services); } public sealed class OrderingModule : IServiceModule { public void Configure(IServiceCollection services) { services.AddRCommon() .WithSimpleGuidGenerator() .WithPersistence(ef => ef.AddDbContext( "Ordering", o => o.UseSqlServer(orderingConnectionString))); } } public sealed class InventoryModule : IServiceModule { public void Configure(IServiceCollection services) { services.AddRCommon() .WithSimpleGuidGenerator() // Same impl as Ordering — idempotent no-op. .WithPersistence(ef => ef.AddDbContext( "Inventory", o => o.UseNpgsql(inventoryConnectionString))); } } ``` Each module pulls in only the features it needs; the framework deduplicates shared registrations automatically. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix and the runnable example under `Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/`. ## Diagnostics During development you can print all registered services to the console to verify configuration: ```csharp Console.WriteLine(builder.Services.GenerateServiceDescriptorsString()); ``` This produces a sorted list of every `ServiceDescriptor` in the container, which is useful for spotting missing or duplicate registrations. --- ## dependency-injection Source: https://rcommon.com/docs/getting-started/dependency-injection --- title: Dependency Injection sidebar_position: 5 description: "How RCommon integrates with Microsoft.Extensions.DependencyInjection — service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores." --- # Dependency Injection RCommon is built entirely on `Microsoft.Extensions.DependencyInjection`. It does not introduce its own container or wrap the standard one. Every service RCommon registers follows the same lifetime conventions you already know from ASP.NET Core. ## How RCommon registers services When you call `AddRCommon()`, the `RCommonBuilder` receives the application's `IServiceCollection` and calls `AddSingleton`, `AddScoped`, and `AddTransient` on it directly. By the time `builder.Build()` is called, all RCommon services are in the same container as your application services — no separate container, no child scope complications. ```csharp // All of this is standard Microsoft DI var builder = WebApplication.CreateBuilder(args); builder.Services.AddRCommon() .WithPersistence(ef => { ... }) .WithMediator(m => { ... }); var app = builder.Build(); ``` ## Service lifetimes used by RCommon Understanding which lifetime each service uses helps you reason about when instances are created and shared. | Service | Lifetime | Reason | |---|---|---| | `EventSubscriptionManager` | Singleton | Subscription metadata is static; built once at startup | | `IEventBus` (`InMemoryEventBus`) | Singleton | Stateless dispatcher; safe to share across requests | | `IEventRouter` (`InMemoryTransactionalEventRouter`) | Scoped | Tied to the unit of work scope; one per HTTP request | | `IEntityEventTracker` (`InMemoryEntityEventTracker`) | Scoped | Accumulates domain events during a single unit of work | | `ISubscriber` handler | Scoped | Event handlers may depend on scoped services like repositories | | `ILinqRepository` / `IReadOnlyRepository` / `IWriteOnlyRepository` | Transient | Stateless wrappers around the scoped DbContext | | `IAggregateRepository` | Transient | Same as above | | `IUnitOfWork` | Transient | New scope created each time a unit of work is opened | | `IUnitOfWorkFactory` | Transient | Lightweight factory; safe as transient | | `IDataStoreFactory` | Transient | Resolves the correct DbContext by name; delegates to the scoped DbContext | | `IGuidGenerator` (sequential) | Transient | Stateless; new instance per call | | `IGuidGenerator` (simple) | Scoped | Registered as scoped by `WithSimpleGuidGenerator` | | `ISystemTime` | Transient | Stateless clock wrapper | | `ICommonFactory` | Scoped | Delegates to scoped service resolution | ## Injecting RCommon services ### Repositories After calling `WithPersistence`, the open-generic repository interfaces are available for injection anywhere: ```csharp public class OrderService { private readonly ILinqRepository _orders; public OrderService(ILinqRepository orders) { _orders = orders; } public async Task> GetPendingOrdersAsync() { return await _orders.FindAsync(o => o.Status == OrderStatus.Pending); } } ``` The DI container resolves `ILinqRepository` to `EFCoreRepository` which in turn receives the scoped `AppDbContext` through constructor injection. ### IEventBus Inject `IEventBus` wherever you need to publish events in-process. It is a singleton, so it can be injected into any lifetime. ```csharp public class OrderApplicationService { private readonly ILinqRepository _orders; private readonly IEventBus _eventBus; public OrderApplicationService( ILinqRepository orders, IEventBus eventBus) { _orders = orders; _eventBus = eventBus; } public async Task PlaceOrderAsync(PlaceOrderRequest request) { var order = new Order(request.CustomerId, request.Items); await _orders.AddAsync(order); await _eventBus.PublishAsync(new OrderPlacedEvent(order.Id)); } } ``` ### ISystemTime Inject `ISystemTime` instead of calling `DateTime.UtcNow` directly so that tests can control the clock: ```csharp public class AuditService { private readonly ISystemTime _clock; public AuditService(ISystemTime clock) { _clock = clock; } public DateTime GetCurrentTime() => _clock.Now; } ``` ### IGuidGenerator Inject `IGuidGenerator` to generate identifiers that are safe to use as primary keys: ```csharp public class ProductFactory { private readonly IGuidGenerator _guidGenerator; public ProductFactory(IGuidGenerator guidGenerator) { _guidGenerator = guidGenerator; } public Product Create(string name, decimal price) { return new Product { Id = _guidGenerator.Create(), Name = name, Price = price }; } } ``` ### ICommonFactory<T> `ICommonFactory` is a DI-aware factory that resolves instances of `T` from the container on demand. Register it with `WithCommonFactory` and then inject it where deferred or conditional resolution is needed: ```csharp // Registration builder.Services.AddRCommon() .WithCommonFactory(); // Usage public class NotificationService { private readonly ICommonFactory _emailFactory; public NotificationService(ICommonFactory emailFactory) { _emailFactory = emailFactory; } public async Task SendAsync(string to, string subject, string body) { var sender = _emailFactory.Create(); await sender.SendAsync(to, subject, body); } } ``` ## Service registration patterns ### Registering your own services alongside RCommon RCommon does not prevent you from registering services before or after `AddRCommon()`. Standard registrations work exactly as expected: ```csharp builder.Services.AddScoped(); builder.Services.AddRCommon() .WithPersistence(ef => { ... }); builder.Services.AddScoped(); ``` ### Named data stores When you register multiple DbContexts (for example a write model and a read model), each gets a name: ```csharp .WithPersistence(ef => { ef.AddDbContext("WriteDb", options => options.UseSqlServer(writeConnectionString)); ef.AddDbContext("ReadDb", options => options.UseSqlServer(readConnectionString)); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "WriteDb"); }) ``` A repository can be directed to use a specific data store by setting `DataStoreName` on the repository instance before use. The `IDataStoreFactory` resolves the correct DbContext by name at runtime. ## Multi-module bootstrapping `AddRCommon()` is safe to call from any number of modules in the same process. The first call constructs the `IRCommonBuilder` and caches it on `IServiceCollection`; subsequent calls return the same instance, so all modules share one builder and one set of sub-builders. Three helper APIs support this pattern directly: | API | Purpose | |---|---| | `IServiceCollection.IsRCommonInitialized()` | Returns `true` if any module in the process has already called `AddRCommon()`. Useful for optional wiring in libraries that should skip themselves when RCommon is absent. | | `IRCommonBuilder.GetOrAddBuilder(Func)` | The hook custom `WithX` extensions use to opt into sub-builder caching. The factory runs at most once per `TSubBuilder` per process. | | `IRCommonBuilder.GetBootstrapDiagnostics()` | After `host.StartAsync()`, returns a report of soft duplicates detected by the descriptor scanner, or `string.Empty` if nothing is suspicious. | A minimal module pattern: ```csharp public interface IServiceModule { void Configure(IServiceCollection services); } public sealed class OrderingModule : IServiceModule { public void Configure(IServiceCollection services) { services.AddRCommon() .WithSimpleGuidGenerator() .WithPersistence(ef => ef.AddDbContext( "Ordering", o => o.UseInMemoryDatabase("ordering"))); } } public sealed class InventoryModule : IServiceModule { public void Configure(IServiceCollection services) { services.AddRCommon() .WithSimpleGuidGenerator() // Same impl — idempotent no-op. .WithPersistence(ef => ef.AddDbContext( "Inventory", o => o.UseInMemoryDatabase("inventory"))); } } // Composition root var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { IServiceModule[] modules = { new OrderingModule(), new InventoryModule() }; foreach (var module in modules) module.Configure(services); }) .Build(); ``` Both `WithPersistence` calls share the same persistence sub-builder, so both `AddDbContext` calls accumulate against one `DataStoreFactoryOptions`. The duplicate `WithSimpleGuidGenerator()` is a no-op because the impl type matches. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix and `Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/` for a runnable three-module example. ## Verifying registrations During development you can dump the full service registration list to the console: ```csharp Console.WriteLine(builder.Services.GenerateServiceDescriptorsString()); ``` To check for duplicate registrations — which can cause subtle bugs — use: ```csharp Console.WriteLine(builder.Services.GeneratePossibleDuplicatesServiceDescriptorsString()); ``` Both methods are extension methods on `IServiceCollection` provided by `RCommon.Core`. --- ## Installation Source: https://rcommon.com/docs/getting-started/installation Install RCommon NuGet packages for .NET 8, 9, or 10. Pick only what you need — persistence, CQRS, messaging, caching, validation, blob storage, and more. # Installation RCommon is distributed as a family of focused NuGet packages. Install only the ones relevant to your application — you are not forced to take every dependency. ## Target framework requirements All RCommon packages target .NET 8, .NET 9, and .NET 10. There is no support for .NET Framework or .NET Standard. ## Core package Every RCommon application starts with the core package. It provides the fluent builder entry point, the in-memory event bus, guard clauses, GUID generation, system time abstraction, and the extension method library. ## Models and entities Shared CQRS models (commands, queries, execution results, pagination) and domain entity base classes live in two separate packages so you can reference only what each project layer needs. ## Persistence Choose one or more persistence providers. Each package registers repository implementations behind the shared `ILinqRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository` abstractions. **Entity Framework Core** **Dapper** **Linq2Db** ## CQRS / Mediator Choose a backing implementation: ## Event handling The in-memory event bus is part of `RCommon.Core`. For distributed messaging, add one of: For the transactional outbox pattern: ## Caching ## Validation ## Email ## Blob storage ## Serialization ## Multi-tenancy ## Security and web ## State machines ## Application services ## Picking what you need A typical web API that uses EF Core, MediatR, FluentValidation, and in-process events needs only: ``` RCommon.Core RCommon.Models RCommon.Entities RCommon.Persistence RCommon.EFCore RCommon.Mediator RCommon.MediatR RCommon.FluentValidation RCommon.ApplicationServices ``` Add further packages incrementally as requirements grow. Packages that share abstractions (for example `RCommon.Persistence` and `RCommon.EFCore`) must be compatible versions; use the same version number for all RCommon packages in a single project. --- ## Overview & Philosophy Source: https://rcommon.com/docs/getting-started/overview Learn what RCommon provides — pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API. # Overview & Philosophy RCommon is a .NET infrastructure library that provides battle-tested abstractions for the concerns common to every production application: persistence, CQRS/mediator, event handling, caching, validation, emailing, and more. The goal is not to invent new patterns but to give you clean, consistent interfaces over the implementations you already use — Entity Framework Core, MediatR, MassTransit, Wolverine, Redis, and others — so that swapping providers never touches your domain or application code. ## What RCommon provides - **Fluent, DI-first configuration** — a single `AddRCommon()` call bootstraps everything through a composable builder chain. RCommon supports both single-call and multi-module composition; see [Modular Composition](../core-concepts/modular-composition.mdx). - **Pluggable providers** — every feature area exposes a generic slot (`WithPersistence`, `WithMediator`, `WithEventHandling`, etc.) that accepts whichever implementation package you install. - **Repository and unit-of-work abstractions** — `ILinqRepository`, `IReadOnlyRepository`, `IWriteOnlyRepository`, and `IAggregateRepository` sit in front of EF Core, Dapper, or Linq2Db. - **Domain-driven design primitives** — base entity classes with auditing, soft delete, and transactional domain events built in. - **In-process event bus** — `IEventBus` with subscriber registration and a transactional `IEventRouter` that coordinates domain events with the unit of work. - **CQRS mediation** — a thin `IMediator` abstraction backed by MediatR or Wolverine. - **Utility types** — `Guard`, `ISystemTime`, `IGuidGenerator`, `ICommonFactory`, specification pattern support, and a rich set of extension methods. ## What RCommon is not RCommon does not dictate your architecture. It does not generate controllers, scaffold screens, impose a folder structure, or require you to inherit from framework base classes in your domain layer. You choose Clean Architecture, Vertical Slice, Modular Monolith, or anything else — RCommon works alongside whatever structure you adopt. ## Supported frameworks RCommon targets: - .NET 8 (LTS) - .NET 9 - .NET 10 ## License RCommon is released under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). It is free for commercial and open-source use. ## Source code The full source is available on GitHub: [https://github.com/RCommon-Team/RCommon](https://github.com/RCommon-Team/RCommon) ## Design principles **Abstractions over implementations.** Your application code depends on `ILinqRepository`, not `EFCoreRepository`. The concrete type is registered at the composition root and can be swapped without touching business logic. **Fluent composition over convention magic.** Every feature is opt-in. If you do not call `WithPersistence()`, no persistence services are registered. There is no scanning, no implicit wiring, and no surprises. **Single entry point.** `services.AddRCommon()` returns an `IRCommonBuilder`. From there, every subsystem is a method call on that builder. The chain is self-documenting and IDE-discoverable. **Testability.** Abstractions like `ISystemTime` and `IGuidGenerator` exist specifically so that deterministic unit tests are straightforward without mocking static members or `DateTime.Now`. --- ## quick-start Source: https://rcommon.com/docs/getting-started/quick-start --- title: Quick Start Guide sidebar_position: 3 description: "Build a .NET web API with RCommon in minutes — define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain." --- # Quick Start Guide This guide walks through the minimum steps to create a new .NET web API, add RCommon, and run a repository query against a SQL Server database using Entity Framework Core. ## 1. Create a new project ```bash dotnet new webapi -n MyApp cd MyApp ``` ## 2. Install packages ## 3. Define your entity Entities in RCommon inherit from a base class provided by `RCommon.Entities`. The base classes handle common concerns like auditing and domain events automatically. ```csharp using RCommon.Entities; public class Product : BusinessEntity { public string Name { get; set; } = string.Empty; public decimal Price { get; set; } } ``` ## 4. Create a DbContext The `RCommonDbContext` base class integrates with RCommon's change tracking and domain event dispatch. ```csharp using Microsoft.EntityFrameworkCore; using RCommon.Persistence.EFCore; public class AppDbContext : RCommonDbContext { public AppDbContext(DbContextOptions options, IEntityEventTracker eventTracker) : base(options, eventTracker) { } public DbSet Products => Set(); } ``` ## 5. Configure RCommon in Program.cs ```csharp using RCommon; using RCommon.Persistence.EFCore; using RCommon.Persistence.Transactions; using Microsoft.EntityFrameworkCore; using System.Transactions; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRCommon() .WithSequentialGuidGenerator(guid => guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString) .WithDateTimeSystem(dt => dt.Kind = DateTimeKind.Utc) .WithPersistence(ef => { ef.AddDbContext("AppDb", options => { options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection")); }); ef.SetDefaultDataStore(ds => { ds.DefaultDataStoreName = "AppDb"; }); }) .WithUnitOfWork(uow => { uow.SetOptions(options => { options.AutoCompleteScope = true; options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }); builder.Services.AddControllers(); var app = builder.Build(); app.MapControllers(); app.Run(); ``` ## 6. Inject and use the repository RCommon automatically registers `ILinqRepository` for every entity once `EFCorePersistenceBuilder` is configured. Inject it directly into your controllers, application services, or handlers. ```csharp using RCommon.Persistence.Crud; using Microsoft.AspNetCore.Mvc; [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly ILinqRepository _products; public ProductsController(ILinqRepository products) { _products = products; } [HttpGet] public async Task GetAll() { var products = await _products.FindAsync(p => p.Price > 0); return Ok(products); } [HttpGet("{id:guid}")] public async Task GetById(Guid id) { var product = await _products.FindAsync(id); if (product is null) return NotFound(); return Ok(product); } [HttpPost] public async Task Create(Product product) { await _products.AddAsync(product); return CreatedAtAction(nameof(GetById), new { id = product.Id }, product); } } ``` ## What happens under the hood - `AddRCommon()` creates an `RCommonBuilder` and registers the core services: the in-memory event bus, the `EventSubscriptionManager`, and the `InMemoryTransactionalEventRouter`. - `WithSequentialGuidGenerator` registers `IGuidGenerator` as a transient `SequentialGuidGenerator`, useful for database-friendly ordered GUIDs. - `WithDateTimeSystem` registers `ISystemTime` so that auditing and time-dependent logic is testable. - `WithPersistence` registers the open-generic repository types (`ILinqRepository<>`, `IReadOnlyRepository<>`, `IWriteOnlyRepository<>`) backed by EF Core, and maps the named DbContext to the data store factory. - `WithUnitOfWork` registers `IUnitOfWork` and `IUnitOfWorkFactory` for transactional coordination. From here you can add CQRS mediation, event handling, validation, or any other RCommon feature by chaining additional `With*` calls on the builder. :::tip Modular apps `AddRCommon()` can be called from multiple modules safely — repeat calls return the same cached builder, sub-builder verbs (`WithPersistence`, `WithMediator`, `WithEventHandling`, etc.) reuse the same sub-builder, and singleton-style verbs are idempotent on identical impls. Registrations merge instead of throwing. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: --- ## RCommon Documentation Source: https://rcommon.com/docs/index Complete documentation for RCommon — a .NET infrastructure library with persistence, CQRS, event handling, messaging, caching, and validation abstractions. # RCommon Documentation Welcome to the RCommon documentation. Use the sidebar to navigate through the available topics. --- ## MassTransit Source: https://rcommon.com/docs/messaging/masstransit Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free. # MassTransit RCommon integrates with MassTransit to deliver events to consumers across service boundaries. The integration registers `MassTransitEventHandler` as a MassTransit `IConsumer` that delegates to the application's `ISubscriber`. Handler code has no dependency on MassTransit types. ## Installation ## Configuration Use `WithEventHandling` on the RCommon builder. The builder inherits from MassTransit's `ServiceCollectionBusConfigurator`, so all standard MassTransit configuration APIs are available directly on it: ```csharp using RCommon; using RCommon.MassTransit; using RCommon.MassTransit.Producers; builder.Services.AddRCommon() .WithEventHandling(mt => { // Register the producer that publishes events to the broker mt.AddProducer(); // Register subscribers; each call also adds a MassTransit consumer mt.AddSubscriber(); mt.AddSubscriber(); // Standard MassTransit transport configuration mt.UsingRabbitMq((ctx, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(ctx); }); }); ``` `AddSubscriber` performs three registrations in one call: 1. Registers `ISubscriber` in the DI container as transient. 2. Calls `AddConsumer>` so MassTransit creates a consumer for the endpoint. 3. Records the event-to-producer association in the `EventSubscriptionManager` so the router routes this event only to MassTransit producers. :::tip Modular composition `WithEventHandling` participates in the cache-aware sub-builder contract: a second call with the same builder type reuses the cached `MassTransitEventHandlingBuilder`, and subscriber/producer registrations from each module accumulate against the one instance. `AddProducer` deduplicates by concrete producer type, so the same producer registered from two modules yields exactly one descriptor. **Known limitation — cross-module composition.** MassTransit's `AddMassTransit(...)` includes a pre-existing `IBus` guard: invoking it again after the bus has been registered throws `MassTransit.ConfigurationException`. RCommon's bootstrapper caches the sub-builder, but the underlying MassTransit guard runs the first time `WithEventHandling` is called. Single-module usage is unchanged. A second module that calls `WithEventHandling` will throw `MassTransit.ConfigurationException`. Tracked as a follow-up; for now, funnel all MassTransit wiring through one module and let other modules use `AddSubscriber` against the shared cached builder via `IRCommonBuilder.GetOrAddBuilder()` if cross-module subscribers are required. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Defining an event Events must implement `ISerializableEvent` and must include a parameterless constructor for broker deserialization: ```csharp using RCommon.Models.Events; public class OrderShipped : ISyncEvent { public OrderShipped(Guid orderId, string trackingNumber) { OrderId = orderId; TrackingNumber = trackingNumber; } // Required for MassTransit deserialization public OrderShipped() { } public Guid OrderId { get; } public string TrackingNumber { get; } = string.Empty; } ``` ## Implementing a subscriber (consumer) Implement `ISubscriber`. No MassTransit types appear in handler code: ```csharp using RCommon.EventHandling.Subscribers; public class OrderShippedHandler : ISubscriber { private readonly ILogger _logger; public OrderShippedHandler(ILogger logger) { _logger = logger; } public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default) { _logger.LogInformation( "Order {OrderId} shipped with tracking {TrackingNumber}", @event.OrderId, @event.TrackingNumber); await Task.CompletedTask; } } ``` ## Producing events Queue events on `IEventRouter` and route them after your database work is complete. The router forwards each event to `PublishWithMassTransitEventProducer`, which calls `IBus.Publish`: ```csharp public class ShippingService { private readonly IEventRouter _eventRouter; public ShippingService(IEventRouter eventRouter) { _eventRouter = eventRouter; } public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken) { // ... perform database work ... _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber)); await _eventRouter.RouteEventsAsync(cancellationToken); } } ``` ## Publish vs Send Two producers are available. Choose based on the delivery semantics you need: | Producer | MassTransit call | Use when | |----------|-----------------|---------| | `PublishWithMassTransitEventProducer` | `IBus.Publish` | Fan-out: all consumers subscribed to the type receive the message | | `SendWithMassTransitEventProducer` | `IBus.Send` | Point-to-point: a single endpoint receives the message | Register `SendWithMassTransitEventProducer` instead of (or alongside) `PublishWithMassTransitEventProducer`: ```csharp mt.AddProducer(); ``` ## Transport configuration The builder inherits all MassTransit bus configuration methods. Any transport supported by MassTransit can be used: ```csharp // RabbitMQ mt.UsingRabbitMq((ctx, cfg) => { cfg.Host("amqp://guest:guest@localhost/"); cfg.ConfigureEndpoints(ctx); }); // Azure Service Bus mt.UsingAzureServiceBus((ctx, cfg) => { cfg.Host("Endpoint=sb://..."); cfg.ConfigureEndpoints(ctx); }); // In-memory (for testing) mt.UsingInMemory((ctx, cfg) => { cfg.ConfigureEndpoints(ctx); }); ``` ## Transactional outbox Pair MassTransit with the outbox pattern for guaranteed at-least-once delivery. See [Transactional Outbox](./transactional-outbox.mdx). ## How the consumer bridge works `MassTransitEventHandler` implements both `IMassTransitEventHandler` and MassTransit's `IConsumer`. When MassTransit delivers a message it calls `Consume(ConsumeContext)`, which resolves `ISubscriber` from DI and calls `HandleAsync`: ```csharp // Framework code — you do not write this yourself public class MassTransitEventHandler : IConsumer where TEvent : class, ISerializableEvent { public async Task Consume(ConsumeContext context) { await _subscriber.HandleAsync(context.Message, context.CancellationToken); } } ``` This keeps application handler code free of any MassTransit API surface. ## API summary | Type | Package | Description | |------|---------|-------------| | `MassTransitEventHandlingBuilder` | `RCommon.MassTransit` | Builder used with `WithEventHandling`; inherits `ServiceCollectionBusConfigurator` | | `IMassTransitEventHandlingBuilder` | `RCommon.MassTransit` | Interface combining `IEventHandlingBuilder` and `IBusRegistrationConfigurator` | | `PublishWithMassTransitEventProducer` | `RCommon.MassTransit` | `IEventProducer` that calls `IBus.Publish` (fan-out) | | `SendWithMassTransitEventProducer` | `RCommon.MassTransit` | `IEventProducer` that calls `IBus.Send` (point-to-point) | | `MassTransitEventHandler` | `RCommon.MassTransit` | Internal `IConsumer` that bridges MassTransit to `ISubscriber` | | `IMassTransitEventHandler` | `RCommon.MassTransit` | Marker interface for MassTransit event handlers | | `AddSubscriber` | `RCommon.MassTransit` | Extension on `IMassTransitEventHandlingBuilder`; registers handler, consumer, and routing | --- ## Overview Source: https://rcommon.com/docs/messaging/overview Overview of RCommon's broker-backed messaging layer built on IEventRouter and IEventProducer, comparing MassTransit and Wolverine transports for cross-service delivery. # Messaging Overview RCommon's messaging layer sits on top of the event handling infrastructure and provides transport-backed, cross-service message delivery. It reuses the same `ISubscriber` interface as in-process event handling, so handler code is portable across transports. ## Message bus vs event bus | Concern | In-process event bus | Messaging (MassTransit / Wolverine) | |---------|---------------------|-------------------------------------| | Scope | Single process | Cross-service, cross-host | | Delivery | Synchronous in-process dispatch | Asynchronous via a broker | | Durability | None (lost on process exit) | At-least-once via outbox or broker retry | | Handler interface | `ISubscriber` | `ISubscriber` (identical) | | Transport | None | RabbitMQ, Azure Service Bus, SQS, etc. | Use the in-process event bus when side effects are local to the same service. Switch to messaging when an event must cross a service boundary, survive a restart, or fan out to multiple independent services. ## IEventBus for messaging `IEventBus` is the in-process bus that dispatches events to registered `ISubscriber` implementations within the current process. When you integrate with MassTransit or Wolverine, the same `IEventBus` pipeline is still available; the difference is that each `IEventProducer` registered with the builder routes events to the external broker instead of dispatching them directly in memory. ```csharp // IEventBus dispatches in-process; producers dispatch to the broker public interface IEventBus { Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default); } ``` For broker-bound events the preferred path is through `IEventRouter`, which batches transactional events and dispatches them after the current database transaction commits: ```csharp public interface IEventRouter { void AddTransactionalEvent(ISerializableEvent serializableEvent); Task RouteEventsAsync(CancellationToken cancellationToken = default); } ``` ## When to use messaging - An event must be consumed by a handler in a different service or process. - You need guaranteed delivery even if a service is temporarily unavailable (use with the transactional outbox). - You want to fan out a single event to multiple independent downstream services without coupling them at the code level. - You need durable, retryable processing with dead-letter queues and retry policies provided by a broker. ## Choosing a transport RCommon supports two messaging transports: | Transport | Package | Best for | |-----------|---------|---------| | MassTransit | `RCommon.MassTransit` | Teams already using MassTransit; broad broker support (RabbitMQ, Azure Service Bus, SQS, in-memory) | | Wolverine | `RCommon.Wolverine` | Applications that need Wolverine's built-in durable messaging, saga support, and tight EF Core integration | Both transports expose the same RCommon interfaces. You can switch transports by changing the builder type passed to `WithEventHandling` without changing any handler code. ## How the pipeline works 1. Application code calls `IEventRouter.AddTransactionalEvent` to queue an event. 2. After a database transaction commits, `IEventRouter.RouteEventsAsync` is called. 3. The router inspects each event and forwards it only to the `IEventProducer` instances subscribed to that event type. 4. The producer (e.g. `PublishWithMassTransitEventProducer`) sends the serialized message to the broker. 5. The broker delivers the message to the appropriate consumer endpoint. 6. The consumer (`MassTransitEventHandler` or `WolverineEventHandler`) resolves the `ISubscriber` from the DI container and calls `HandleAsync`. ## Event type requirements Events that flow through a broker must implement `ISerializableEvent` and must include a parameterless constructor for deserialization: ```csharp using RCommon.Models.Events; public class OrderShipped : ISyncEvent { public OrderShipped(Guid orderId, string trackingNumber) { OrderId = orderId; TrackingNumber = trackingNumber; } // Required for broker deserialization public OrderShipped() { } public Guid OrderId { get; } public string TrackingNumber { get; } = string.Empty; } ``` Two sub-markers control how the router dispatches multiple events at once: | Interface | Behaviour | |-----------|-----------| | `ISyncEvent` | Events are produced sequentially | | `IAsyncEvent` | Events are produced concurrently via `Task.WhenAll` | ## Section contents - [MassTransit](./masstransit.mdx) — Setup, consumers, producers, transport configuration - [Wolverine](./wolverine.mdx) — Setup, message handlers, durable messaging - [Transactional Outbox](./transactional-outbox.mdx) — Guaranteed at-least-once delivery with MassTransit and Wolverine - [State Machines](./state-machines.mdx) — MassTransit saga/state machine integration --- ## State Machines Source: https://rcommon.com/docs/messaging/state-machines Orchestrate long-running workflows in RCommon using MassTransit's dictionary-based state machine adapter with IStateMachineConfigurator, guards, and async entry/exit actions. # State Machines (MassTransit Sagas) RCommon provides a lightweight state machine abstraction that can be backed by either the `Stateless` library or a MassTransit-compatible dictionary-based implementation. The MassTransit adapter (`RCommon.MassTransit.StateMachines`) integrates with the RCommon builder pipeline and is appropriate for orchestrating multi-step workflows in messaging scenarios. ## Overview A state machine tracks the lifecycle of a long-running process. It defines: - **States** — discrete conditions the process can be in (e.g. `Pending`, `Processing`, `Completed`) - **Triggers** — events that cause transitions between states (e.g. `Submit`, `Approve`, `Reject`) - **Transitions** — rules that map a (state, trigger) pair to a destination state, optionally guarded by a condition - **Entry/exit actions** — async callbacks invoked when the machine enters or leaves a state The MassTransit state machine adapter is independent of MassTransit Automatonymous sagas. It uses the same `IStateMachineConfigurator` abstraction as the Stateless adapter, so you can swap implementations without changing business logic. ## Installation ## Configuration Register the MassTransit state machine adapter with `WithMassTransitStateMachine`: ```csharp using RCommon; builder.Services.AddRCommon() .WithMassTransitStateMachine(); ``` This registers `MassTransitStateMachineConfigurator` as the `IStateMachineConfigurator` implementation. The registration is open-generic, so any combination of state and trigger enum types is supported without further configuration. ## Defining states and triggers States and triggers are plain enums: ```csharp public enum OrderState { Pending, Approved, Shipped, Cancelled } public enum OrderTrigger { Approve, Ship, Cancel } ``` ## Configuring transitions Inject `IStateMachineConfigurator`, call `ForState` for each state, then call `Build` to produce a running machine instance: ```csharp public class OrderWorkflow { private readonly IStateMachineConfigurator _configurator; public OrderWorkflow(IStateMachineConfigurator configurator) { _configurator = configurator; _configurator.ForState(OrderState.Pending) .Permit(OrderTrigger.Approve, OrderState.Approved) .Permit(OrderTrigger.Cancel, OrderState.Cancelled); _configurator.ForState(OrderState.Approved) .Permit(OrderTrigger.Ship, OrderState.Shipped) .Permit(OrderTrigger.Cancel, OrderState.Cancelled) .OnEntry(async ct => { // side-effect when order is approved await NotifyWarehouseAsync(ct); }); _configurator.ForState(OrderState.Shipped) .OnEntry(async ct => { await SendShippingConfirmationAsync(ct); }); } public IStateMachine CreateFor(OrderState currentState) => _configurator.Build(currentState); } ``` ## Guarded transitions Use `PermitIf` to add a condition to a transition: ```csharp _configurator.ForState(OrderState.Approved) .PermitIf(OrderTrigger.Ship, OrderState.Shipped, () => inventoryAvailable) .Permit(OrderTrigger.Cancel, OrderState.Cancelled); ``` The guard is a synchronous `Func`. If the guard returns `false` the trigger cannot fire from that state. ## Firing triggers Once a machine instance is built, fire triggers asynchronously: ```csharp var machine = _orderWorkflow.CreateFor(order.State); if (machine.CanFire(OrderTrigger.Approve)) { await machine.FireAsync(OrderTrigger.Approve, cancellationToken); order.State = machine.CurrentState; // persist new state } ``` You can also pass data with a trigger: ```csharp await machine.FireAsync(OrderTrigger.Ship, shipmentDetails, cancellationToken); ``` Note that the MassTransit dictionary-based adapter accepts parameterized trigger calls but does not use the data parameter — the transition executes identically to a parameterless fire. Use this overload for interface compatibility. ## IStateMachine members | Member | Description | |--------|-------------| | `CurrentState` | The current state of the machine | | `CanFire(trigger)` | Returns `true` if the trigger is permitted from the current state | | `PermittedTriggers` | All triggers that can currently be fired | | `FireAsync(trigger, ct)` | Fires a trigger, executing exit/entry actions and transitioning state | | `FireAsync(trigger, data, ct)` | Fires a trigger with associated data | ## IStateConfigurator members | Method | Description | |--------|-------------| | `Permit(trigger, dest)` | Adds an unconditional transition | | `PermitIf(trigger, dest, guard)` | Adds a guarded transition that fires only when `guard()` returns `true` | | `OnEntry(action)` | Registers an async action invoked when this state is entered | | `OnExit(action)` | Registers an async action invoked when this state is exited | ## Comparison with Stateless Both adapters implement the same `IStateMachineConfigurator` and `IStateMachine` interfaces. The choice affects the underlying engine: | Aspect | `RCommon.MassTransit.StateMachines` | `RCommon.Stateless` | |--------|-------------------------------------|---------------------| | Backing library | Custom dictionary-based FSM | `Stateless` NuGet | | Parameterized triggers | Accepted but data is ignored | Fully supported via `SetTriggerParameters` | | Configuration reuse | Single configurator produces independent machine instances | Single configurator produces independent machine instances | | Registration method | `WithMassTransitStateMachine()` | `WithStatelessStateMachine()` | For full parameterized trigger support, prefer the Stateless adapter. See [Stateless](../state-machines/stateless.mdx). ## API summary | Type | Package | Description | |------|---------|-------------| | `MassTransitStateMachineConfigurator` | `RCommon.MassTransit.StateMachines` | Implements `IStateMachineConfigurator`; builds `MassTransitStateMachine` instances | | `MassTransitStateMachine` | `RCommon.MassTransit.StateMachines` | Dictionary-based `IStateMachine` implementation | | `MassTransitStateConfigurator` | `RCommon.MassTransit.StateMachines` | Per-state configuration storing transitions and actions | | `IStateMachineConfigurator` | `RCommon.Core` | Abstraction for building state machines | | `IStateMachine` | `RCommon.Core` | Running state machine abstraction | | `IStateConfigurator` | `RCommon.Core` | Per-state transition and action configuration | | `WithMassTransitStateMachine()` | `RCommon.MassTransit.StateMachines` | Extension method on `IRCommonBuilder` | --- ## Transactional Outbox Source: https://rcommon.com/docs/messaging/transactional-outbox Ensure messages reach the broker only when the database transaction commits using the transactional outbox for MassTransit or Wolverine with EF Core integration. # Transactional Outbox The transactional outbox pattern guarantees that events are published to a message broker if and only if the database transaction that triggered them commits. RCommon provides outbox integration for both MassTransit and Wolverine. ## The problem the outbox solves Without an outbox there is a window between committing a database change and publishing an event where a crash can cause the event to be lost. Conversely, publishing an event before committing risks publishing for a transaction that is later rolled back. The outbox eliminates both failure modes by writing the event to the same database transaction as the business data, then relaying it to the broker after the transaction commits. ## MassTransit outbox ### Installation MassTransit's Entity Framework Core outbox stores pending messages in the same `DbContext` as your application data. The outbox tables are added to any `DbContext` you supply. ### Configuration Call `AddOutbox` on the `IMassTransitEventHandlingBuilder` returned by `WithEventHandling`: ```csharp using RCommon; using RCommon.MassTransit; using RCommon.MassTransit.Producers; builder.Services.AddRCommon() .WithEventHandling(mt => { mt.AddProducer(); mt.AddSubscriber(); // Configure the transactional outbox backed by AppDbContext mt.AddOutbox(outbox => { outbox.UseSqlServer(); // or outbox.UsePostgres() outbox.UseBusOutbox(); }); mt.UsingRabbitMq((ctx, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(ctx); }); }); ``` ### IMassTransitOutboxBuilder members | Method | Description | |--------|-------------| | `UseSqlServer()` | Configures the outbox to use the SQL Server dialect | | `UsePostgres()` | Configures the outbox to use the PostgreSQL dialect | | `UseBusOutbox(configure?)` | Enables the bus outbox relay; accepts an optional `IBusOutboxConfigurator` action | ### Applying outbox migrations The MassTransit outbox adds its own tables to the `DbContext`. Add and apply a migration after configuring the outbox: ```bash dotnet ef migrations add AddMassTransitOutbox dotnet ef database update ``` ## Wolverine outbox ### Installation Wolverine's outbox integrates with Entity Framework Core transactions, persisting messages alongside application data in the same transaction scope. ### Configuration Call `AddOutbox` on the `IWolverineEventHandlingBuilder`: ```csharp using RCommon; using RCommon.Wolverine; using RCommon.Wolverine.Producers; builder.Host.UseWolverine(); builder.Services.AddRCommon() .WithEventHandling(wlv => { wlv.AddProducer(); wlv.AddSubscriber(); wlv.AddOutbox(outbox => { outbox.UseEntityFrameworkCoreTransactions(); }); }); ``` ### IWolverineOutboxBuilder members | Method | Description | |--------|-------------| | `UseEntityFrameworkCoreTransactions()` | Integrates Wolverine's outbox with EF Core transaction management | ## How delivery works Once the outbox is configured: 1. When `IEventRouter.RouteEventsAsync` is called, the producer writes the message to the outbox table inside the current database transaction rather than sending it directly to the broker. 2. After the transaction commits, a background relay process (managed by MassTransit or Wolverine) reads pending messages from the outbox table and forwards them to the broker. 3. Each message is deleted from the outbox only after the broker acknowledges receipt. This gives at-least-once delivery semantics. Consumers must be idempotent to handle the rare case of duplicate delivery after a relay failure. ## API summary ### MassTransit outbox | Type | Package | Description | |------|---------|-------------| | `MassTransitOutboxBuilderExtensions.AddOutbox` | `RCommon.MassTransit.Outbox` | Registers the EF Core outbox for the given `DbContext` | | `IMassTransitOutboxBuilder` | `RCommon.MassTransit.Outbox` | Builder exposing dialect and bus relay configuration | | `MassTransitOutboxBuilder` | `RCommon.MassTransit.Outbox` | Concrete implementation wrapping `IEntityFrameworkOutboxConfigurator` | ### Wolverine outbox | Type | Package | Description | |------|---------|-------------| | `WolverineOutboxBuilderExtensions.AddOutbox` | `RCommon.Wolverine.Outbox` | Registers Wolverine's outbox via `ConfigureWolverine` | | `IWolverineOutboxBuilder` | `RCommon.Wolverine.Outbox` | Builder for EF Core transaction integration | | `WolverineOutboxBuilder` | `RCommon.Wolverine.Outbox` | Concrete implementation wrapping `WolverineOptions` | --- ## Wolverine Source: https://rcommon.com/docs/messaging/wolverine Configure Wolverine as RCommon's messaging transport with durable delivery, fan-out publish or point-to-point send, and ISubscriber handlers free of Wolverine dependencies. # Wolverine RCommon integrates with Wolverine to deliver events both in-process and across service boundaries. The integration registers `WolverineEventHandler` as a Wolverine `IWolverineHandler` that delegates to the application's `ISubscriber`. Handler code has no dependency on Wolverine types. ## Installation Wolverine requires host-level registration via `UseWolverine()` in addition to the RCommon service configuration: ```csharp builder.Host.UseWolverine(opts => { // Wolverine transport and endpoint options go here }); ``` ## Configuration Use `WithEventHandling` on the RCommon builder: ```csharp using RCommon; using RCommon.Wolverine; using RCommon.Wolverine.Producers; builder.Services.AddRCommon() .WithEventHandling(wlv => { // Register the producer that publishes events via Wolverine wlv.AddProducer(); // Register subscribers and record routing information wlv.AddSubscriber(); wlv.AddSubscriber(); }); ``` `AddSubscriber` registers `ISubscriber` as scoped in the DI container and records the event-to-producer association in the `EventSubscriptionManager` so the router routes this event only to Wolverine producers. An overload accepts a factory delegate when you need more control over how the subscriber is resolved: ```csharp wlv.AddSubscriber( sp => new OrderShippedHandler(sp.GetRequiredService())); ``` ## Defining an event Events must implement `ISerializableEvent`. Include a parameterless constructor for Wolverine deserialization: ```csharp using RCommon.Models.Events; public class OrderShipped : ISyncEvent { public OrderShipped(Guid orderId, string trackingNumber) { OrderId = orderId; TrackingNumber = trackingNumber; } // Required for Wolverine deserialization public OrderShipped() { } public Guid OrderId { get; } public string TrackingNumber { get; } = string.Empty; } ``` ## Implementing a subscriber (handler) Implement `ISubscriber`. No Wolverine types appear in handler code: ```csharp using RCommon.EventHandling.Subscribers; public class OrderShippedHandler : ISubscriber { private readonly ILogger _logger; public OrderShippedHandler(ILogger logger) { _logger = logger; } public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default) { _logger.LogInformation( "Order {OrderId} shipped with tracking {TrackingNumber}", @event.OrderId, @event.TrackingNumber); await Task.CompletedTask; } } ``` ## Producing events Queue events on `IEventRouter` and route them after your database work is complete. The router forwards each event to `PublishWithWolverineEventProducer`, which calls `IMessageBus.PublishAsync`: ```csharp public class ShippingService { private readonly IEventRouter _eventRouter; public ShippingService(IEventRouter eventRouter) { _eventRouter = eventRouter; } public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken) { // ... perform database work ... _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber)); await _eventRouter.RouteEventsAsync(cancellationToken); } } ``` ## Publish vs Send Two producers are available: | Producer | Wolverine call | Use when | |----------|---------------|---------| | `PublishWithWolverineEventProducer` | `IMessageBus.PublishAsync` | Fan-out: all handlers subscribed to the type receive the message | | `SendWithWolverineEventProducer` | `IMessageBus.SendAsync` | Point-to-point: a single endpoint receives the message | Register `SendWithWolverineEventProducer` for command-style messaging: ```csharp wlv.AddProducer(); ``` ## Durable messaging Wolverine supports durable, transactional messaging through its built-in persistence layer. Configure Wolverine's persistence in the `UseWolverine` callback: ```csharp builder.Host.UseWolverine(opts => { // Use Wolverine's Marten integration for durable messaging (example) // opts.PersistMessagesWithMarten(); // Or EF Core-based persistence — see the Transactional Outbox page }); ``` For guaranteed at-least-once delivery using EF Core, see [Transactional Outbox](./transactional-outbox.mdx). ## How the handler bridge works `WolverineEventHandler` implements both `IWolverineEventHandler` and Wolverine's `IWolverineHandler`. When Wolverine delivers a message it calls `HandleAsync(TEvent, CancellationToken)`, which delegates to the registered `ISubscriber`: ```csharp // Framework code — you do not write this yourself public class WolverineEventHandler : IWolverineHandler where TEvent : class, ISerializableEvent { public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default) { await _subscriber.HandleAsync(@event, cancellationToken); } } ``` This keeps application handler code free of any Wolverine API surface. ## API summary | Type | Package | Description | |------|---------|-------------| | `WolverineEventHandlingBuilder` | `RCommon.Wolverine` | Builder used with `WithEventHandling` to configure Wolverine event handling | | `IWolverineEventHandlingBuilder` | `RCommon.Wolverine` | Extends `IEventHandlingBuilder` with Wolverine-specific subscription capabilities | | `PublishWithWolverineEventProducer` | `RCommon.Wolverine` | `IEventProducer` that calls `IMessageBus.PublishAsync` (fan-out) | | `SendWithWolverineEventProducer` | `RCommon.Wolverine` | `IEventProducer` that calls `IMessageBus.SendAsync` (point-to-point) | | `WolverineEventHandler` | `RCommon.Wolverine` | Internal `IWolverineHandler` that bridges Wolverine to `ISubscriber` | | `IWolverineEventHandler` | `RCommon.Wolverine` | Marker interface for Wolverine event handlers | | `AddSubscriber` | `RCommon.Wolverine` | Extension on `IWolverineEventHandlingBuilder`; registers handler and routing | --- ## finbuckle Source: https://rcommon.com/docs/multi-tenancy/finbuckle --- title: Finbuckle sidebar_position: 2 description: RCommon.Finbuckle integrates Finbuckle.MultiTenant with RCommon's persistence layer, automatically scoping queries and entity writes to the request-resolved tenant. --- # Finbuckle ## Overview `RCommon.Finbuckle` integrates [Finbuckle.MultiTenant](https://www.finbuckle.com/multitenant) with RCommon's persistence layer. Finbuckle handles all tenant resolution — inspecting HTTP headers, route values, host names, JWT claims, or any custom strategy you configure. RCommon's side of the integration is a single adapter class, `FinbuckleTenantIdAccessor`, which implements `ITenantIdAccessor` by reading the tenant ID from Finbuckle's `IMultiTenantContextAccessor`. When you call `WithMultiTenancy>` at startup, `FinbuckleMultiTenantBuilder` replaces the default `NullTenantIdAccessor` registration with `FinbuckleTenantIdAccessor`. From that point on every repository operation that touches an `IMultiTenant` entity is automatically scoped to whichever tenant Finbuckle has resolved for the current request. :::warning RCommon.Finbuckle does not wire Finbuckle's EF Core integration Tenant enforcement here is entirely repository-level: `MultiTenantHelper`, driven by `ITenantIdAccessor.GetTenantId()`. `RCommon.Finbuckle` never touches Finbuckle's own EF Core integration (`IMultiTenantDbContext` / `EnforceMultiTenant()`) — `RCommonDbContext` does not implement it and nothing in this package configures it. If you layer Finbuckle's `IMultiTenantDbContext`/`EnforceMultiTenant()` on top of `RCommonDbContext` yourself, be aware that `EnforceMultiTenant()` is Finbuckle's own mechanism and can throw *before* Finbuckle's `TenantMismatchMode` is consulted, independent of anything RCommon does. That behavior belongs to Finbuckle, not to `RCommon.Finbuckle`, and nothing in this package mitigates it. ::: ### How tenant resolution flows 1. An HTTP request arrives. ASP.NET Core runs the Finbuckle middleware which identifies the tenant using the strategy you configured (header, route segment, hostname, etc.) and sets `IMultiTenantContextAccessor.MultiTenantContext`. 2. A repository method executes. It calls `ITenantIdAccessor.GetTenantId()`. 3. `FinbuckleTenantIdAccessor` reads `MultiTenantContext.TenantInfo.Id` and returns it. 4. The repository appends a tenant filter to the query or stamps the entity's `TenantId` property. ## Installation This package depends on `RCommon.MultiTenancy` and `Finbuckle.MultiTenant.Core`, which are pulled in automatically. ## Configuration ### Basic setup Configure Finbuckle using its own API first, then wire it into RCommon with `WithMultiTenancy`: ```csharp using RCommon; using RCommon.Finbuckle; using Finbuckle.MultiTenant; var builder = WebApplication.CreateBuilder(args); // 1. Configure Finbuckle tenant resolution. builder.Services.AddMultiTenant() .WithHeaderStrategy("X-Tenant-Id") .WithConfigurationStore(); // 2. Configure RCommon, including the Finbuckle multi-tenancy adapter. builder.Services.AddRCommon(config => { config .WithClaimsAndPrincipalAccessorForWeb() .WithPersistence(ef => { ef.AddDbContext( "App", options => options.UseSqlServer( builder.Configuration.GetConnectionString("Default"))); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "App"); }) .WithMultiTenancy>(mt => { // FinbuckleTenantIdAccessor is registered automatically. }); }); var app = builder.Build(); // 3. Add the Finbuckle middleware so tenant resolution runs on every request. app.UseMultiTenant(); app.MapControllers(); app.Run(); ``` ### Custom TenantInfo Finbuckle requires a class that implements `ITenantInfo`. You can use the built-in `TenantInfo` or define your own: ```csharp using Finbuckle.MultiTenant.Abstractions; public class AppTenantInfo : ITenantInfo { public string? Id { get; set; } public string? Identifier { get; set; } public string? Name { get; set; } // Custom properties for your application. public string? ConnectionString { get; set; } public string? Theme { get; set; } } ``` Pass your custom type as the generic argument everywhere: ```csharp builder.Services.AddMultiTenant() .WithRouteStrategy("tenant") .WithEFCoreStore(); builder.Services.AddRCommon(config => { config.WithMultiTenancy>(mt => { }); }); ``` ### Tenant resolution strategies Finbuckle ships with several built-in strategies. Choose the one that fits your application's URL scheme: ```csharp // Resolve from a request header (e.g. X-Tenant-Id: acme). builder.Services.AddMultiTenant() .WithHeaderStrategy("X-Tenant-Id") .WithConfigurationStore(); // Resolve from a route parameter (e.g. /acme/orders). builder.Services.AddMultiTenant() .WithRouteStrategy("tenant") .WithConfigurationStore(); // Resolve from the hostname (e.g. acme.myapp.com). builder.Services.AddMultiTenant() .WithHostStrategy("__tenant__.*") .WithConfigurationStore(); // Resolve from a JWT claim. builder.Services.AddMultiTenant() .WithClaimStrategy("tenantid") .WithConfigurationStore(); ``` ### Tenant stores Finbuckle resolves tenant configuration from a store. Common options: ```csharp // Store tenant definitions in appsettings.json under "Finbuckle:MultiTenant:Stores:ConfigurationStore". builder.Services.AddMultiTenant() .WithHeaderStrategy("X-Tenant-Id") .WithConfigurationStore(); // Store tenants in an EF Core database table. builder.Services.AddMultiTenant() .WithHeaderStrategy("X-Tenant-Id") .WithEFCoreStore(); // In-memory store — useful for testing. builder.Services.AddMultiTenant() .WithHeaderStrategy("X-Tenant-Id") .WithInMemoryStore(options => { options.Tenants.Add(new TenantInfo { Id = "tenant-1", Identifier = "acme", Name = "Acme Corp" }); }); ``` ## Usage :::tip Runnable example See `Examples/MultiTenancy/Examples.MultiTenancy.Finbuckle/` for a complete console app demonstrating tenant-scoped reads/writes across two tenants and `TenantScope.Bypass()` for a cross-tenant admin view — no ASP.NET Core host or HTTP requests required; the tenant context is set directly via Finbuckle's `IMultiTenantContextSetter`. ::: ### Defining tenant-scoped entities Implement `IMultiTenant` on entities that should be isolated per tenant: ```csharp using RCommon.Entities; public class Order : BusinessEntity, IMultiTenant { public string CustomerName { get; set; } = string.Empty; public decimal Total { get; set; } public OrderStatus Status { get; set; } // Populated automatically by the repository. public string? TenantId { get; set; } } ``` ### Reading tenant-scoped data Inject the repository and query as normal. Tenant filtering is applied transparently: ```csharp public class OrderService { private readonly IGraphRepository _orders; public OrderService(IGraphRepository orders) { _orders = orders; } public async Task> GetPendingOrdersAsync(CancellationToken ct) { // Only returns orders for the tenant resolved by Finbuckle on this request. return await _orders.FindAsync(o => o.Status == OrderStatus.Pending, ct); } } ``` ### Writing tenant-scoped data ```csharp public async Task PlaceOrderAsync(PlaceOrderRequest request, CancellationToken ct) { var order = new Order { CustomerName = request.CustomerName, Total = request.Total, Status = OrderStatus.New // TenantId is not set here; the repository stamps it automatically. }; await _orders.AddAsync(order, ct); return order.Id; } ``` ### Accessing the current tenant directly If you need the tenant information outside of a repository, inject Finbuckle's context accessor: ```csharp using Finbuckle.MultiTenant.Abstractions; public class TenantAwareService { private readonly IMultiTenantContextAccessor _tenantAccessor; public TenantAwareService(IMultiTenantContextAccessor tenantAccessor) { _tenantAccessor = tenantAccessor; } public string? GetCurrentTenantName() { return _tenantAccessor.MultiTenantContext?.TenantInfo?.Name; } } ``` Or use RCommon's `ITenantIdAccessor` when you only need the ID: ```csharp using RCommon.Security.Claims; public class AuditService { private readonly ITenantIdAccessor _tenantIdAccessor; public AuditService(ITenantIdAccessor tenantIdAccessor) { _tenantIdAccessor = tenantIdAccessor; } public void LogAction(string action) { var tenantId = _tenantIdAccessor.GetTenantId(); // Use tenantId in your audit log entry. } } ``` ## API Summary | Type | Description | |------|-------------| | `FinbuckleMultiTenantBuilder` | Registers `FinbuckleTenantIdAccessor` and implements `IMultiTenantBuilder`; use as the type argument to `WithMultiTenancy` | | `IFinbuckleMultiTenantBuilder` | Marker interface extending `IMultiTenantBuilder`; scoped to `TTenantInfo : ITenantInfo` | | `FinbuckleTenantIdAccessor` | Implements `ITenantIdAccessor` by reading `IMultiTenantContextAccessor.MultiTenantContext.TenantInfo.Id` | | `ITenantIdAccessor` | RCommon's abstraction; `GetTenantId()` returns the current tenant ID or `null` | --- ## overview Source: https://rcommon.com/docs/multi-tenancy/overview --- title: Overview sidebar_position: 1 description: Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant. --- # Multi-Tenancy Overview ## Overview Multi-tenancy support in RCommon allows a single application instance to serve multiple tenants while keeping their data isolated. The framework provides a thin abstraction layer over third-party tenant resolution libraries so that your domain and persistence code remains unaware of how tenants are identified. The design is split across two concerns: - **Tenant resolution** — determining which tenant is active for the current request. This is delegated to a provider (currently Finbuckle) that inspects the HTTP context, claims, route data, or any other signal you configure. - **Tenant filtering** — automatically scoping repository queries and entity stamps to the active tenant. This happens inside RCommon's repository implementations whenever the entity implements `IMultiTenant`. ### Core abstractions `ITenantIdAccessor` is the single point of integration between tenant resolution and the persistence layer. Its `GetTenantId()` method is called by repositories whenever they need to apply tenant isolation. The default registration (`NullTenantIdAccessor`) returns `null`, which disables filtering entirely — useful during bootstrapping or when multi-tenancy is not yet configured. `IMultiTenantBuilder` is the fluent builder interface that all provider-specific builders implement. It gives providers access to the DI service collection so they can replace `NullTenantIdAccessor` with their own implementation. `WithMultiTenancy` is the extension method on `IRCommonBuilder` that wires a provider into the RCommon startup pipeline. It constructs the builder by convention (via `Activator.CreateInstance`) and passes it to the configuration action you supply. ### Entity-level isolation Any entity that implements `IMultiTenant` participates in automatic isolation: ```csharp using RCommon.Entities; public class Invoice : BusinessEntity, IMultiTenant { public string Number { get; set; } = string.Empty; public decimal Amount { get; set; } // Populated automatically by the repository on add/update. public string? TenantId { get; set; } } ``` At query time the repository reads the current `TenantId` from `ITenantIdAccessor` and appends a filter expression so only records belonging to that tenant are returned. At write time the same value is stamped onto the entity before it is persisted. When `GetTenantId()` returns `null` or an empty string all tenant filtering is bypassed, which is intentional for administrative scenarios where a privileged caller needs cross-tenant access. ### Stamping semantics The exact guard applied when a repository writes an entity (`MultiTenantHelper.SetTenantIdIfApplicable`) is: - If the resolved tenant ID is `null` or empty, stamping is **skipped entirely** — the entity's existing `TenantId` (if any) is left untouched, not overwritten with `null`. - If the resolved tenant ID is non-empty, it **unconditionally overwrites** whatever the entity's `TenantId` already was. This asymmetry is what makes `TenantScope.Bypass()` (below) safe to use for the tenant-bootstrap scenario: suspending resolution down to `null` for the scope's lifetime means stamping is skipped, so an aggregate's explicitly-set `TenantId` survives `AddAsync` unmodified. ## Installation A provider package is also required. For Finbuckle: ## Configuration Call `WithMultiTenancy` inside your RCommon startup block, passing the concrete builder type for the provider you are using: ```csharp using RCommon; using RCommon.Finbuckle; using Finbuckle.MultiTenant; // Configure the tenant resolution strategy (Finbuckle's own API). builder.Services.AddMultiTenant() .WithHeaderStrategy("X-Tenant-Id") .WithConfigurationStore(); // Wire Finbuckle into RCommon. builder.Services.AddRCommon(config => { config .WithClaimsAndPrincipalAccessorForWeb() .WithPersistence(ef => { ef.AddDbContext( "App", options => options.UseSqlServer(connectionString)); }) .WithMultiTenancy>(mt => { // FinbuckleTenantIdAccessor is registered automatically. // No further configuration is required here unless you need // to register custom services against the builder. }); }); ``` :::tip Modular composition `WithMultiTenancy` is a cache-aware sub-builder verb. When multiple modules call `WithMultiTenancy>(...)` with the same builder type, the cached builder is reused and each module's configuration action runs against the same instance. `FinbuckleTenantIdAccessor` is registered exactly once. This lets modules independently add tenant-aware services (custom tenant stores, per-tenant options) without coordinating wiring. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Usage ### Tenant-scoped entities Mark entities with `IMultiTenant` to opt into automatic isolation: ```csharp public class Product : BusinessEntity, IMultiTenant { public string Name { get; set; } = string.Empty; public decimal Price { get; set; } // Set automatically; do not assign manually in normal usage. public string? TenantId { get; set; } } ``` Repository reads are filtered to the current tenant transparently: ```csharp // Returns only the current tenant's products. ICollection products = await _repository.FindAsync( p => p.Price > 0, cancellationToken); ``` Repository writes stamp the current tenant ID automatically: ```csharp // TenantId is populated from ITenantIdAccessor before INSERT. await _repository.AddAsync(new Product { Name = "Widget", Price = 9.99m }, cancellationToken); ``` ### Bypassing tenant isolation for one call: `TenantScope.Bypass()` Swapping in a different `ITenantIdAccessor` registration is an all-or-nothing, DI-configuration-time change — it affects every request for the life of the process. Most cross-tenant needs are narrower than that: one specific call (list every tenant a super-admin belongs to, create the first row for a brand-new tenant) that should skip ambient scoping without touching anything else. `TenantScope.Bypass()` (`RCommon.Security.Claims`) is a scoped, per-call primitive for exactly this. While the scope returned by `Bypass()` is active — including across `await` continuations, via `AsyncLocal` — any accessor wrapped with `TenantScopeAwareTenantIdAccessor` resolves to `null`, which every repository already treats as "skip filtering / skip stamping": ```csharp using RCommon.Security.Claims; public class TenantAdminService { private readonly IGraphRepository _tenants; public async Task> ListAllTenantsAsync(CancellationToken ct) { // Requires the caller to already be authorized for cross-tenant access -- // TenantScope.Bypass() has no authorization check of its own. using (TenantScope.Bypass()) { return await _tenants.FindAsync(t => true, ct); } } } ``` Always dispose the returned handle — a `using` block is strongly recommended. If it is never disposed (e.g. an exception path that skips the `using` block due to a bug in calling code), the bypass remains active for the rest of the current logical call context. Nested scopes compose correctly: disposing an inner scope while an outer scope is still active restores "bypassed," not "not bypassed." RCommon wraps its own built-in accessors with `TenantScopeAwareTenantIdAccessor` automatically at their existing registration sites — `ClaimsTenantIdAccessor` (via `WithClaimsAndPrincipalAccessor()`) and `FinbuckleTenantIdAccessor` (via `FinbuckleMultiTenantBuilder`) both honor `TenantScope.Bypass()` with no code change required. `NullTenantIdAccessor` is intentionally not wrapped, since it always returns `null` already. #### Worked scenario: tenant bootstrap Creating the very first row for a brand-new tenant is the one case where you *want* an explicitly-set `TenantId` to survive `AddAsync` untouched — not stamped with `null`, and not stamped with whatever tenant the caller happens to be ambiently scoped to: ```csharp public class CreateTenantCommandHandler { private readonly IAggregateRepository _tenants; public async Task HandleAsync(CreateTenantCommand cmd, CancellationToken ct) { using (TenantScope.Bypass()) { var tenant = new Tenant(cmd.NewTenantId, cmd.Name); // tenant.TenantId is set explicitly by the aggregate's own constructor. With the // bypass active, ITenantIdAccessor.GetTenantId() resolves to null, so the stamping // guard above skips stamping entirely -- the explicitly-set TenantId is left as-is. await _tenants.AddAsync(tenant, ct); } } } ``` #### Opting in a custom `ITenantIdAccessor` If you've written your own `ITenantIdAccessor` implementation instead of using `ClaimsTenantIdAccessor` or Finbuckle's accessor, wrap it with `TenantScopeAwareTenantIdAccessor` at your own registration site to get the same bypass support — RCommon has no way to intercept a registration it doesn't own: ```csharp services.AddTransient(); services.AddTransient(sp => new TenantScopeAwareTenantIdAccessor(sp.GetRequiredService())); ``` :::danger Authorization is the caller's responsibility `TenantScope.Bypass()` removes tenant isolation for its scope with no authorization check of its own — the same trust model as `ITenantIdAccessor` itself. Gate any code path that calls `Bypass()` behind your own authorization check (e.g. an admin role). RCommon has no way to know which callers should be allowed to see cross-tenant data. ::: ## API Summary | Type | Package | Description | |------|---------|-------------| | `ITenantIdAccessor` | `RCommon.Security` | Returns the current tenant ID; `null` disables all filtering | | `NullTenantIdAccessor` | `RCommon.Security` | Default implementation; always returns `null` | | `ClaimsTenantIdAccessor` | `RCommon.Security` | Reads tenant ID from the authenticated user's claims | | `TenantScope` | `RCommon.Security` | `Bypass()` suspends tenant scoping for a scoped, ambient, per-call lifetime | | `TenantScopeAwareTenantIdAccessor` | `RCommon.Security` | Decorator making any `ITenantIdAccessor` honor `TenantScope.Bypass()` | | `IMultiTenantBuilder` | `RCommon.MultiTenancy` | Builder interface that provider packages implement | | `MultiTenancyBuilderExtensions` | `RCommon.MultiTenancy` | Provides `WithMultiTenancy()` on `IRCommonBuilder` | | `IMultiTenant` | `RCommon.Entities` | Entity marker interface; exposes `string? TenantId` | --- ## Aggregate Repository Source: https://rcommon.com/docs/persistence/aggregate-repository Use IAggregateRepository for DDD-constrained aggregate root persistence across EF Core, Dapper, and Linq2Db, with provider-specific capability differences documented explicitly. # Aggregate Repository ## Overview `IAggregateRepository` is a narrower, DDD-constrained sibling to the general [Repository Pattern](./repository-pattern) interfaces. It only exposes aggregate-appropriate operations -- load by ID, find by specification, existence check, add, update, delete, and eager loading -- and does not expose `IQueryable` or arbitrary collection queries, since those belong on a read-model or query handler, not on the aggregate boundary. ```csharp public interface IAggregateRepository : INamedDataSource where TAggregate : class, IAggregateRoot where TKey : IEquatable { Task GetByIdAsync(TKey id, CancellationToken cancellationToken = default); Task FindAsync(ISpecification specification, CancellationToken cancellationToken = default); Task ExistsAsync(TKey id, CancellationToken cancellationToken = default); Task AddAsync(TAggregate aggregate, CancellationToken cancellationToken = default); Task UpdateAsync(TAggregate aggregate, CancellationToken cancellationToken = default); Task DeleteAsync(TAggregate aggregate, CancellationToken cancellationToken = default); IAggregateRepository Include(Expression> path); IAggregateRepository ThenInclude(Expression> path); } ``` ## Installation You will also need at least one provider package (see [EF Core](./efcore), [Dapper](./dapper), or [Linq2Db](./linq2db)) -- `IAggregateRepository<,>` is registered automatically alongside the other repository interfaces when you configure a provider. ## The persistence contract: root-row-only, on every provider **`AddAsync` and `UpdateAsync` guarantee persistence of the aggregate root's own row.** Whether a call also persists children reachable through a collection navigation depends on the provider and the operation: A few things follow directly from this table: - **`UpdateAsync` never persists child collections, on any provider.** This is deliberate, not a limitation to work around -- see "Adding or modifying children" below for the two fully-supported patterns. - **EF Core's `AddAsync` cascade-inserting brand-new child collections is an EF-specific convenience**, not a portable guarantee. If you need Dapper or Linq2Db portability, persist new children through their own repository even on initial aggregate creation. - **Dapper never supports eager loading** (`Include`/`ThenInclude` are no-ops that return `this` for fluent-chaining compatibility) -- Dommel is a micro-ORM over flat, single-table CRUD with no navigation-property concept. Load child collections via a separate query against the child's own repository. ## Adding or modifying children There are two fully-supported patterns, and one documented boundary that is not silently broken -- it fails loudly. ### Pattern 1: load, mutate, update -- in one DbContext scope (recommended) ```csharp public class AddTeamMemberCommandHandler { private readonly IAggregateRepository _teams; public async Task HandleAsync(AddTeamMemberCommand cmd, CancellationToken ct) { var team = await _teams.Include(t => t.Memberships).GetByIdAsync(cmd.TeamId, ct); team!.AddMember(cmd.UserId, cmd.Role); // raises a domain event internally await _teams.UpdateAsync(team, ct); // -- the new membership is inserted; any existing, already-loaded membership whose // properties changed is updated; the domain event raised by AddMember is dispatched // automatically, because everything happened through this one repository call. } } ``` This is the common case: load the aggregate (optionally with `Include` for the children you need to inspect or modify), mutate it in memory, call `UpdateAsync` once. New children are correctly inserted; already-tracked existing children's property changes are correctly updated. No special handling is required. ### Pattern 2: cross-scope / disconnected mutation Sometimes an aggregate is loaded in one scope (or process, or deserialized from a message) and mutated in a different one from where `UpdateAsync` will eventually be called -- for example, a background worker resuming a saga, or a message handler operating on a payload built elsewhere. In that case, `UpdateAsync` cannot distinguish "a child that's genuinely new" from "a child that already exists but was never loaded in this scope" -- both look identical (untracked) to this DbContext. Persist the new child through its own repository instead, and register the aggregate for event dispatch explicitly: ```csharp public class AddTeamMemberCommandHandler { private readonly ILinqRepository _memberships; private readonly IEntityEventTracker _eventTracker; public async Task HandleAsync(AddTeamMemberCommand cmd, Team team, CancellationToken ct) { team.AddMember(cmd.UserId, cmd.Role); // raises a domain event internally await _memberships.AddAsync(team.Memberships.Last(), ct); // Required: UpdateAsync was not called on `team`, so nothing else registers it for // event dispatch. Without this line, AddMember's domain event is silently never // dispatched -- IEntityEventTracker.AddEntity is the same mechanism UpdateAsync uses // internally; calling it directly here reproduces that registration by hand. _eventTracker.AddEntity(team); } } ``` ### Boundary: what happens if you skip both patterns If `UpdateAsync` is called against a fully disconnected graph -- the aggregate and an *existing* child were both constructed fresh (e.g. deserialized) rather than loaded through this DbContext, and neither pattern above was followed -- the existing child looks exactly as "new" as a genuinely new one. The result is a loud failure (a unique-constraint violation attempting to re-insert an already-existing row), not silent data loss. If you hit this, use Pattern 2 instead. ## TenantId stamping and tenant-scoped bypass If your aggregate implements `IMultiTenant`, `AddAsync` stamps `TenantId` automatically. The exact guard: stamping is skipped entirely (not set to null) when the resolved tenant ID is null or empty; a non-empty resolved tenant ID unconditionally overwrites whatever the entity's `TenantId` already was. See [Multi-Tenancy](../multi-tenancy/overview) for the full semantics, including `TenantScope.Bypass()` for the tenant-bootstrap scenario (creating the very first row for a brand-new tenant, where you want the entity's explicitly-set `TenantId` left untouched). ## API Summary | Member | Purpose | |--------|---------| | `GetByIdAsync(TKey)` | Loads the aggregate by primary key. Does not eager-load collections by default. | | `FindAsync(ISpecification)` | Loads a single aggregate matching a specification. | | `ExistsAsync(TKey)` | Lightweight existence check without loading the full aggregate. | | `AddAsync(TAggregate)` | Persists a new aggregate. See the provider comparison table for cascade-insert behavior. | | `UpdateAsync(TAggregate)` | Persists changes to an existing aggregate's own row. Never persists child collections -- see "Adding or modifying children" above. | | `DeleteAsync(TAggregate)` | Soft-deletes if the aggregate implements `ISoftDelete`; otherwise a physical delete. | | `Include` / `ThenInclude` | Fluent eager-loading. No-op on Dapper. | --- ## caching-memory Source: https://rcommon.com/docs/persistence/caching-memory --- title: Caching with Memory sidebar_position: 7 description: Decorate RCommon repositories with in-process memory caching using ICachingGraphRepository and caller-supplied cache keys, with write operations always bypassing the cache. --- # Persistence Caching — Memory ## Overview The persistence caching layer decorates existing repositories with cache-aware query overloads. Instead of hitting the database on every read, results can be stored in a cache and retrieved by a caller-supplied key. Writes (add, update, delete) are always delegated directly to the underlying repository without touching the cache. Two in-process memory cache options are available: - **In-memory (`InMemoryCacheService`)** — uses `IMemoryCache` from `Microsoft.Extensions.Caching.Memory`. Best for single-node deployments. - **Distributed memory (`DistributedMemoryCacheService`)** — uses `IDistributedCache` with the in-memory implementation from `Microsoft.Extensions.Caching.StackExchangeRedis`. Useful for testing distributed cache code paths locally without a Redis instance. Both options register the same set of caching repository decorators: - `ICachingGraphRepository` — for use with `IGraphRepository` (EF Core) - `ICachingLinqRepository` — for use with `ILinqRepository` (EF Core, Linq2Db) - `ICachingSqlMapperRepository` — for use with `ISqlMapperRepository` (Dapper) ## Installation This package depends on `RCommon.Persistence.Caching`, which is pulled in transitively. ## Configuration Call `AddInMemoryPersistenceCaching` or `AddDistributedMemoryPersistenceCaching` on the persistence builder **after** configuring the underlying provider: ```csharp builder.Services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext( "AppDb", options => options.UseSqlServer(connectionString)); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "AppDb"); // In-process memory cache ef.AddInMemoryPersistenceCaching(); // -- OR -- distributed memory cache (useful for local testing) // ef.AddDistributedMemoryPersistenceCaching(); }); ``` Both methods configure `CachingOptions` with these defaults: | Option | Default | |--------|---------| | `CachingEnabled` | `true` | | `CacheDynamicallyCompiledExpressions` | `true` | ## Usage ### Injecting a caching repository Inject `ICachingGraphRepository` instead of `IGraphRepository`. All non-cached operations fall through to the inner repository unchanged; only the overloads that accept a `cacheKey` use the cache. ```csharp public class ProductQueryHandler { private readonly ICachingGraphRepository _products; public ProductQueryHandler(ICachingGraphRepository products) { _products = products; _products.DataStoreName = "AppDb"; } } ``` ### Cached reads Every `ICachingGraphRepository` overload that accepts a `cacheKey` checks the cache first and falls through to the database on a miss, storing the result for subsequent calls: ```csharp // Cache by a composite key string key = $"products:category:{categoryId}"; ICollection cached = await _products.FindAsync( cacheKey: key, expression: p => p.CategoryId == categoryId, token: cancellationToken); ``` Paged query with caching: ```csharp string pageKey = $"products:page:{pageNumber}:{pageSize}"; IPaginatedList page = await _products.FindAsync( cacheKey: pageKey, expression: p => p.IsActive, orderByExpression: p => p.Name, orderByAscending: true, pageNumber: pageNumber, pageSize: pageSize, token: cancellationToken); ``` Using a specification: ```csharp var spec = new ActiveProductSpec(); string key = "products:active"; ICollection active = await _products.FindAsync( cacheKey: key, specification: spec, token: cancellationToken); ``` Paged query from a paged specification: ```csharp var spec = new PagedSpecification( p => p.IsActive, p => p.Name, orderByAscending: true, pageNumber: 1, pageSize: 10); IPaginatedList page = await _products.FindAsync( cacheKey: "products:active:p1", specification: spec, token: cancellationToken); ``` ### Non-cached operations All write operations and non-keyed reads delegate directly to the inner repository without interacting with the cache: ```csharp // These bypass the cache entirely await _products.AddAsync(product, cancellationToken); await _products.UpdateAsync(product, cancellationToken); await _products.DeleteAsync(product, cancellationToken); // Non-cached read (no cacheKey overload) Product? p = await _products.FindAsync(productId, cancellationToken); ``` ### ICachingLinqRepository and ICachingSqlMapperRepository The same caching pattern applies to `ICachingLinqRepository` (for Linq2Db or EF Core through the LINQ interface) and `ICachingSqlMapperRepository` (for Dapper): ```csharp // Linq2Db private readonly ICachingLinqRepository _products; ICollection cached = await _products.FindAsync( "products:all", p => true, cancellationToken); ``` ```csharp // Dapper private readonly ICachingSqlMapperRepository _products; ICollection cached = await _products.FindAsync( "products:featured", p => p.IsFeatured, cancellationToken); ``` ## Cache key design Cache keys are arbitrary `object` values. Using structured, namespaced string keys (e.g., `entity:qualifier:value`) makes it straightforward to reason about cache invalidation: ```csharp // Good: namespaced and parameterised string key = $"products:category:{categoryId}:page:{page}"; // Avoid: generic keys that may collide across entity types string key = $"{categoryId}:{page}"; ``` Cache invalidation is the responsibility of the caller. After a write operation, remove or refresh the relevant cache entries using the underlying `ICacheService` if your scenario requires it. ## API Summary | Type | Purpose | |------|---------| | `ICachingGraphRepository` | Decorator over `IGraphRepository`; adds `cacheKey` overloads to `FindAsync` | | `ICachingLinqRepository` | Decorator over `ILinqRepository`; adds `cacheKey` overloads to `FindAsync` | | `ICachingSqlMapperRepository` | Decorator over `ISqlMapperRepository`; adds `cacheKey` overloads to `FindAsync` | | `AddInMemoryPersistenceCaching()` | Extension on `IPersistenceBuilder` that registers `InMemoryCacheService` and caching repositories | | `AddDistributedMemoryPersistenceCaching()` | Extension that registers `DistributedMemoryCacheService` and caching repositories | | `PersistenceCachingStrategy` | Enum used internally to select the `ICacheService` implementation | --- ## caching-redis Source: https://rcommon.com/docs/persistence/caching-redis --- title: Caching with Redis sidebar_position: 8 description: Back RCommon's repository caching decorators with Redis via RedisCacheService for a distributed, out-of-process cache shared across multiple service instances and restarts. --- # Persistence Caching — Redis ## Overview The Redis persistence caching package registers `RedisCacheService` as the `ICacheService` backing the caching repository decorators. It shares the same decorator model as the memory caching package — read operations that supply a `cacheKey` are served from Redis on a hit, and fall through to the underlying repository on a miss. Write operations always bypass the cache. The same three caching repository interfaces are available regardless of which cache backend is used: - `ICachingGraphRepository` — EF Core - `ICachingLinqRepository` — EF Core or Linq2Db through the LINQ interface - `ICachingSqlMapperRepository` — Dapper Use this package when you need a distributed, out-of-process cache that survives application restarts and is shared across multiple service instances. ## Installation This package depends on `RCommon.Persistence.Caching`, which is pulled in transitively. You will also need a Redis client library; `StackExchange.Redis` is the standard choice and is typically added as a transitive dependency through `Microsoft.Extensions.Caching.StackExchangeRedis`. ## Configuration ### 1. Add Redis distributed cache Register Redis with ASP.NET Core's distributed cache infrastructure before configuring RCommon: ```csharp builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("Redis"); options.InstanceName = "MyApp:"; }); ``` ### 2. Add Redis persistence caching to the persistence builder Call `AddRedisPersistenceCaching` on the persistence builder **after** configuring the underlying provider: ```csharp builder.Services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext( "AppDb", options => options.UseSqlServer(connectionString)); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "AppDb"); ef.AddRedisPersistenceCaching(); }); ``` `AddRedisPersistenceCaching` registers: - `RedisCacheService` as `ICacheService` - A `Func` factory that resolves `RedisCacheService` for `PersistenceCachingStrategy.Default` - `ICachingGraphRepository<>`, `ICachingLinqRepository<>`, `ICachingSqlMapperRepository<>` decorators - `CachingOptions` with `CachingEnabled = true` and `CacheDynamicallyCompiledExpressions = true` ## Usage The API is identical to the memory caching package. Swap the injected interface and the configuration call; your handler code remains the same. ### Injecting a caching repository ```csharp public class ProductQueryHandler { private readonly ICachingGraphRepository _products; public ProductQueryHandler(ICachingGraphRepository products) { _products = products; _products.DataStoreName = "AppDb"; } } ``` ### Cached reads ```csharp string key = $"products:category:{categoryId}"; ICollection cached = await _products.FindAsync( cacheKey: key, expression: p => p.CategoryId == categoryId, token: cancellationToken); ``` Paged result with caching: ```csharp string pageKey = $"products:page:{pageNumber}:{pageSize}"; IPaginatedList page = await _products.FindAsync( cacheKey: pageKey, expression: p => p.IsActive, orderByExpression: p => p.Name, orderByAscending: true, pageNumber: pageNumber, pageSize: pageSize, token: cancellationToken); ``` Using a specification: ```csharp var spec = new ActiveProductSpec(); ICollection active = await _products.FindAsync( cacheKey: "products:active", specification: spec, token: cancellationToken); ``` ### Non-cached operations Write operations and reads without a `cacheKey` always go to the database: ```csharp await _products.AddAsync(product, cancellationToken); await _products.UpdateAsync(product, cancellationToken); await _products.DeleteAsync(product, cancellationToken); ``` ### Using with Linq2Db ```csharp private readonly ICachingLinqRepository _products; ICollection cached = await _products.FindAsync( "products:all", p => true, cancellationToken); ``` ### Using with Dapper ```csharp private readonly ICachingSqlMapperRepository _products; ICollection featured = await _products.FindAsync( "products:featured", p => p.IsFeatured, cancellationToken); ``` ## Cache key design Choose structured, namespaced keys to make cache management straightforward: ```csharp // Namespaced by entity, qualifier, and parameter values string key = $"products:category:{categoryId}:page:{page}:size:{size}"; ``` Redis keys have no automatic expiration by default when using `RedisCacheService` with `IDistributedCache`; set expiration policies on the `CachingOptions` or manage them through the `ICacheService` interface directly if your use case requires TTL control. ### Invalidation strategy Cache invalidation after a write is the responsibility of the caller. A common pattern is to delete or refresh the relevant key after mutating data: ```csharp await _products.UpdateAsync(product, cancellationToken); // Evict the stale cache entry so the next read goes to the database await _cacheService.DeleteAsync($"products:category:{product.CategoryId}"); ``` Inject `ICacheService` directly when you need explicit cache management outside the repository decorators. ## API Summary | Type | Purpose | |------|---------| | `AddRedisPersistenceCaching()` | Extension on `IPersistenceBuilder` that registers `RedisCacheService` and caching repositories | | `ICachingGraphRepository` | Decorator over `IGraphRepository`; adds `cacheKey` overloads to `FindAsync` | | `ICachingLinqRepository` | Decorator over `ILinqRepository`; adds `cacheKey` overloads to `FindAsync` | | `ICachingSqlMapperRepository` | Decorator over `ISqlMapperRepository`; adds `cacheKey` overloads to `FindAsync` | | `RedisCacheService` | Concrete `ICacheService` backed by `IDistributedCache` with a Redis store | | `PersistenceCachingStrategy` | Enum used internally to select the `ICacheService` implementation | --- ## Dapper Source: https://rcommon.com/docs/persistence/dapper Use RCommon's Dapper provider for lightweight SQL data access with Dommel-generated queries, raw SQL via ISqlMapperRepository, and automatic soft-delete handling. # Dapper ## Overview The Dapper provider wires `DapperRepository` into the repository abstraction layer. It implements `ISqlMapperRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository`. Under the hood the Dapper provider uses **Dommel** for SQL generation. Dommel translates entity mappings and LINQ-style lambda expressions into parameterized SQL, so common CRUD operations do not require hand-written SQL. Raw SQL execution is still available through `ISqlMapperRepository` when you need it. Unlike the EF Core provider, Dapper does not expose `ILinqRepository` or `IGraphRepository` because it has no change-tracking mechanism. Each operation opens a `DbConnection` from the configured `RDbConnection`, executes the statement, and closes the connection. ## Installation ## Connection setup Your connection class must derive from `RDbConnection`, which implements `IDataStore` so that the `IDataStoreFactory` can resolve it by name. ```csharp using Microsoft.Extensions.Options; using RCommon.Persistence.Sql; public class AppDbConnection : RDbConnection { public AppDbConnection(IOptions options) : base(options) { } } ``` `RDbConnectionOptions` carries the connection string. Configure it when registering the connection (see below). ## Configuration Register the Dapper persistence provider in your application startup: ```csharp builder.Services.AddRCommon() .WithPersistence(dapper => { dapper.AddDbConnection( "AppDb", options => options.ConnectionString = builder.Configuration.GetConnectionString("AppDb")); dapper.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "AppDb"); }); ``` Multiple connections can be registered by calling `AddDbConnection` more than once with different names. ## Usage ### Injecting and targeting a data store Inject `ISqlMapperRepository` (or the narrower `IReadOnlyRepository` / `IWriteOnlyRepository`) and set `DataStoreName`: ```csharp public class ProductQueryHandler { private readonly IReadOnlyRepository _products; public ProductQueryHandler(IReadOnlyRepository products) { _products = products; _products.DataStoreName = "AppDb"; } } ``` ### CRUD operations ```csharp // Create await _repo.AddAsync(product, cancellationToken); await _repo.AddRangeAsync(products, cancellationToken); // Read by primary key Product? p = await _repo.FindAsync(productId, cancellationToken); // Read by expression (Dommel translates to SQL WHERE) ICollection available = await _repo.FindAsync(p => p.StockQuantity > 0, cancellationToken); // Single or default Product? featured = await _repo.FindSingleOrDefaultAsync(p => p.IsFeatured, cancellationToken); // Existence check bool exists = await _repo.AnyAsync(p => p.Sku == sku, cancellationToken); // Count long count = await _repo.GetCountAsync(p => p.CategoryId == categoryId, cancellationToken); // Update await _repo.UpdateAsync(product, cancellationToken); // Delete (auto-detects ISoftDelete) await _repo.DeleteAsync(product, cancellationToken); // Bulk delete by expression int affected = await _repo.DeleteManyAsync(p => p.StockQuantity == 0, cancellationToken); ``` ### Primary keys `AddAsync`/`AddRangeAsync` populate an auto-increment `int`/`long` `Id` with the database-generated value after insert — inject `ISqlMapperRepository` (or `IWriteOnlyRepository`), call `AddAsync`, and the entity's `Id` is set when the call returns, the same as with the EF Core provider. A `Guid` or `string` `Id` is always inserted exactly as you set it — the Dapper provider never overwrites a non-numeric key. :::tip Runnable example See `Examples/Persistence/Examples.Persistence.Dapper/` for a complete console app exercising this end to end, including the SQLite-specific `DommelMapper.AddSqlBuilder` registration needed for identity retrieval against SQLite. ::: ### Soft delete If the entity implements `ISoftDelete`, `DeleteAsync` and `DeleteManyAsync` will set `IsDeleted = true` and issue an UPDATE rather than a physical DELETE. To force a physical DELETE regardless of the interface: ```csharp await _repo.DeleteAsync(product, isSoftDelete: false, cancellationToken); ``` ### Using specifications Specifications work the same as with any other provider: ```csharp var spec = new ActiveProductSpec(); ICollection active = await _repo.FindAsync(spec, cancellationToken); ``` ### Raw SQL via ISqlMapperRepository `ISqlMapperRepository` exposes `QueryAsync` and `ExecuteAsync` for scenarios where Dommel's SQL generation is insufficient: ```csharp public class ReportService { private readonly ISqlMapperRepository _reports; public ReportService(ISqlMapperRepository reports) { _reports = reports; _reports.DataStoreName = "AppDb"; } public async Task> GetMonthlyAsync(int year, int month, CancellationToken cancellationToken) { return await _reports.QueryAsync( "SELECT * FROM SalesReports WHERE Year = @Year AND Month = @Month", new { Year = year, Month = month }, cancellationToken); } } ``` ## Dommel entity mapping Dommel infers table and column names from entity class and property names by convention. You can configure mappings explicitly: ```csharp DommelMapper.SetTableNameResolver(new PluralizedTableNameResolver()); DommelMapper.AddSqlBuilder(typeof(SqlConnection), new SqlServerSqlBuilder()); ``` Refer to the [Dommel documentation](https://github.com/henkmollema/Dommel) for the full mapping API. ## API Summary | Type | Purpose | |------|---------| | `IDapperBuilder` | Fluent startup builder with `AddDbConnection` and `SetDefaultDataStore` | | `DapperPersistenceBuilder` | Concrete implementation that registers Dapper repositories in the DI container | | `RDbConnection` | Abstract base class for all Dapper connections; implements `IDataStore` | | `RDbConnectionOptions` | Options carrying the connection string | | `DapperRepository` | Concrete repository; implements `ISqlMapperRepository`, `IReadOnlyRepository`, `IWriteOnlyRepository` | | `ISqlMapperRepository` | Extends read/write with raw-SQL `QueryAsync` and `ExecuteAsync` | --- ## efcore Source: https://rcommon.com/docs/persistence/efcore --- title: Entity Framework Core sidebar_position: 4 description: Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control. --- # Entity Framework Core ## Overview The EF Core provider wires `EFCoreRepository` into the repository abstraction layer. It implements `IReadOnlyRepository`, `IWriteOnlyRepository`, `ILinqRepository`, and `IGraphRepository`, giving you full LINQ query support, eager loading, automatic soft-delete handling, and opt-in change-tracking control. The provider resolves a `RCommonDbContext`-derived `DbContext` from the `IDataStoreFactory` at runtime, which makes it possible to register multiple named DbContext instances and route repositories to the right one. ## Installation ## DbContext setup Your `DbContext` must derive from `RCommonDbContext` rather than `DbContext` directly. `RCommonDbContext` implements `IDataStore`, which is the mechanism the `IDataStoreFactory` uses to resolve the correct context at runtime. ```csharp using Microsoft.EntityFrameworkCore; using RCommon.Persistence.EFCore; public class LeaveManagementDbContext : RCommonDbContext { public LeaveManagementDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(LeaveManagementDbContext).Assembly); } public DbSet LeaveRequests { get; set; } public DbSet LeaveTypes { get; set; } public DbSet LeaveAllocations { get; set; } } ``` If you need audit stamping or other cross-cutting concerns, introduce an intermediate abstract class that still inherits from `RCommonDbContext`: ```csharp public abstract class AuditableDbContext : RCommonDbContext { private readonly ICurrentUser _currentUser; private readonly ISystemTime _systemTime; public AuditableDbContext(DbContextOptions options, ICurrentUser currentUser, ISystemTime systemTime) : base(options) { _currentUser = currentUser; _systemTime = systemTime; } public override Task SaveChangesAsync( bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { foreach (var entry in ChangeTracker.Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)) { string userId = _currentUser?.Id?.ToString() ?? "System"; entry.Entity.DateLastModified = _systemTime.Now; entry.Entity.LastModifiedBy = userId; if (entry.State == EntityState.Added) { entry.Entity.DateCreated = _systemTime.Now; entry.Entity.CreatedBy = userId; } } return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } } ``` ## Configuration Register the EF Core persistence provider in your application startup: ```csharp builder.Services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext( "LeaveManagement", options => options.UseSqlServer( builder.Configuration.GetConnectionString("LeaveManagement"))); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "LeaveManagement"); }); ``` Multiple DbContexts can be registered by calling `AddDbContext` multiple times with different names: ```csharp ef.AddDbContext("Inventory", options => options.UseNpgsql(inventoryConnectionString)); ef.AddDbContext("Ordering", options => options.UseSqlServer(orderingConnectionString)); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Ordering"); ``` ## Modular composition `WithPersistence` is cache-aware. When two modules both call it, the second call reuses the cached `EFCorePersistenceBuilder` rather than constructing a new one — both configuration delegates run against the same instance, so each module can declare its own DbContexts side by side: ```csharp // In OrderingModule.Configure services.AddRCommon() .WithPersistence(ef => ef.AddDbContext("Ordering", o => o.UseSqlServer(orderingConn))); // In InventoryModule.Configure services.AddRCommon() .WithPersistence(ef => ef.AddDbContext("Inventory", o => o.UseNpgsql(inventoryConn))); ``` After both modules run, `IDataStoreFactory` resolves `"Ordering"` and `"Inventory"` to their respective contexts. The `EFCorePersistenceBuilder` itself is constructed exactly once. `AddDbContext(name, ...)` has the following conflict semantics across modules: | Re-registration shape | Behaviour | |---|---| | Same `name` + same `TDbContext` (same options-builder allowed to differ) | Idempotent — the duplicate is dropped. | | Same `name` + different `TDbContext` | Throws `UnsupportedDataStoreException` naming both context types. | | Different `name` + same or different `TDbContext` | Both are registered. | See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ## Usage ### Injecting and targeting a data store Inject `IGraphRepository` (or any narrower interface) and set `DataStoreName` to match the name given to `AddDbContext`: ```csharp public class CreateLeaveTypeCommandHandler { private readonly IGraphRepository _leaveTypeRepository; public CreateLeaveTypeCommandHandler(IGraphRepository leaveTypeRepository) { _leaveTypeRepository = leaveTypeRepository; _leaveTypeRepository.DataStoreName = "LeaveManagement"; } public async Task HandleAsync(CreateLeaveTypeCommand request, CancellationToken cancellationToken) { var leaveType = new LeaveType { Name = request.Name, DefaultDays = request.DefaultDays }; await _leaveTypeRepository.AddAsync(leaveType, cancellationToken); } } ``` ### CRUD operations ```csharp // Create await _repo.AddAsync(entity, cancellationToken); await _repo.AddRangeAsync(entities, cancellationToken); // Read LeaveType? lt = await _repo.FindAsync(id, cancellationToken); var all = await _repo.FindAsync(lt => lt.DefaultDays > 0, cancellationToken); var single = await _repo.FindSingleOrDefaultAsync(lt => lt.Name == "Annual", cancellationToken); bool any = await _repo.AnyAsync(lt => lt.DefaultDays > 20, cancellationToken); long count = await _repo.GetCountAsync(lt => lt.DefaultDays > 0, cancellationToken); // Update await _repo.UpdateAsync(entity, cancellationToken); // Delete (auto-detects ISoftDelete) await _repo.DeleteAsync(entity, cancellationToken); // Bulk delete int affected = await _repo.DeleteManyAsync(lt => lt.DefaultDays == 0, cancellationToken); ``` ### LINQ queries Because `IGraphRepository` extends `IQueryable` you can use it directly in LINQ expressions: ```csharp IQueryable query = _repo.FindQuery(lt => lt.DefaultDays > 5); ``` ### Eager loading ```csharp var requests = await _leaveRequestRepo .Include(r => r.LeaveType) .FindAsync(r => r.EmployeeId == employeeId, cancellationToken); ``` Chain additional navigation properties with `ThenInclude`: ```csharp var allocations = await _allocationRepo .Include(a => a.LeaveType) .FindAsync(a => a.EmployeeId == userId, cancellationToken); ``` ### Disabling change tracking Set `Tracking = false` for read-only queries that do not need to participate in the change tracker: ```csharp _repo.Tracking = false; var list = await _repo.FindAsync(lt => lt.DefaultDays > 0, cancellationToken); ``` ### Paged queries ```csharp IPaginatedList page = await _repo.FindAsync( expression: lt => lt.DefaultDays > 0, orderByExpression: lt => lt.Name, orderByAscending: true, pageNumber: 1, pageSize: 10, token: cancellationToken); ``` ### Using specifications ```csharp var spec = new AllocationExistsSpec(userId, leaveTypeId, year); long count = await _allocationRepo.GetCountAsync(spec, cancellationToken); ``` ### Read models Alongside `IGraphRepository`, `EFCorePersistenceBuilder`'s constructor also registers `IReadModelRepository` (backed by `EFCoreReadModelRepository`) for every type — no separate opt-in call is required. It targets `IReadModel`-marked projection/query types rather than full aggregates, and is specification-driven rather than exposing raw LINQ: ```csharp using RCommon.Persistence.Crud; public class LeaveBalanceSummary : IReadModel { public Guid EmployeeId { get; set; } public int RemainingDays { get; set; } } public class LeaveBalanceQueryHandler { private readonly IReadModelRepository _readModels; public LeaveBalanceQueryHandler(IReadModelRepository readModels) { _readModels = readModels; } public async Task HandleAsync(Guid employeeId, CancellationToken cancellationToken) { return await _readModels.FindAsync(new EmployeeBalanceSpec(employeeId), cancellationToken); } } ``` | Member | Description | |---|---| | `FindAsync(ISpecification, CancellationToken)` | Returns the first match, or `null`. | | `FindAllAsync(ISpecification, CancellationToken)` | Returns all matches as a read-only list. | | `GetPagedAsync(IPagedSpecification, CancellationToken)` | Returns a paged result set. | | `GetCountAsync(ISpecification, CancellationToken)` | Returns the count of matches. | | `AnyAsync(ISpecification, CancellationToken)` | Returns whether any match exists. | | `Include(Expression>)` | Eager-loads a navigation property; chainable. | `IReadModelRepository` also implements `INamedDataSource`, so `DataStoreName` can be set the same way as on `IGraphRepository` to target a specific registered `DbContext`. Aggregates are also registered automatically: `IAggregateRepository` (via `EFCoreAggregateRepository<,>`) for loading and persisting aggregate roots with their child collections — see [Aggregate Repository](aggregate-repository.mdx) — and `ISagaStore` (via `EFCoreSagaStore<,>`) for saga/process-manager state — see [Sagas](sagas.mdx). ## API Summary | Type | Purpose | |------|---------| | `IEFCorePersistenceBuilder` | Fluent startup builder with `AddDbContext` and `SetDefaultDataStore` | | `EFCorePersistenceBuilder` | Concrete implementation that registers EF Core repositories in the DI container. (The obsolete, misspelled `EFCorePerisistenceBuilder` name still compiles as a deprecated alias, but new code should use this corrected spelling.) | | `RCommonDbContext` | Abstract base class for all EF Core contexts; implements `IDataStore` | | `EFCoreRepository` | Concrete repository; implements `IGraphRepository` and lower interfaces | | `IGraphRepository` | Full CRUD + LINQ + paging + eager loading + `Tracking` property | | `IReadModelRepository` | Specification-driven read access for `IReadModel` projection types; registered automatically alongside the write-side repositories | | `IAggregateRepository` | Loads/persists aggregate roots and their child collections; see [Aggregate Repository](aggregate-repository.mdx) | | `ISagaStore` | Persists saga/process-manager state; see [Sagas](sagas.mdx) | --- ## Linq2Db Source: https://rcommon.com/docs/persistence/linq2db Wire up RCommon's Linq2Db provider for ILinqRepository support with LINQ queries, paging, eager loading via LoadWith, and multi-database connection management without EF Core. # Linq2Db ## Overview The Linq2Db provider wires `Linq2DbRepository` into the repository abstraction layer. It implements `IReadOnlyRepository`, `IWriteOnlyRepository`, and `ILinqRepository`, giving you LINQ query support and paging without requiring a full ORM like EF Core. Linq2Db operates through a `RCommonDataConnection`, which wraps Linq2Db's `DataConnection` and implements `IDataStore` so the `IDataStoreFactory` can resolve named connections at runtime. Unlike EF Core, Linq2Db has no server-side change tracking, so `IGraphRepository` (with its `Tracking` property) is not supported. ## Installation ## Data connection setup Your connection class must derive from `RCommonDataConnection`: ```csharp using RCommon.Persistence.Linq2Db; public class AppDataConnection : RCommonDataConnection { public AppDataConnection(DataOptions options) : base(options) { } } ``` `RCommonDataConnection` inherits from Linq2Db's `DataConnection`, so you can configure entity mappings on it using Linq2Db's fluent mapping API. ## Configuration Register the Linq2Db persistence provider in your application startup: ```csharp builder.Services.AddRCommon() .WithPersistence(linq2db => { linq2db.AddDataConnection( "AppDb", (serviceProvider, dataOptions) => dataOptions.UseSqlServer( builder.Configuration.GetConnectionString("AppDb"))); linq2db.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "AppDb"); }); ``` Multiple connections can be registered by calling `AddDataConnection` more than once with different names. ### Database provider options The `options` factory receives the current `DataOptions` and returns a configured instance. Linq2Db supports all major databases: ```csharp // SQL Server dataOptions.UseSqlServer(connectionString) // PostgreSQL dataOptions.UsePostgreSQL(connectionString) // SQLite dataOptions.UseSQLite(connectionString) // MySQL dataOptions.UseMySQL(connectionString) ``` ## Usage ### Injecting and targeting a data store Inject `ILinqRepository` (or a narrower interface) and set `DataStoreName`: ```csharp public class ProductQueryHandler { private readonly ILinqRepository _products; public ProductQueryHandler(ILinqRepository products) { _products = products; _products.DataStoreName = "AppDb"; } } ``` ### CRUD operations ```csharp // Create await _repo.AddAsync(product, cancellationToken); await _repo.AddRangeAsync(products, cancellationToken); // Read by primary key Product? p = await _repo.FindAsync(productId, cancellationToken); // Read by expression ICollection available = await _repo.FindAsync(p => p.StockQuantity > 0, cancellationToken); // Single or default Product? featured = await _repo.FindSingleOrDefaultAsync(p => p.IsFeatured, cancellationToken); // Existence bool exists = await _repo.AnyAsync(p => p.Sku == sku, cancellationToken); // Count long count = await _repo.GetCountAsync(p => p.CategoryId == categoryId, cancellationToken); // Update await _repo.UpdateAsync(product, cancellationToken); // Delete (auto-detects ISoftDelete) await _repo.DeleteAsync(product, cancellationToken); // Bulk delete int affected = await _repo.DeleteManyAsync(p => p.StockQuantity == 0, cancellationToken); ``` ### LINQ queries `ILinqRepository` exposes `IQueryable` directly and provides `FindQuery` overloads that return `IQueryable`: ```csharp IQueryable query = _repo.FindQuery(p => p.CategoryId == categoryId); // Further compose with LINQ operators var names = await query.Select(p => p.Name).ToListAsync(); ``` ### Paged queries ```csharp IPaginatedList page = await _repo.FindAsync( expression: p => p.CategoryId == categoryId, orderByExpression: p => p.Name, orderByAscending: true, pageNumber: 1, pageSize: 20, token: cancellationToken); ``` Or use a `PagedSpecification`: ```csharp var spec = new PagedSpecification( p => p.CategoryId == categoryId, p => p.Name, orderByAscending: true, pageNumber: 1, pageSize: 20); IPaginatedList page = await _repo.FindAsync(spec, cancellationToken); ``` ### Eager loading ```csharp var orders = await _orderRepo .Include(o => o.Customer) .FindAsync(o => o.Status == OrderStatus.Pending, cancellationToken); ``` Linq2Db implements eager loading using `LoadWith` / `ThenLoad` internally, which translates to SQL `JOIN` statements. ### Using specifications ```csharp var spec = new ActiveProductSpec(); ICollection active = await _repo.FindAsync(spec, cancellationToken); ``` ### Soft delete If the entity implements `ISoftDelete`, delete operations mark `IsDeleted = true` and issue an UPDATE rather than a physical DELETE. To bypass: ```csharp await _repo.DeleteAsync(product, isSoftDelete: false, cancellationToken); ``` ### Primary keys Unlike the Dapper provider (which infers a database-generated key from the "Id" naming convention), LinqToDB never guesses — an auto-increment key must be mapped explicitly, either with an attribute on the entity or with fluent mapping at startup. Without this mapping, `AddAsync` inserts the entity's key exactly as set (`0` for a new `int` key), so a second insert fails with a primary-key violation. Fluent mapping is the more convenient option for entities that derive from `BusinessEntity`, since it doesn't require redeclaring the inherited `Id` property: ```csharp using LinqToDB.Mapping; new FluentMappingBuilder(MappingSchema.Default) .Entity().HasTableName("Products") .Property(p => p.Id).IsPrimaryKey().IsIdentity() .Build(); ``` Register this once at startup, before the first `DataConnection` is built. A `Guid` or `string` key needs no such mapping — it is always inserted exactly as you set it. :::tip Runnable example See `Examples/Persistence/Examples.Persistence.Linq2Db/` for a complete console app exercising this end to end. ::: ## API Summary | Type | Purpose | |------|---------| | `ILinq2DbPersistenceBuilder` | Fluent startup builder with `AddDataConnection` and `SetDefaultDataStore` | | `Linq2DbPersistenceBuilder` | Concrete implementation that registers Linq2Db repositories in the DI container | | `RCommonDataConnection` | Abstract base class for all Linq2Db connections; derives from `DataConnection`, implements `IDataStore` | | `Linq2DbRepository` | Concrete repository; implements `ILinqRepository`, `IReadOnlyRepository`, `IWriteOnlyRepository` | | `ILinqRepository` | Full CRUD + `IQueryable` + paging + eager loading | --- ## repository-pattern Source: https://rcommon.com/docs/persistence/repository-pattern --- title: Repository Pattern sidebar_position: 1 description: Use RCommon's provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM. --- # Repository Pattern ## Overview The repository pattern in RCommon provides a uniform, provider-agnostic abstraction over data access. Rather than coupling your domain and application code to a specific ORM or database technology, you program against interfaces from `RCommon.Persistence.Crud`. Swapping from Entity Framework Core to Dapper (or any other supported provider) requires only a configuration change at the composition root. The abstraction hierarchy works as follows: - `IReadOnlyRepository` — async query methods - `IWriteOnlyRepository` — async write methods (add, update, delete) - `ILinqRepository` — combines read, write, and exposes `IQueryable` plus paging helpers - `IGraphRepository` — extends `ILinqRepository` with change-tracking control; used with ORM providers such as EF Core All repository interfaces require the entity to implement `IBusinessEntity`. ## Installation Install the core persistence package. You will also need at least one provider package (see [EF Core](./efcore), [Dapper](./dapper), or [Linq2Db](./linq2db)). ## Configuration Repositories are registered automatically when you configure a provider. Call `WithPersistence` in your application startup: ```csharp builder.Services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext( "LeaveManagement", options => options.UseSqlServer(connectionString)); ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "LeaveManagement"); }); ``` When multiple data stores are registered you select which one a repository targets by setting `DataStoreName` on the repository instance: ```csharp public class CreateLeaveTypeCommandHandler { private readonly IGraphRepository _repository; public CreateLeaveTypeCommandHandler(IGraphRepository repository) { _repository = repository; _repository.DataStoreName = "LeaveManagement"; // matches the name used in AddDbContext } } ``` ## Usage ### Injecting repositories Inject the interface that matches your needs. For most domain/application code use `IGraphRepository` when the provider is EF Core, or `ILinqRepository` for Linq2Db. Use `IReadOnlyRepository` or `IWriteOnlyRepository` when you want to enforce narrower contracts. ```csharp public class OrderService { private readonly IGraphRepository _orders; public OrderService(IGraphRepository orders) { _orders = orders; } } ``` ### Create ```csharp var order = new Order { CustomerId = customerId, Total = 99.99m }; await _orders.AddAsync(order, cancellationToken); ``` Add multiple entities in one call: ```csharp await _orders.AddRangeAsync(newOrders, cancellationToken); ``` ### Read Find by primary key: ```csharp var order = await _orders.FindAsync(orderId, cancellationToken); ``` Find with a lambda expression: ```csharp ICollection pending = await _orders.FindAsync( o => o.Status == OrderStatus.Pending, cancellationToken); ``` Find a single entity or default: ```csharp Order? draft = await _orders.FindSingleOrDefaultAsync( o => o.CustomerId == customerId && o.Status == OrderStatus.Draft, cancellationToken); ``` Check existence: ```csharp bool exists = await _orders.AnyAsync(o => o.Id == orderId, cancellationToken); ``` Count: ```csharp long count = await _orders.GetCountAsync(o => o.Status == OrderStatus.Pending, cancellationToken); ``` ### Paging `ILinqRepository` and `IGraphRepository` include paged query methods that return `IPaginatedList`: ```csharp IPaginatedList page = await _orders.FindAsync( expression: o => o.CustomerId == customerId, orderByExpression: o => o.DateCreated, orderByAscending: false, pageNumber: 2, pageSize: 20, token: cancellationToken); ``` Or pass a `PagedSpecification` (see [Specifications](./specifications)): ```csharp var spec = new PagedSpecification( o => o.CustomerId == customerId, o => o.DateCreated, orderByAscending: false, pageNumber: 1, pageSize: 10); IPaginatedList page = await _orders.FindAsync(spec, cancellationToken); ``` ### Eager loading `ILinqRepository` exposes fluent `Include` / `ThenInclude` methods: ```csharp var orders = await _orders .Include(o => o.Lines) .FindAsync(o => o.CustomerId == customerId, cancellationToken); ``` ### Update ```csharp order.Status = OrderStatus.Shipped; await _orders.UpdateAsync(order, cancellationToken); ``` ### Delete Auto-detects soft delete if the entity implements `ISoftDelete`; otherwise performs a physical delete: ```csharp await _orders.DeleteAsync(order, cancellationToken); ``` Bulk delete by expression: ```csharp int affected = await _orders.DeleteManyAsync( o => o.Status == OrderStatus.Cancelled, cancellationToken); ``` Override soft-delete behaviour explicitly: ```csharp // Force physical delete even when ISoftDelete is implemented await _orders.DeleteAsync(order, isSoftDelete: false, cancellationToken); ``` ### Turning off change tracking When `IGraphRepository` is used with EF Core you can disable tracking for read-only scenarios: ```csharp _orders.Tracking = false; var readOnlyOrders = await _orders.FindAsync(o => o.Status == OrderStatus.Active, cancellationToken); ``` ## Provider Comparison ## API Summary | Interface | Purpose | |-----------|---------| | `IReadOnlyRepository` | Async query operations: `FindAsync`, `FindSingleOrDefaultAsync`, `AnyAsync`, `GetCountAsync` | | `IWriteOnlyRepository` | Async write operations: `AddAsync`, `AddRangeAsync`, `UpdateAsync`, `DeleteAsync`, `DeleteManyAsync` | | `ILinqRepository` | Combines read + write + `IQueryable` + paging (`FindAsync` with `IPaginatedList`) + `Include` | | `IGraphRepository` | Extends `ILinqRepository` with `Tracking` property for change-tracking control | | `ISqlMapperRepository` | Raw-SQL repository for Dapper — exposes `QueryAsync` and `ExecuteAsync` directly | | `INamedDataSource` | Base interface exposing `DataStoreName` property, inherited by all repository interfaces | --- ## sagas Source: https://rcommon.com/docs/persistence/sagas --- title: Sagas sidebar_position: 8 description: Orchestrate long-running business processes with RCommon's saga framework using state machines, durable SagaState persistence, correlation IDs, and automatic compensation. --- # Sagas RCommon includes a saga orchestration framework that coordinates multi-step business processes with automatic compensation on failure. The saga pattern manages long-running transactions that span multiple services or aggregates, using state machines to drive transitions and persistent stores to track progress. ## When to use sagas Use sagas when you need to: - Coordinate a business process across multiple aggregates or bounded contexts - Ensure compensating actions run when a step fails (e.g. refund a payment if shipping fails) - Track the progress of a multi-step workflow with durable state - Combine event-driven processing with state machine logic ## Core abstractions ### SagaState<TKey> The base class for all saga state. Tracks the lifecycle of a saga instance: ```csharp public abstract class SagaState where TKey : IEquatable { public TKey Id { get; set; } public string CorrelationId { get; set; } public DateTimeOffset StartedAt { get; set; } public DateTimeOffset? CompletedAt { get; set; } public string CurrentStep { get; set; } public bool IsCompleted { get; set; } public bool IsFaulted { get; set; } public string? FaultReason { get; set; } public int Version { get; set; } } ``` Extend this to add domain-specific state for your saga: ```csharp public class OrderSagaState : SagaState { public Guid OrderId { get; set; } public decimal Amount { get; set; } public string PaymentTransactionId { get; set; } public bool InventoryReserved { get; set; } } ``` ### ISaga<TState, TKey> The contract every saga must implement — handle events and compensate on failure: ```csharp public interface ISaga where TState : SagaState where TKey : IEquatable { Task HandleAsync(TEvent @event, TState state, CancellationToken ct = default) where TEvent : ISerializableEvent; Task CompensateAsync(TState state, CancellationToken ct = default); } ``` ### ISagaStore<TState, TKey> Persistence interface for saga state. Supports lookup by primary key or correlation ID: ```csharp public interface ISagaStore where TState : SagaState where TKey : IEquatable { Task FindByCorrelationIdAsync(string correlationId, CancellationToken ct = default); Task GetByIdAsync(TKey id, CancellationToken ct = default); Task SaveAsync(TState state, CancellationToken ct = default); Task DeleteAsync(TState state, CancellationToken ct = default); } ``` ### SagaOrchestrator<TState, TKey, TSagaState, TSagaTrigger> The abstract base class that wires together `ISaga`, `ISagaStore`, and `IStateMachineConfigurator`. You define the state machine transitions, map incoming events to triggers, and implement compensation logic: ```csharp public abstract class SagaOrchestrator : ISaga where TState : SagaState where TKey : IEquatable where TSagaState : struct, Enum where TSagaTrigger : struct, Enum { protected ISagaStore Store { get; } protected abstract void ConfigureStateMachine( IStateMachineConfigurator configurator); protected abstract TSagaTrigger MapEventToTrigger(TEvent @event) where TEvent : ISerializableEvent; protected abstract TSagaState InitialState { get; } public abstract Task CompensateAsync(TState state, CancellationToken ct = default); } ``` When `HandleAsync` is called, the orchestrator: 1. Reads the current step from the saga state (or uses `InitialState` if new) 2. Builds a state machine instance at that state 3. Maps the incoming event to a trigger via `MapEventToTrigger` 4. Fires the trigger if permitted (no-op if not) 5. Updates `CurrentStep` and persists the state via the store ## Example: order fulfillment saga ### Define states and triggers ```csharp public enum OrderSagaStep { Pending, PaymentProcessed, InventoryReserved, Shipped, Completed, Faulted } public enum OrderSagaTrigger { PaymentReceived, InventoryConfirmed, ShipmentDispatched, OrderDelivered, StepFailed } ``` ### Implement the orchestrator ```csharp public class OrderFulfillmentSaga : SagaOrchestrator { public OrderFulfillmentSaga( ISagaStore store, IStateMachineConfigurator configurator) : base(store, configurator) { } protected override OrderSagaStep InitialState => OrderSagaStep.Pending; protected override void ConfigureStateMachine( IStateMachineConfigurator configurator) { configurator.ForState(OrderSagaStep.Pending) .Permit(OrderSagaTrigger.PaymentReceived, OrderSagaStep.PaymentProcessed) .Permit(OrderSagaTrigger.StepFailed, OrderSagaStep.Faulted); configurator.ForState(OrderSagaStep.PaymentProcessed) .Permit(OrderSagaTrigger.InventoryConfirmed, OrderSagaStep.InventoryReserved) .Permit(OrderSagaTrigger.StepFailed, OrderSagaStep.Faulted); configurator.ForState(OrderSagaStep.InventoryReserved) .Permit(OrderSagaTrigger.ShipmentDispatched, OrderSagaStep.Shipped) .Permit(OrderSagaTrigger.StepFailed, OrderSagaStep.Faulted); configurator.ForState(OrderSagaStep.Shipped) .Permit(OrderSagaTrigger.OrderDelivered, OrderSagaStep.Completed); } protected override OrderSagaTrigger MapEventToTrigger(TEvent @event) { return @event switch { PaymentReceivedEvent => OrderSagaTrigger.PaymentReceived, InventoryConfirmedEvent => OrderSagaTrigger.InventoryConfirmed, ShipmentDispatchedEvent => OrderSagaTrigger.ShipmentDispatched, OrderDeliveredEvent => OrderSagaTrigger.OrderDelivered, _ => OrderSagaTrigger.StepFailed }; } public override async Task CompensateAsync( OrderSagaState state, CancellationToken ct = default) { // Reverse completed steps in order if (state.InventoryReserved) { // Release reserved inventory } if (!string.IsNullOrEmpty(state.PaymentTransactionId)) { // Issue refund } state.IsFaulted = true; state.FaultReason = "Compensation executed"; await Store.SaveAsync(state, ct); } } ``` ### Register and use ```csharp services.AddRCommon() .WithPersistence(ef => { ef.AddDbContext("OrderDb", options => options.UseSqlServer(connectionString)); }) .WithStatelessStateMachine(); // Register your saga services.AddScoped(); ``` The `ISagaStore<,>` is automatically registered by each persistence builder — no manual registration needed. :::tip Runnable example See `Examples/Persistence/Examples.Persistence.Sagas/` for a complete console app exercising the full happy path and a compensation path, using `InMemorySagaStore` so it requires no real database. ::: ## Saga store providers Each persistence provider includes a saga store implementation that is automatically registered when you configure the provider: | Provider | Store class | Package | |----------|------------|---------| | EF Core | `EFCoreSagaStore` | `RCommon.EfCore` | | Dapper | `DapperSagaStore` | `RCommon.Dapper` | | Linq2Db | `Linq2DbSagaStore` | `RCommon.Linq2Db` | | In-Memory | `InMemorySagaStore` | `RCommon.Persistence` | All stores are registered as `ISagaStore` with scoped lifetime. ### In-memory store The `InMemorySagaStore` uses a `ConcurrentDictionary` and is useful for testing or prototyping. It is included in the base `RCommon.Persistence` package: ```csharp services.AddScoped(typeof(ISagaStore<,>), typeof(InMemorySagaStore<,>)); ``` ### EF Core store Requires your `RCommonDbContext` to include a `DbSet` for the saga state entity. The store uses the `IDataStoreFactory` to resolve the correct context: ```csharp public class OrderDbContext : RCommonDbContext { public DbSet OrderSagas { get; set; } } ``` ### Dapper and Linq2Db stores Both use `IDataStoreFactory` to resolve connections and follow the same upsert pattern — attempt an update first, insert if no rows were affected (Dapper) or use `InsertOrReplace` (Linq2Db). ## Relationship to state machines The `SagaOrchestrator` depends on `IStateMachineConfigurator` from the [State Machines](../state-machines/overview.mdx) module. You must configure a state machine provider (Stateless or MassTransit) for the orchestrator to function. The state machine drives the transitions while the saga store provides durability. ## Key design decisions - **Correlation ID** — Every saga instance has a `CorrelationId` that ties together all events belonging to the same business process. Use `FindByCorrelationIdAsync` to look up an in-flight saga when an event arrives. - **Optimistic concurrency** — The `Version` property on `SagaState` can be used for optimistic concurrency checks in your store implementation. - **Compensation** — `CompensateAsync` is called explicitly when your application detects a failure. The framework does not auto-compensate; you decide when and how to trigger compensation based on your domain logic. - **Idempotency** — The orchestrator checks `CanFire` before firing a trigger. If an event arrives for a transition that isn't permitted in the current state, it is silently ignored. This provides natural idempotency for duplicate event delivery. --- ## Specifications Source: https://rcommon.com/docs/persistence/specifications Encapsulate reusable query predicates as named Specification objects, compose them with & and | operators, and pass them directly to any RCommon repository method. # Specifications ## Overview The Specification pattern encapsulates a query predicate as a named, reusable object. Instead of scattering raw lambda expressions throughout your handlers and services, you define a specification class once and reference it by name. This keeps query logic close to the domain, makes intent explicit, and allows query logic to be tested independently of any database. RCommon ships two interfaces and two concrete implementations: - `ISpecification` — a single filter predicate - `IPagedSpecification` — adds ordering and paging on top of a filter - `Specification` — default implementation, supports `&` and `|` operator composition - `PagedSpecification` — default implementation of `IPagedSpecification` All repository interfaces accept specifications alongside their equivalent lambda-based overloads, so you can switch between the two styles freely. ## Installation Specifications are part of the core package: ## Defining a Specification Inherit from `Specification` and pass the filter predicate to the base constructor: ```csharp using RCommon; public class AllocationExistsSpec : Specification { public AllocationExistsSpec(string userId, int leaveTypeId, int period) : base(q => q.EmployeeId == userId && q.LeaveTypeId == leaveTypeId && q.Period == period) { } } ``` This is a real specification from the CleanWithCQRS example. It encapsulates a three-part uniqueness check for a leave allocation. ## Using a Specification with a Repository Pass the specification to any repository method that accepts `ISpecification`: ```csharp var spec = new AllocationExistsSpec(employeeId, leaveTypeId, currentYear); long count = await _allocationRepository.GetCountAsync(spec, cancellationToken); if (count == 0) { await _allocationRepository.AddAsync(newAllocation, cancellationToken); } ``` Find a collection: ```csharp ICollection matches = await _allocationRepository .FindAsync(spec, cancellationToken); ``` Find single or default: ```csharp LeaveAllocation? existing = await _allocationRepository .FindSingleOrDefaultAsync(spec, cancellationToken); ``` Check existence: ```csharp bool exists = await _allocationRepository.AnyAsync(spec, cancellationToken); ``` ## Combining Specifications `Specification` overloads the `&` and `|` operators to compose predicates without additional subclassing: ```csharp var activeSpec = new Specification(o => o.IsActive); var pendingSpec = new Specification(o => o.Status == OrderStatus.Pending); var largeSpec = new Specification(o => o.Total > 1000m); // Both conditions must be true var activePending = activeSpec & pendingSpec; // Either condition is sufficient var activeOrLarge = activeSpec | largeSpec; ``` The same composition is available through extension methods on `ISpecification` when you hold an interface reference: ```csharp ISpecification combined = activeSpec .And(pendingSpec) .And(largeSpec); ``` Negate a specification: ```csharp ISpecification notActive = activeSpec.Not(); ``` ## Paged Specifications When you need filtering, ordering, and paging in a single object, use `PagedSpecification`: ```csharp var pagedSpec = new PagedSpecification( predicate: o => o.CustomerId == customerId, orderByExpression: o => o.DateCreated, orderByAscending: false, pageNumber: 1, pageSize: 25); IPaginatedList page = await _orderRepository .FindAsync(pagedSpec, cancellationToken); ``` `IPagedSpecification` exposes: | Property | Type | Description | |----------|------|-------------| | `Predicate` | `Expression>` | The filter expression | | `OrderByExpression` | `Expression>` | The ordering expression | | `OrderByAscending` | `bool` | Sort direction | | `PageNumber` | `int` | 1-based page number | | `PageSize` | `int` | Number of items per page | ## Direct Predicate Evaluation Specifications can be evaluated in memory against a single entity, which is useful in unit tests or domain validation: ```csharp var spec = new AllocationExistsSpec(userId, leaveTypeId, year); bool satisfied = spec.IsSatisfiedBy(allocation); ``` ## API Summary | Type | Purpose | |------|---------| | `ISpecification` | Contract: `Expression> Predicate` and `bool IsSatisfiedBy(T entity)` | | `IPagedSpecification` | Extends `ISpecification` with `PageNumber`, `PageSize`, `OrderByExpression`, `OrderByAscending` | | `Specification` | Concrete implementation; supports `&` and `|` operator composition | | `PagedSpecification` | Concrete implementation of `IPagedSpecification` | | `SpecificationExtensions` | Extension methods: `.And()`, `.Or()`, `.Not()` on `ISpecification` | --- ## Unit of Work Source: https://rcommon.com/docs/persistence/unit-of-work Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support. # Unit of Work ## Overview The Unit of Work pattern coordinates writes to one or more repositories within a single transaction boundary. In RCommon the unit of work wraps a `System.Transactions.TransactionScope`, which means it can span multiple data stores without requiring distributed transaction infrastructure in most scenarios. The typical workflow is: 1. Create a unit of work via `IUnitOfWorkFactory`. 2. Execute repository operations (add, update, delete) as normal. 3. Call `CommitAsync` to commit; or dispose without committing to roll back. RCommon also integrates with MediatR and Wolverine pipelines so that units of work can be opened and committed automatically around command handlers — see [MediatR integration](../cqrs-mediator/mediatr). ## Installation ## Configuration Configure the unit of work in your application startup using `WithUnitOfWork`: ```csharp builder.Services.AddRCommon() .WithUnitOfWork(unitOfWork => { unitOfWork.SetOptions(options => { options.AutoCompleteScope = true; // commit on dispose if not already committed options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }); ``` :::tip Modular composition `WithUnitOfWork` is cached by concrete builder type. When two modules both call `WithUnitOfWork(...)`, the second call reuses the cached sub-builder and runs its `SetOptions` delegate against the same instance — last-write-wins for option values, but the builder is constructed exactly once. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: `UnitOfWorkSettings` defaults: | Setting | Default | Description | |---------|---------|-------------| | `DefaultIsolation` | `ReadCommitted` | The isolation level for new `TransactionScope` instances | | `AutoCompleteScope` | `false` | When `true`, the scope is completed on disposal if no explicit commit or rollback occurred | ## Usage ### Manual transaction management Inject `IUnitOfWorkFactory` and create a unit of work around your operations: ```csharp public class TransferService { private readonly IUnitOfWorkFactory _uowFactory; private readonly IGraphRepository _accounts; public TransferService(IUnitOfWorkFactory uowFactory, IGraphRepository accounts) { _uowFactory = uowFactory; _accounts = accounts; } public async Task TransferAsync(Guid fromId, Guid toId, decimal amount, CancellationToken cancellationToken) { using IUnitOfWork uow = _uowFactory.Create(); Account from = await _accounts.FindAsync(fromId, cancellationToken); Account to = await _accounts.FindAsync(toId, cancellationToken); from.Debit(amount); to.Credit(amount); await _accounts.UpdateAsync(from, cancellationToken); await _accounts.UpdateAsync(to, cancellationToken); await uow.CommitAsync(cancellationToken); } } ``` Disposing the `IUnitOfWork` without calling `CommitAsync` rolls back the transaction. When `AutoCompleteScope` is `false`, this rollback discards any writes made in the scope with no exception — a forgotten `CommitAsync` (or a repository write invoked outside a committing scope) is otherwise silent. RCommon logs a **Warning** on this dispose-without-commit path so the discarded work is visible; enable `AutoCompleteScope` or call `CommitAsync` if the changes were meant to persist. :::warning Use `CommitAsync`, not the obsolete `Commit()` The synchronous `Commit()` is `[Obsolete]` and completes the transaction only — it does **not** run outbox persistence or domain-event dispatch, so any pending domain events on tracked entities are dropped. When events are pending it now logs a Warning, but always prefer `CommitAsync`, which persists and dispatches events correctly. (The `AutoCompleteScope`-on-dispose path routes through `Commit()`, so if you rely on auto-complete with domain events, commit explicitly with `CommitAsync`.) ::: ### Specifying transaction mode and isolation level `IUnitOfWorkFactory.Create` has overloads for controlling how the unit of work participates in ambient transactions: ```csharp // Default settings from configuration IUnitOfWork uow = _uowFactory.Create(); // Explicit transaction mode IUnitOfWork uow = _uowFactory.Create(TransactionMode.New); // Explicit mode and isolation level IUnitOfWork uow = _uowFactory.Create(TransactionMode.New, IsolationLevel.Serializable); ``` `TransactionMode` maps to the `TransactionScopeOption` used when creating the `TransactionScope`: | Mode | Behaviour | |------|-----------| | `Default` | Uses `TransactionScopeOption.Required` — joins an ambient transaction if one exists | | `New` | Uses `TransactionScopeOption.RequiresNew` — always creates a new transaction | | `Suppress` | Uses `TransactionScopeOption.Suppress` — executes outside any ambient transaction | ### Unit of work lifecycle states `IUnitOfWork.State` reports the current lifecycle stage: | State | Meaning | |-------|---------| | `Created` | Newly created; no commit or rollback has been attempted | | `CommitAttempted` | `CommitAsync` has been called | | `Completed` | Successfully committed | | `RolledBack` | The transaction was rolled back | | `Disposed` | The unit of work has been disposed and cannot be reused | ### Pipeline-based unit of work (MediatR) In the CleanWithCQRS example the unit of work is applied to every command handler automatically through the MediatR pipeline: ```csharp builder.Services.AddRCommon() .WithMediator(mediator => { mediator.AddUnitOfWorkToRequestPipeline(); // wraps each handler in a unit of work }) .WithUnitOfWork(unitOfWork => { unitOfWork.SetOptions(options => { options.AutoCompleteScope = true; options.DefaultIsolation = IsolationLevel.ReadCommitted; }); }); ``` With `AutoCompleteScope = true` the unit of work commits automatically when the pipeline behavior disposes it after a successful handler execution. ## API Summary | Type | Purpose | |------|---------| | `IUnitOfWorkFactory` | Creates `IUnitOfWork` instances with default or explicit transaction settings | | `IUnitOfWork` | Manages the transaction: exposes `CommitAsync()`, `State`, `AutoComplete`, `IsolationLevel`, `TransactionMode`, `TransactionId` | | `IUnitOfWorkBuilder` | Fluent startup builder — call `SetOptions(Action)` to configure defaults | | `UnitOfWorkSettings` | Holds `DefaultIsolation` and `AutoCompleteScope` defaults | | `UnitOfWorkState` | Enum: `Created`, `CommitAttempted`, `Completed`, `RolledBack`, `Disposed` | | `TransactionMode` | Enum: `Default`, `New`, `Suppress` | --- ## Authorization Source: https://rcommon.com/docs/security-web/authorization RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs. # Authorization ## Overview `RCommon.Security` provides the claims and principal accessor abstractions that RCommon uses internally for identity resolution. It does not replace ASP.NET Core's authorization pipeline — instead it sits alongside it, giving your application code a consistent way to read the current user's identity, roles, and claims without taking a hard dependency on `HttpContext` or `Thread.CurrentPrincipal`. The core design is a layered stack: - `ICurrentPrincipalAccessor` — retrieves the current `ClaimsPrincipal` and supports temporarily replacing it for a scoped context (useful in background workers and tests). - `ICurrentUser` — reads well-known identity properties (ID, roles, tenant, arbitrary claims) from whatever principal the accessor provides. - `ICurrentClient` — reads the OAuth client ID claim from the same principal. - `ClaimTypesConst` — a static class of configurable claim type URIs so that you can remap standard claims to the values your identity provider actually issues. - `AuthorizationException` — a structured exception type for signaling authorization failures through the application tier. `RCommon.Authorization.Web` adds two Swashbuckle operation filters that keep your OpenAPI/Swagger documentation accurate when your API uses authorization: - `AuthorizeCheckOperationFilter` — detects `[Authorize]` on controllers and actions and adds 401/403 responses plus an OAuth2 security requirement to the generated operation. - `AuthorizationHeaderParameterOperationFilter` — detects `AuthorizeFilter` in the MVC filter pipeline and adds a required `Authorization` header parameter to the operation so that Swagger UI can accept a bearer token. ## Installation For the core security abstractions (principal accessor, current user, claims helpers): For ASP.NET Core web apps that need HTTP-context-aware principal resolution: For the Swashbuckle/OpenAPI authorization filters: ## Configuration ### Claims and principal accessor (non-web / background services) Register the thread-based accessor when your application does not run inside ASP.NET Core request middleware: ```csharp using RCommon; builder.Services.AddRCommon(config => { config.WithClaimsAndPrincipalAccessor(); }); ``` This registers: - `ICurrentPrincipalAccessor` → `ThreadCurrentPrincipalAccessor` (reads `Thread.CurrentPrincipal`) - `ICurrentUser` → `CurrentUser` - `ICurrentClient` → `CurrentClient` - `ITenantIdAccessor` → `ClaimsTenantIdAccessor` ### Claims and principal accessor (ASP.NET Core web apps) Use the web variant which reads the principal from `HttpContext.User` instead: ```csharp using RCommon; builder.Services.AddRCommon(config => { config.WithClaimsAndPrincipalAccessorForWeb(); }); ``` This registers the same interfaces but substitutes `HttpContextCurrentPrincipalAccessor` for `ThreadCurrentPrincipalAccessor`, and calls `AddHttpContextAccessor()` automatically. :::tip Modular composition Both `WithClaimsAndPrincipalAccessor()` and `WithClaimsAndPrincipalAccessorForWeb()` are `TryAdd`-hardened — repeat calls from multiple modules are idempotent. The principal accessor, `ICurrentUser`, `ICurrentClient`, and `ITenantIdAccessor` registrations are each added exactly once. Do not mix the two variants in the same process: `TryAdd` keeps whichever ran first and silently ignores the other, which can leave a web host running with `ThreadCurrentPrincipalAccessor` (or vice versa). Pick one variant in the host's composition root. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ### Overriding claim type URIs If your identity provider uses non-standard claim type URIs, override the static properties on `ClaimTypesConst` once at startup before any requests are processed: ```csharp using RCommon.Security.Claims; // Map to the short claim names issued by your OIDC provider. // Configure must be called once at startup, before any property is accessed. ClaimTypesConst.Configure(options => { options.UserId = "sub"; options.Role = "roles"; options.Email = "email"; options.TenantId = "tenant_id"; options.ClientId = "client_id"; }); ``` ### Swashbuckle operation filters Add both filters to your Swagger generation options: ```csharp using RCommon.Authorization.Web.Filters; builder.Services.AddSwaggerGen(options => { options.OperationFilter(); options.OperationFilter(); // Register your OAuth2 security scheme so that the filters can reference it. options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, Flows = new OpenApiOAuthFlows { AuthorizationCode = new OpenApiOAuthFlow { AuthorizationUrl = new Uri("https://your-idp/connect/authorize"), TokenUrl = new Uri("https://your-idp/connect/token"), Scopes = new Dictionary { ["api"] = "API access" } } } }); }); ``` ## Usage ### Accessing the current user Inject `ICurrentUser` wherever you need identity information: ```csharp using RCommon.Security.Users; public class OrderCommandHandler { private readonly ICurrentUser _currentUser; private readonly IGraphRepository _orders; public OrderCommandHandler(ICurrentUser currentUser, IGraphRepository orders) { _currentUser = currentUser; _orders = orders; } public async Task Handle(PlaceOrderCommand command, CancellationToken ct) { if (!_currentUser.IsAuthenticated) throw new AuthorizationException("You must be signed in to place an order."); var order = new Order { CustomerId = _currentUser.Id!, CustomerName = _currentUser.FindClaimValue(ClaimTypes.GivenName) ?? "Unknown", Total = command.Total }; await _orders.AddAsync(order, ct); } } ``` ### `ICurrentUser` member reference `ICurrentUser` exposes seven members. All of them read from whatever `ClaimsPrincipal` the configured `ICurrentPrincipalAccessor` currently returns: - **`string? Id { get; }`** — the current user's unique identifier, read from the `ClaimTypesConst.UserId` claim. `null` if no user is authenticated or the claim is absent. - **`bool IsAuthenticated { get; }`** — whether the underlying `IIdentity.IsAuthenticated` reports `true`. Check this before trusting any other member. - **`string[] Roles { get; }`** — the distinct set of role names from `ClaimTypesConst.Role` claims. Empty array (never `null`) if the user has no role claims. - **`string? TenantId { get; }`** — the tenant identifier from the `ClaimTypesConst.TenantId` claim on the current principal. See the callout below — this is a different concept from `IMultiTenant.TenantId`. - **`Claim? FindClaim(string claimType)`** — the first claim matching `claimType`, or `null` if none is present. - **`Claim[] FindClaims(string claimType)`** — all claims matching `claimType`. Empty array if none match. - **`Claim[] GetAllClaims()`** — every claim on the current principal, regardless of type. Empty array if the user has no claims. :::info `ICurrentUser.TenantId` vs. `IMultiTenant.TenantId` These are two unrelated properties that happen to share a name: - **`ICurrentUser.TenantId`** (`RCommon.Security`) is the *caller's* tenant — a claim read off the current `ClaimsPrincipal`, describing who is making the current request. - **`IMultiTenant.TenantId`** (`RCommon.Entities`) is a *persistence-layer* property stamped onto an individual entity, describing which tenant that row belongs to. RCommon's multi-tenancy pipeline uses `ITenantIdAccessor` (typically backed by `ICurrentUser.TenantId` via `ClaimsTenantIdAccessor`) to decide which `IMultiTenant.TenantId` value to stamp on new entities and to filter queries by. The two properties usually hold the same value at runtime, but they live at different layers and are set independently — see [Multi-Tenancy Overview](../multi-tenancy/overview.mdx) for how the accessor bridges them. ::: ### Reading roles ```csharp string[] roles = _currentUser.Roles; // distinct role values from ClaimTypesConst.Role claims if (!_currentUser.Roles.Contains("Administrator")) throw new AuthorizationException("Only administrators can perform this action.", "INSUFFICIENT_ROLE"); ``` ### Reading arbitrary claims ```csharp // Find the first matching claim. Claim? claim = _currentUser.FindClaim("custom:department"); // Find all matching claims. Claim[] allDeptClaims = _currentUser.FindClaims("custom:department"); // Find a claim value as a string. string? department = _currentUser.FindClaimValue("custom:department"); ``` ### Accessing the current client ```csharp using RCommon.Security.Clients; public class ApiAuditMiddleware { private readonly ICurrentClient _currentClient; public ApiAuditMiddleware(ICurrentClient currentClient) { _currentClient = currentClient; } public void LogRequest(string path) { if (_currentClient.IsAuthenticated) { Console.WriteLine($"Client {_currentClient.Id} called {path}"); } } } ``` ### Temporarily replacing the principal `ICurrentPrincipalAccessor.Change` replaces the current principal for the lifetime of the returned `IDisposable`. This is useful in background jobs and integration tests: ```csharp using RCommon.Security.Claims; using System.Security.Claims; public async Task RunAsServiceAccountAsync( ICurrentPrincipalAccessor principalAccessor, Func work) { var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypesConst.UserId, Guid.NewGuid().ToString()), new Claim(ClaimTypesConst.Role, "ServiceAccount") }, "ServiceAccount"); using (principalAccessor.Change(new ClaimsPrincipal(identity))) { await work(); // Previous principal is restored when the using block exits. } } ``` The extension overloads let you pass a single `Claim`, a collection of claims, or a `ClaimsIdentity` directly: ```csharp using (principalAccessor.Change(new Claim(ClaimTypesConst.Role, "Tester"))) { // ... } ``` ### Signaling authorization failures Throw `AuthorizationException` from application-layer code when a rule is violated. Global exception handlers or middleware can then translate it to an appropriate HTTP 403 response: ```csharp using RCommon.Security.Authorization; public void EnsureCanEdit(Document document) { if (document.OwnerId != _currentUser.Id) { throw new AuthorizationException( message: "You do not have permission to edit this document.", code: "DOCUMENT_ACCESS_DENIED") .WithData("DocumentId", document.Id) .WithData("UserId", _currentUser.Id); } } ``` ### Claims identity helpers `ClaimsIdentityExtensions` provides fluent extension methods for managing claims: ```csharp using RCommon.Security; // Add a claim only if one with the same type does not already exist. identity.AddIfNotContains(new Claim("custom:role", "Editor")); // Remove all claims of the same type and set a new value. identity.AddOrReplace(new Claim("custom:role", "Administrator")); // Add an identity to a principal only if the same authentication type is not already present. principal.AddIdentityIfNotContains(new ClaimsIdentity(claims, "Cookie")); ``` Extract well-known values directly from a `ClaimsPrincipal`: ```csharp string? userId = principal.FindUserId(); // reads ClaimTypesConst.UserId string? tenant = principal.FindTenantId(); // reads ClaimTypesConst.TenantId string? client = principal.FindClientId(); // reads ClaimTypesConst.ClientId ``` ## API Summary | Type | Package | Description | |------|---------|-------------| | `ICurrentPrincipalAccessor` | `RCommon.Security` | Provides the current `ClaimsPrincipal`; supports scoped override via `Change()` | | `CurrentPrincipalAccessorBase` | `RCommon.Security` | Abstract base implementing `Change()` with `AsyncLocal` storage | | `ThreadCurrentPrincipalAccessor` | `RCommon.Security` | Default implementation; reads `Thread.CurrentPrincipal` | | `CurrentPrincipalAccessorExtensions` | `RCommon.Security` | `Change(Claim)`, `Change(IEnumerable)`, `Change(ClaimsIdentity)` overloads | | `ICurrentUser` | `RCommon.Security` | Exposes `Id`, `IsAuthenticated`, `Roles`, `TenantId`, `FindClaim`, `FindClaims`, `GetAllClaims` — see [member reference](#icurrentuser-member-reference) above | | `CurrentUser` | `RCommon.Security` | Default `ICurrentUser` implementation backed by `ICurrentPrincipalAccessor` | | `CurrentUserExtensions` | `RCommon.Security` | `FindClaimValue(string)`, `GetId()` | | `ICurrentClient` | `RCommon.Security` | Exposes `Id` and `IsAuthenticated` for OAuth client identities | | `CurrentClient` | `RCommon.Security` | Default `ICurrentClient` backed by `ICurrentPrincipalAccessor` | | `ClaimTypesConst` | `RCommon.Security` | Configure-once claim type URIs via `Configure(Action)`: `UserName`, `Name`, `SurName`, `UserId`, `Role`, `Email`, `TenantId`, `ClientId` | | `ClaimsIdentityExtensions` | `RCommon.Security` | `FindUserId`, `FindTenantId`, `FindClientId`, `AddIfNotContains`, `AddOrReplace`, `AddIdentityIfNotContains` | | `AuthorizationException` | `RCommon.Security` | Application-layer exception for access-denied scenarios; carries `Code`, `LogLevel`, and fluent `WithData()` | | `AuthorizeCheckOperationFilter` | `RCommon.Authorization.Web` | Swashbuckle filter: adds 401/403 responses and OAuth2 security requirement to `[Authorize]`-decorated operations | | `AuthorizationHeaderParameterOperationFilter` | `RCommon.Authorization.Web` | Swashbuckle filter: adds a required `Authorization` header parameter to operations protected by `AuthorizeFilter` | --- ## web-utilities Source: https://rcommon.com/docs/security-web/web-utilities --- title: Web Utilities sidebar_position: 2 description: RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps. --- # Web Utilities ## Overview `RCommon.Web` provides ASP.NET Core-specific integrations that bridge RCommon's security abstractions to the HTTP request pipeline. Its primary purpose is to replace the thread-based principal accessor with one that reads the authenticated user from the current `HttpContext`, which is the correct source of identity in ASP.NET Core web applications. The package contains: - `HttpContextCurrentPrincipalAccessor` — an `ICurrentPrincipalAccessor` implementation that reads `HttpContext.User` via `IHttpContextAccessor`. - `WebConfigurationExtensions` — a startup extension method that registers all security services using the HTTP-context-based accessor. ### Why a separate web package? RCommon's core security library (`RCommon.Security`) has no ASP.NET Core dependency. Its default principal accessor reads from `Thread.CurrentPrincipal`, which works in console apps, background services, and test harnesses. In an ASP.NET Core web app the authenticated user is set on `HttpContext.User` by the authentication middleware, not on `Thread.CurrentPrincipal`. `RCommon.Web` provides the adapter that makes the correct source available to all RCommon services. ## Installation This package depends on `RCommon.Security`, which is pulled in automatically. ## Configuration Call `WithClaimsAndPrincipalAccessorForWeb()` instead of `WithClaimsAndPrincipalAccessor()` in your ASP.NET Core startup: ```csharp using RCommon; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRCommon(config => { config.WithClaimsAndPrincipalAccessorForWeb(); }); var app = builder.Build(); // Ensure authentication and authorization middleware run before your endpoints. app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run(); ``` `WithClaimsAndPrincipalAccessorForWeb()` registers the following services: | Interface | Implementation | |-----------|----------------| | `ICurrentPrincipalAccessor` | `HttpContextCurrentPrincipalAccessor` | | `ICurrentUser` | `CurrentUser` | | `ICurrentClient` | `CurrentClient` | | `ITenantIdAccessor` | `ClaimsTenantIdAccessor` | It also calls `services.AddHttpContextAccessor()` so that `IHttpContextAccessor` is available for injection. :::tip Modular composition `WithClaimsAndPrincipalAccessorForWeb()` is `TryAdd`-hardened — repeat calls from multiple modules are idempotent. Each principal/claims/tenant interface is registered exactly once. Modules that need access to `ICurrentUser` can each call this verb without coordinating with other modules. Do not mix `WithClaimsAndPrincipalAccessor()` and `WithClaimsAndPrincipalAccessorForWeb()` in the same process — `TryAdd` will keep whichever ran first and silently ignore the other. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ### Combined with other RCommon features `WithClaimsAndPrincipalAccessorForWeb()` is fluent and chains naturally with persistence, multi-tenancy, and other configuration: ```csharp builder.Services.AddRCommon(config => { config .WithClaimsAndPrincipalAccessorForWeb() .WithPersistence(ef => { ef.AddDbContext( "App", options => options.UseSqlServer( builder.Configuration.GetConnectionString("Default"))); }) .WithMultiTenancy>(mt => { }); }); ``` ## Usage ### Injecting ICurrentUser in a controller or service Once `WithClaimsAndPrincipalAccessorForWeb()` is registered, inject `ICurrentUser` anywhere in your application. The resolved identity comes from the authenticated `HttpContext.User` for the current request: ```csharp using RCommon.Security.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; [Authorize] [ApiController] [Route("api/[controller]")] public class ProfileController : ControllerBase { private readonly ICurrentUser _currentUser; public ProfileController(ICurrentUser currentUser) { _currentUser = currentUser; } [HttpGet] public IActionResult GetProfile() { return Ok(new { UserId = _currentUser.Id, Roles = _currentUser.Roles, TenantId = _currentUser.TenantId }); } } ``` ### Using ICurrentUser in middleware ```csharp using RCommon.Security.Users; public class RequestAuditMiddleware { private readonly RequestDelegate _next; public RequestAuditMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, ICurrentUser currentUser) { if (currentUser.IsAuthenticated) { var userId = currentUser.Id ?? "anonymous"; context.Items["AuditUserId"] = userId; } await _next(context); } } ``` ### Temporarily overriding the principal `HttpContextCurrentPrincipalAccessor` inherits the `Change()` method from `CurrentPrincipalAccessorBase`. The override is stored in `AsyncLocal` and does not mutate `HttpContext.User`, making it safe to use in middleware or background tasks that need to act as a different identity: ```csharp using RCommon.Security.Claims; using System.Security.Claims; public class IntegrationEventHandler { private readonly ICurrentPrincipalAccessor _principalAccessor; private readonly ICurrentUser _currentUser; public IntegrationEventHandler( ICurrentPrincipalAccessor principalAccessor, ICurrentUser currentUser) { _principalAccessor = principalAccessor; _currentUser = currentUser; } public async Task HandleAsync(IntegrationEvent @event) { // Run as a system identity when processing out-of-band events. var systemIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypesConst.UserId, @event.InitiatingUserId.ToString()), new Claim(ClaimTypesConst.TenantId, @event.TenantId) }, "System"); using (_principalAccessor.Change(new ClaimsPrincipal(systemIdentity))) { // ICurrentUser now resolves against systemIdentity. Console.WriteLine($"Processing as user {_currentUser.Id} for tenant {_currentUser.TenantId}"); await ProcessEventAsync(@event); } // Original principal (or null outside a request) is restored here. } private Task ProcessEventAsync(IntegrationEvent @event) => Task.CompletedTask; } ``` ### Switching from ThreadCurrentPrincipalAccessor If you started with `WithClaimsAndPrincipalAccessor()` and are migrating to an ASP.NET Core host, replace it with `WithClaimsAndPrincipalAccessorForWeb()`. The registered interfaces are identical; only the `ICurrentPrincipalAccessor` implementation changes: ```csharp // Before (non-web or background service): config.WithClaimsAndPrincipalAccessor(); // After (ASP.NET Core web application): config.WithClaimsAndPrincipalAccessorForWeb(); ``` No other code changes are needed. All services that depend on `ICurrentUser`, `ICurrentClient`, or `ITenantIdAccessor` continue to work without modification. ## API Summary | Type | Package | Description | |------|---------|-------------| | `HttpContextCurrentPrincipalAccessor` | `RCommon.Web` | Reads the current `ClaimsPrincipal` from `IHttpContextAccessor.HttpContext.User`; falls back to `null` outside of a request | | `WebConfigurationExtensions.WithClaimsAndPrincipalAccessorForWeb` | `RCommon.Web` | Registers `HttpContextCurrentPrincipalAccessor`, `ICurrentUser`, `ICurrentClient`, `ITenantIdAccessor`, and `IHttpContextAccessor` | | `ICurrentPrincipalAccessor` | `RCommon.Security` | Core abstraction; exposes `Principal` and `Change(ClaimsPrincipal)` | | `CurrentPrincipalAccessorBase` | `RCommon.Security` | Abstract base; `Change()` stores the override in `AsyncLocal` so it flows across async continuations | | `ThreadCurrentPrincipalAccessor` | `RCommon.Security` | Non-web default; reads `Thread.CurrentPrincipal` | --- ## Newtonsoft.Json Source: https://rcommon.com/docs/serialization/newtonsoft RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options. # Newtonsoft.Json ## Overview `RCommon.JsonNet` wraps Newtonsoft.Json (Json.NET) behind the `IJsonSerializer` interface. It provides the full feature set of Json.NET — custom converters, reference handling, polymorphic serialization, and so on — while allowing your application code to depend only on the common abstraction. The underlying `JsonSerializerSettings` instance is registered in the DI container via the options pattern, so you can apply any Newtonsoft.Json setting that `JsonSerializerSettings` exposes. ## Installation ## Configuration ### Default setup ```csharp using RCommon; using RCommon.JsonNet; builder.Services.AddRCommon() .WithJsonSerialization(); ``` This registers `JsonNetSerializer` as the `IJsonSerializer` implementation with default `JsonSerializeOptions` (camelCase enabled, indentation disabled). ### Configuring global serialize options ```csharp builder.Services.AddRCommon() .WithJsonSerialization(opts => { opts.CamelCase = true; opts.Indented = true; }); ``` ### Configuring Newtonsoft.Json settings directly Use the `Configure` extension on `IJsonNetBuilder` to access the underlying `JsonSerializerSettings`: ```csharp using Newtonsoft.Json; using RCommon; using RCommon.JsonNet; builder.Services.AddRCommon() .WithJsonSerialization(b => { b.Configure(settings => { settings.NullValueHandling = NullValueHandling.Ignore; settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; settings.Converters.Add(new StringEnumConverter()); }); }); ``` Combining global serialize options with custom settings: ```csharp builder.Services.AddRCommon() .WithJsonSerialization( serializeOptions: opts => { opts.CamelCase = true; opts.Indented = false; }, deSerializeOptions: _ => { }, actions: b => { b.Configure(settings => { settings.NullValueHandling = NullValueHandling.Ignore; }); }); ``` ### Modular composition `WithJsonSerialization()` is a singleton-style verb across all six overloads (parameterless, `serializeOptions`, `serializeOptions + deSerializeOptions`, `actions`, `serializeOptions + actions`, and `serializeOptions + deSerializeOptions + actions`). Only one `IJsonSerializer` implementation makes sense per process, so the verb enforces the choice at startup. | Re-registration shape | Behaviour | |---|---| | `WithJsonSerialization()` called from two modules | Idempotent no-op — the second registration is skipped, but the configuration delegate still runs so per-call option values follow last-write-wins. | | One module calls `WithJsonSerialization()` and another calls `WithJsonSerialization()` | Throws `RCommonBuilderException` naming both builder types. Pick exactly one JSON serializer. | See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ## Usage Inject `IJsonSerializer` and call `Serialize` or `Deserialize` as needed. ```csharp public class OrderSyncService { private readonly IJsonSerializer _serializer; public OrderSyncService(IJsonSerializer serializer) { _serializer = serializer; } public string SerializeOrder(Order order) { return _serializer.Serialize(order); } public Order? DeserializeOrder(string json) { return _serializer.Deserialize(json); } } ``` ### Using a custom Newtonsoft.Json converter Register the converter in settings during startup (see Configuration above), then serialize and deserialize as normal. The converter applies automatically to all calls through `IJsonSerializer`. ```csharp // In startup — register a custom converter. b.Configure(settings => { settings.Converters.Add(new MyCustomConverter()); }); // In application code — no reference to Newtonsoft.Json needed. string json = _serializer.Serialize(myObject); MyType result = _serializer.Deserialize(json)!; ``` ## API Summary ### `JsonNetBuilder` Implements `IJsonBuilder` and `IJsonNetBuilder`. Registered automatically by `WithJsonSerialization`. On construction it registers `JsonNetSerializer` as `IJsonSerializer` with a transient lifetime. ### `IJsonNetBuilder` extension methods | Method | Description | |--------|-------------| | `Configure(Action)` | Configures the underlying `JsonSerializerSettings` used by `JsonNetSerializer`. Returns `IJsonNetBuilder` for chaining. | ### `JsonNetSerializer` Registered as `IJsonSerializer`. Accepts `IOptions` via constructor injection. Implements all four `IJsonSerializer` methods. Per-call `JsonSerializeOptions` or `JsonDeserializeOptions` override the shared settings instance for that call: `CamelCase = true` applies `CamelCasePropertyNamesContractResolver`; `Indented = true` sets `Formatting.Indented`. --- ## Overview Source: https://rcommon.com/docs/serialization/overview Provider-agnostic JSON serialization for .NET with IJsonSerializer, supporting Newtonsoft.Json and System.Text.Json with per-call and global option configuration. # Serialization Overview ## Overview RCommon provides a thin abstraction over JSON serialization so that the rest of your application depends on an interface rather than a specific library. You can swap between Newtonsoft.Json (Json.NET) and System.Text.Json without touching any code outside of startup configuration. ### Why abstract serialization? Different teams and libraries have different preferences and compatibility requirements. Newtonsoft.Json has a richer feature set and broader third-party support; System.Text.Json is part of the .NET runtime and tends to perform better on modern runtimes. By writing against `IJsonSerializer`, your domain and application services remain insulated from that choice, and testing becomes simpler because the serializer can be swapped for a stub. ### Core abstractions `IJsonSerializer` is the single interface your code should depend on. It exposes four methods: - `Serialize(object, options?)` — serialize to a JSON string using the runtime type. - `Serialize(object, type, options?)` — serialize using a specific declared type, useful when the declared type differs from the runtime type. - `Deserialize(json, options?)` — deserialize to a strongly typed result. - `Deserialize(json, type, options?)` — deserialize when the target type is only known at runtime. `JsonSerializeOptions` carries per-call serialization settings: `CamelCase` (defaults to `true`) and `Indented` (defaults to `false`). `JsonDeserializeOptions` carries per-call deserialization settings: `CamelCase` (defaults to `true`). The global defaults for these options are set during startup via the `WithJsonSerialization` builder overloads, so most code can call `Serialize` and `Deserialize` without passing options at all. ## Installation Install the abstractions package: Then install one provider package: or ## Configuration Call `WithJsonSerialization` inside your `AddRCommon()` block. Pass the builder type for the provider you want to use. ### Default configuration ```csharp using RCommon; using RCommon.JsonNet; builder.Services.AddRCommon() .WithJsonSerialization(); ``` This uses Newtonsoft.Json with camelCase naming and no indentation — the defaults defined in `JsonSerializeOptions`. ### Configuring serialize options globally ```csharp builder.Services.AddRCommon() .WithJsonSerialization(opts => { opts.CamelCase = true; opts.Indented = true; }); ``` ### Configuring both serialize and deserialize options ```csharp builder.Services.AddRCommon() .WithJsonSerialization( serializeOptions: opts => { opts.CamelCase = true; opts.Indented = false; }, deSerializeOptions: opts => { opts.CamelCase = true; }); ``` ## Usage Inject `IJsonSerializer` wherever you need to serialize or deserialize JSON. ```csharp public class ReportService { private readonly IJsonSerializer _serializer; public ReportService(IJsonSerializer serializer) { _serializer = serializer; } public string BuildPayload(ReportRequest request) { return _serializer.Serialize(request); } public ReportResponse? ParseResponse(string json) { return _serializer.Deserialize(json); } } ``` ### Overriding options per call ```csharp // Force indented output for a debug log entry. string pretty = _serializer.Serialize( obj: myObject, options: new JsonSerializeOptions { Indented = true }); ``` ### Using the declared type override ```csharp // Serialize using the base type to avoid including derived-type properties. string json = _serializer.Serialize( obj: derivedInstance, type: typeof(BaseClass)); ``` ## API Summary ### `IJsonSerializer` | Method | Description | |--------|-------------| | `Serialize(obj, options?)` | Serializes `obj` to a JSON string using its runtime type. | | `Serialize(obj, type, options?)` | Serializes `obj` using the specified declared type. | | `Deserialize(json, options?)` | Deserializes `json` into an instance of `T`. | | `Deserialize(json, type, options?)` | Deserializes `json` into an instance of the given type. | ### `JsonSerializeOptions` | Property | Default | Description | |----------|---------|-------------| | `CamelCase` | `true` | Use camelCase property names during serialization. | | `Indented` | `false` | Produce indented (pretty-printed) JSON output. | ### `JsonDeserializeOptions` | Property | Default | Description | |----------|---------|-------------| | `CamelCase` | `true` | Expect camelCase property names during deserialization. | ### `IRCommonBuilder` extension | Method | Description | |--------|-------------| | `WithJsonSerialization()` | Registers the serializer with default options. | | `WithJsonSerialization(serializeOptions)` | Registers with custom serialize options. | | `WithJsonSerialization(deSerializeOptions)` | Registers with custom deserialize options. | | `WithJsonSerialization(serializeOptions, deSerializeOptions)` | Registers with both options configured. | | `WithJsonSerialization(actions)` | Registers and passes a configuration action to the builder itself. | | `WithJsonSerialization(serializeOptions, deSerializeOptions, actions)` | Full overload; covers all configuration scenarios. | --- ## System.Text.Json Source: https://rcommon.com/docs/serialization/system-text-json RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters. # System.Text.Json ## Overview `RCommon.SystemTextJson` wraps the built-in `System.Text.Json` library behind the `IJsonSerializer` interface. Because `System.Text.Json` ships with the .NET runtime, this provider adds no external dependencies beyond the NuGet package itself, making it a good default for greenfield applications targeting .NET 6 or later. The underlying `JsonSerializerOptions` instance is registered via the options pattern so you can configure every aspect of the serializer that `JsonSerializerOptions` exposes, including custom converters. The package also includes two ready-to-use converters: - `JsonIntEnumConverter` — serializes enums as integers. - `JsonByteEnumConverter` — serializes enums as bytes. ## Installation ## Configuration ### Default setup ```csharp using RCommon; using RCommon.SystemTextJson; builder.Services.AddRCommon() .WithJsonSerialization(); ``` This registers `TextJsonSerializer` as the `IJsonSerializer` implementation with default `JsonSerializeOptions` (camelCase enabled, indentation disabled). ### Configuring global serialize options ```csharp builder.Services.AddRCommon() .WithJsonSerialization(opts => { opts.CamelCase = true; opts.Indented = true; }); ``` ### Configuring JsonSerializerOptions directly Use the `Configure` extension on `ITextJsonBuilder` to access the underlying `JsonSerializerOptions`: ```csharp using System.Text.Json; using RCommon; using RCommon.SystemTextJson; builder.Services.AddRCommon() .WithJsonSerialization(b => { b.Configure(options => { options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; options.Converters.Add(new JsonStringEnumConverter()); }); }); ``` Combining global serialize options with custom `JsonSerializerOptions`: ```csharp builder.Services.AddRCommon() .WithJsonSerialization( serializeOptions: opts => { opts.CamelCase = true; opts.Indented = false; }, deSerializeOptions: _ => { }, actions: b => { b.Configure(options => { options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; }); }); ``` ### Using enum converters ```csharp using RCommon.SystemTextJson; builder.Services.AddRCommon() .WithJsonSerialization(b => { b.Configure(options => { // Serialize enums as integer values. options.Converters.Add(new JsonIntEnumConverter()); }); }); ``` ### Modular composition `WithJsonSerialization()` is a singleton-style verb across all six overloads (parameterless, `serializeOptions`, `serializeOptions + deSerializeOptions`, `actions`, `serializeOptions + actions`, and `serializeOptions + deSerializeOptions + actions`). Only one `IJsonSerializer` implementation makes sense per process, so the verb enforces the choice at startup. | Re-registration shape | Behaviour | |---|---| | `WithJsonSerialization()` called from two modules | Idempotent no-op — the second registration is skipped, but the configuration delegate still runs so per-call option values follow last-write-wins. | | One module calls `WithJsonSerialization()` and another calls `WithJsonSerialization()` | Throws `RCommonBuilderException` naming both builder types. Pick exactly one JSON serializer. | See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ## Usage Inject `IJsonSerializer` and call `Serialize` or `Deserialize`. ```csharp public class EventPublisher { private readonly IJsonSerializer _serializer; public EventPublisher(IJsonSerializer serializer) { _serializer = serializer; } public string ToJson(DomainEvent @event) { return _serializer.Serialize(@event); } public DomainEvent? FromJson(string json) { return _serializer.Deserialize(json); } } ``` ### Per-call options ```csharp // Produce indented output for diagnostic logging. string pretty = _serializer.Serialize( obj: payload, options: new JsonSerializeOptions { Indented = true, CamelCase = true }); ``` ## API Summary ### `TextJsonBuilder` Implements `IJsonBuilder` and `ITextJsonBuilder`. Registered automatically by `WithJsonSerialization`. On construction it registers `TextJsonSerializer` as `IJsonSerializer` with a transient lifetime. ### `ITextJsonBuilder` extension methods | Method | Description | |--------|-------------| | `Configure(Action)` | Configures the underlying `JsonSerializerOptions` used by `TextJsonSerializer`. Returns `ITextJsonBuilder` for chaining. | ### `TextJsonSerializer` Registered as `IJsonSerializer`. Accepts `IOptions` via constructor injection. Implements all four `IJsonSerializer` methods. Per-call `JsonSerializeOptions` or `JsonDeserializeOptions` mutate the shared options instance for that call: `CamelCase = true` sets `PropertyNamingPolicy = JsonNamingPolicy.CamelCase`; `Indented = true` sets `WriteIndented = true`. ### Built-in converters | Type | Description | |------|-------------| | `JsonIntEnumConverter` | Serializes and deserializes enum values as `int`. | | `JsonByteEnumConverter` | Serializes and deserializes enum values as `byte`. | --- ## Overview Source: https://rcommon.com/docs/state-machines/overview Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling. # State Machines Overview RCommon provides a provider-agnostic state machine abstraction that lets you define finite state machines (FSMs) using plain C# enums for states and triggers. The abstraction is backed by pluggable adapters, currently the `Stateless` library and a lightweight dictionary-based implementation included in `RCommon.MassTransit.StateMachines`. ## The state machine pattern A finite state machine models a process that is always in exactly one of a finite number of states. Transitions between states are driven by triggers (also called events or signals). Each transition can be unconditional or guarded by a condition. States can have entry and exit actions that fire async side effects when the machine moves in or out of them. Common use cases include: - Order lifecycle management (Pending → Approved → Shipped → Delivered) - Workflow approval processes (Draft → Submitted → Under Review → Approved / Rejected) - Connection state tracking (Connecting → Connected → Disconnecting → Disconnected) - Document publishing pipelines (Draft → Review → Published / Archived) ## Core abstractions ### IStateMachineConfigurator<TState, TTrigger> The entry point for defining a state machine. Call `ForState` once per state to configure its transitions, then call `Build` to create a running machine instance: ```csharp public interface IStateMachineConfigurator where TState : struct, Enum where TTrigger : struct, Enum { IStateConfigurator ForState(TState state); IStateMachine Build(TState initialState); } ``` A single configurator instance can be used to build multiple independent machine instances with different initial states, all sharing the same transition configuration. ### IStateConfigurator<TState, TTrigger> Returned by `ForState`, used to define transitions and actions for a specific state: ```csharp public interface IStateConfigurator where TState : struct, Enum where TTrigger : struct, Enum { IStateConfigurator Permit(TTrigger trigger, TState destinationState); IStateConfigurator PermitIf(TTrigger trigger, TState destinationState, Func guard); IStateConfigurator OnEntry(Func action); IStateConfigurator OnExit(Func action); } ``` ### IStateMachine<TState, TTrigger> The running state machine instance. Inspect current state, check permitted triggers, and fire them: ```csharp public interface IStateMachine where TState : struct, Enum where TTrigger : struct, Enum { TState CurrentState { get; } bool CanFire(TTrigger trigger); IEnumerable PermittedTriggers { get; } Task FireAsync(TTrigger trigger, CancellationToken cancellationToken = default); Task FireAsync(TTrigger trigger, TData data, CancellationToken cancellationToken = default); } ``` ## Stateless vs MassTransit adapter RCommon ships two adapters for the state machine abstraction. Both register as `IStateMachineConfigurator` and produce `IStateMachine` instances: | Aspect | `RCommon.Stateless` | `RCommon.MassTransit.StateMachines` | |--------|---------------------|-------------------------------------| | Backing library | `Stateless` NuGet package | Custom dictionary-based FSM (no extra dependency) | | Parameterized triggers | Fully supported via Stateless `SetTriggerParameters` | Accepted but data parameter is ignored | | Configuration model | Deferred: actions recorded and replayed on each `Build` call | Shared dictionary cloned per `Build` call | | Registration | `WithStatelessStateMachine()` | `WithMassTransitStateMachine()` | | Suitable for | Most applications; rich guard and parameterized trigger support | Simple state tracking in messaging-heavy contexts | Use `RCommon.Stateless` unless you have a specific reason to prefer the dictionary-based adapter. ## Choosing an adapter Choose `RCommon.Stateless` when: - Your state machine needs parameterized triggers with data passed to entry actions. - You want the full feature set of the Stateless library (sub-states, trigger parameters, dot-graph export). - You prefer a well-established open-source library as the engine. Choose `RCommon.MassTransit.StateMachines` when: - You are already using `RCommon.MassTransit` and want to avoid an additional dependency. - Your state machine only uses simple unconditional transitions and guarded transitions with no trigger data. :::tip Modular composition Both `WithStatelessStateMachine()` and `WithMassTransitStateMachine()` are `TryAdd`-hardened — repeat calls from multiple modules are idempotent and the open-generic `IStateMachineConfigurator<,>` registration is added exactly once. Modules can opt into state-machine support independently. Registering both adapters in the same process is not recommended (the second registration is ignored by `TryAdd`); pick one per process. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Section contents - [Stateless](./stateless.mdx) — Stateless library integration, defining states/triggers, full configuration reference --- ## Stateless Source: https://rcommon.com/docs/state-machines/stateless RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions. # Stateless `RCommon.Stateless` wraps the popular [Stateless](https://github.com/dotnet-state-machine/stateless) library behind the RCommon `IStateMachineConfigurator` abstraction. Application code works only with the RCommon interfaces; the Stateless library is an implementation detail. ## Installation ## Configuration Register the Stateless adapter with `WithStatelessStateMachine` on the RCommon builder: ```csharp using RCommon; builder.Services.AddRCommon() .WithStatelessStateMachine(); ``` This registers `StatelessConfigurator` as the open-generic `IStateMachineConfigurator` implementation. Any combination of state and trigger enum types is automatically available for injection. :::tip Runnable example See `Examples/StateMachines/Examples.StateMachines.Stateless/` for a complete console app exercising guarded transitions, entry/exit actions, and parameterized triggers using this exact order-state-machine walkthrough. ::: :::tip Modular composition `WithStatelessStateMachine()` is `TryAdd`-hardened — repeat calls from multiple modules are idempotent. The open-generic `IStateMachineConfigurator<,>` registration is added exactly once regardless of how many modules invoke this verb. Modules can opt into state-machine support independently without coordinating. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Defining states and triggers States and triggers are plain `struct` enums: ```csharp public enum OrderState { Pending, Approved, Shipped, Cancelled } public enum OrderTrigger { Approve, Ship, Cancel } ``` ## Configuring a state machine Inject `IStateMachineConfigurator` and configure each state using `ForState`. Call `Build` to produce a running machine instance: ```csharp public class OrderStateMachineService { private readonly IStateMachineConfigurator _configurator; public OrderStateMachineService( IStateMachineConfigurator configurator) { _configurator = configurator; _configurator.ForState(OrderState.Pending) .Permit(OrderTrigger.Approve, OrderState.Approved) .Permit(OrderTrigger.Cancel, OrderState.Cancelled); _configurator.ForState(OrderState.Approved) .Permit(OrderTrigger.Ship, OrderState.Shipped) .Permit(OrderTrigger.Cancel, OrderState.Cancelled) .OnEntry(async ct => { await NotifyWarehouseAsync(ct); }) .OnExit(async ct => { await LogStateChangeAsync("leaving Approved", ct); }); _configurator.ForState(OrderState.Shipped) .OnEntry(async ct => { await SendShippingConfirmationAsync(ct); }); } public IStateMachine BuildFor(OrderState currentState) => _configurator.Build(currentState); } ``` Each call to `Build` produces a fully independent machine instance with its own current state. The configurator can be reused across the lifetime of the application without resetting. ## How deferred configuration works `StatelessConfigurator` uses a deferred pattern internally. Calls to `ForState` record configuration actions rather than applying them immediately. When `Build` is called: 1. A new `Stateless.StateMachine` is created with the given initial state. 2. All recorded configuration actions are replayed against the new machine. 3. The machine is wrapped in `StatelessStateMachine` and returned as `IStateMachine`. This allows one `IStateMachineConfigurator` instance (registered as transient) to produce many independent machines. ## Guarded transitions Use `PermitIf` to add a condition to a transition. The guard is evaluated when `CanFire` or `FireAsync` is called: ```csharp _configurator.ForState(OrderState.Approved) .PermitIf(OrderTrigger.Ship, OrderState.Shipped, () => _inventoryService.HasStock()) .Permit(OrderTrigger.Cancel, OrderState.Cancelled); ``` ## Firing triggers ```csharp var machine = _service.BuildFor(order.State); if (machine.CanFire(OrderTrigger.Approve)) { await machine.FireAsync(OrderTrigger.Approve, cancellationToken); order.State = machine.CurrentState; await _repository.UpdateAsync(order, cancellationToken); } ``` ## Parameterized triggers The Stateless adapter fully supports parameterized triggers. The `FireAsync` overload passes data to the underlying Stateless machine using `SetTriggerParameters`: ```csharp // Fire a trigger and pass data to the entry action await machine.FireAsync(OrderTrigger.Ship, new ShipmentDetails { Carrier = "FedEx" }, cancellationToken); ``` The trigger parameter descriptor is cached by `(trigger, dataType)` to prevent double-registration, which Stateless does not allow. ## Entry and exit actions Entry and exit actions are `Func`. Stateless's own `OnEntryAsync`/`OnExitAsync` do not accept a `CancellationToken`; the adapter passes `CancellationToken.None` internally when forwarding to Stateless. The `CancellationToken` you provide to `FireAsync` is still checked before the trigger fires. ```csharp _configurator.ForState(OrderState.Approved) .OnEntry(async ct => { // ct is passed from FireAsync; Stateless itself receives CancellationToken.None await _emailService.SendApprovalEmailAsync(ct); }); ``` ## Checking permitted triggers ```csharp var machine = _service.BuildFor(order.State); foreach (var trigger in machine.PermittedTriggers) { Console.WriteLine($"Can fire: {trigger}"); } ``` ## API summary | Type | Package | Description | |------|---------|-------------| | `StatelessConfigurator` | `RCommon.Stateless` | Implements `IStateMachineConfigurator`; records deferred config and builds machines | | `StatelessStateMachine` | `RCommon.Stateless` | Wraps `Stateless.StateMachine` as `IStateMachine` | | `DeferredStateConfigurator` | `RCommon.Stateless` | Internal class that records state configuration for replay at build time | | `IStateMachineConfigurator` | `RCommon.Core` | Abstraction; implement to swap the backing engine | | `IStateMachine` | `RCommon.Core` | Running machine abstraction | | `IStateConfigurator` | `RCommon.Core` | Per-state transition and action configuration | | `WithStatelessStateMachine()` | `RCommon.Stateless` | Extension method on `IRCommonBuilder` | --- ## overview Source: https://rcommon.com/docs/testing/overview --- title: Overview sidebar_position: 1 description: Learn how to test .NET applications built on RCommon using mocks, EF Core InMemory, and xUnit or NUnit base classes for unit and integration test strategies. --- # Testing with RCommon RCommon is designed with testability as a first-class concern. Because RCommon wraps infrastructure concerns — persistence, messaging, caching, emailing — behind clean abstractions, you can substitute real implementations with mocks or in-memory alternatives in your test suite without changing your application code. ## Testing Philosophy RCommon separates infrastructure concerns from domain logic through interfaces. A service that writes to a repository via `IGraphRepository` does not know whether it is talking to SQL Server, an in-memory database, or a mock. The same is true for `IEmailService`, `ICacheService`, `IMediatorService`, and all other infrastructure services. This makes three testing strategies practical: - **Unit tests** — mock infrastructure interfaces with Moq; test business logic in isolation with no external dependencies. - **Integration tests** — use in-memory providers (EF Core InMemory, `IDistributedCache` in-memory) to test the full stack cheaply. - **End-to-end tests** — wire real providers against a test database or container; RCommon's base classes handle bootstrapping. ## When to Use Each Strategy | Strategy | When to Use | Tools | |---|---|---| | Unit tests | Testing a single class in isolation | Moq, FluentAssertions, xUnit | | Integration tests | Testing a stack of components including DI and the persistence layer | RCommon.TestBase, EF Core InMemory, xUnit | | End-to-end tests | Testing against real infrastructure in CI | RCommon.TestBase, SQL Server, Docker | ## Mocking RCommon Abstractions Because all RCommon services are registered as interfaces in the DI container, they are straightforward to mock with Moq. ### Mocking a Repository ```csharp using Moq; using RCommon.Persistence.Crud; using FluentAssertions; public class OrderServiceTests { private readonly Mock> _mockOrderRepo; private readonly Mock _mockUoWFactory; private readonly OrderService _sut; public OrderServiceTests() { _mockOrderRepo = new Mock>(); _mockUoWFactory = new Mock(); _sut = new OrderService(_mockOrderRepo.Object, _mockUoWFactory.Object); } [Fact] public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder() { // Arrange var expected = new Order { Id = 1, ProductName = "Widget" }; _mockOrderRepo .Setup(r => r.FindAsync(1, default)) .ReturnsAsync(expected); // Act var result = await _sut.GetOrderAsync(1); // Assert result.Should().BeEquivalentTo(expected); } } ``` ### Mocking an HTTP Client The `TestBootstrapper` base class provides a `CreateMockHttpClient` helper for testing services that depend on `IHttpClientFactory`: ```csharp using System.Net; using System.Net.Http; using RCommon.TestBase; using Moq; public class ExternalApiServiceTests : TestBootstrapper { [Test] public async Task FetchData_WhenServerReturns500_ReturnsNull() { // Arrange var mockResponse = new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError }; var mockFactory = CreateMockHttpClient(mockResponse); var service = new ExternalApiService(mockFactory.Object); // Act var result = await service.FetchDataAsync(); // Assert Assert.That(result, Is.Null); } } ``` ### Mocking Time-Dependent Code `ISystemTime` abstracts the system clock, making any code that uses it testable: ```csharp using Moq; using RCommon; var mockTime = new Mock(); mockTime.Setup(t => t.Now).Returns(new DateTime(2024, 6, 15, 12, 0, 0)); var service = new SubscriptionRenewalService(mockTime.Object, /* ... */); ``` ## Integration Testing with In-Memory Providers For integration tests, use EF Core's InMemory database to test the full persistence stack without a real database: ```csharp using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using RCommon.Persistence.EFCore; using Xunit; public class OrderRepositoryIntegrationTests : IDisposable { private readonly ServiceProvider _serviceProvider; public OrderRepositoryIntegrationTests() { var services = new ServiceCollection(); services.AddLogging(); services.AddSingleton(); var builder = new EFCorePersistenceBuilder(services); builder.AddDbContext("AppDb", options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); builder.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb"); _serviceProvider = services.BuildServiceProvider(); } public void Dispose() => _serviceProvider?.Dispose(); [Fact] public async Task AddAsync_PersistsEntity() { // Arrange var repo = _serviceProvider.GetRequiredService>(); var order = new Order { ProductName = "Widget", Quantity = 5 }; // Act await repo.AddAsync(order); // Assert var retrieved = await repo.FindAsync(order.Id); retrieved.Should().NotBeNull(); retrieved!.ProductName.Should().Be("Widget"); } } ``` ## Using the xUnit TestFixture Base Class For xUnit projects, `TestFixture` (from `RCommon.TestBase.XUnit`) provides DI bootstrapping, configuration loading, and service resolution in a single base class: ```csharp using RCommon.TestBase.XUnit; using Microsoft.Extensions.DependencyInjection; public class ProductServiceTests : TestFixture { protected override void ConfigureServices(IServiceCollection services) { base.ConfigureServices(services); // Add your services and mocks services.AddSingleton>(); services.AddTransient(); } [Fact] public void ProductService_CanBeResolved() { var service = GetService(); service.Should().NotBeNull(); } } ``` ## Event Handling in Tests Test event handling by subscribing to the in-memory event bus: ```csharp using RCommon.EventHandling; using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection(); services.AddRCommon(builder => { builder.WithEventHandling(events => { events.AddSubscriber(); }); }); var provider = services.BuildServiceProvider(); var eventBus = provider.GetRequiredService(); // Publish and verify the handler was called await eventBus.Publish(new OrderCreatedEvent { OrderId = Guid.NewGuid() }); ``` ## Testing CQRS Handlers Test command and query handlers directly by instantiating them with mock dependencies, bypassing the bus: ```csharp public class CreateOrderHandlerTests { private readonly Mock> _mockRepo; private readonly CreateOrderHandler _sut; public CreateOrderHandlerTests() { _mockRepo = new Mock>(); _sut = new CreateOrderHandler(_mockRepo.Object); } [Fact] public async Task HandleAsync_ValidCommand_AddsOrder() { // Arrange var command = new CreateOrderCommand { ProductName = "Widget", Quantity = 3 }; // Act var result = await _sut.HandleAsync(command, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); _mockRepo.Verify(r => r.AddAsync(It.IsAny(), default), Times.Once); } } ``` ## Summary RCommon's abstraction boundaries make all major infrastructure concerns testable: - Repository operations via `IReadOnlyRepository`, `IWriteOnlyRepository`, `ILinqRepository`, `IGraphRepository`, and `ISqlMapperRepository` - Unit of work and transactions via `IUnitOfWork` and `IUnitOfWorkFactory` - Event handling via `IEventBus`, `ISubscriber`, and `IEventProducer` - Mediator operations via `IMediatorService` - Email sending via `IEmailService` - Caching via `ICacheService` - Time-dependent logic via `ISystemTime` - HTTP clients via `IHttpClientFactory` (with the built-in mock helper on `TestBootstrapper`) --- ## test-base-classes Source: https://rcommon.com/docs/testing/test-base-classes --- title: Test Base Classes sidebar_position: 2 description: Reference guide for RCommon.TestBase, TestBootstrapper, TestFixture for xUnit, TestRepository for EF Core integration tests, and TestDataActions fake data generation. --- # Test Base Classes RCommon provides three test support packages that reduce boilerplate across unit and integration tests: `RCommon.TestBase`, `RCommon.TestBase.XUnit`, and `RCommon.TestBase.Data`. These packages are internal test utilities — they are not published as NuGet packages — but their patterns are representative of how to structure your own test helpers. ## RCommon.TestBase The `RCommon.TestBase` package is the foundational test support library. It provides: - `TestBootstrapper` — an abstract base class with DI bootstrapping, configuration loading, logging via Serilog, and HTTP client mocking helpers - `TestDataActions` — a static factory using [Bogus](https://github.com/bchavez/Bogus) to generate realistic test entity stubs - Test entity types (`Customer`, `Order`, `OrderItem`, `Product`, `SalesPerson`) used across integration test suites - `CustomerSearchSpec` — a `PagedSpecification` example that demonstrates the specification pattern ### TestBootstrapper `TestBootstrapper` is the base for NUnit integration test classes. It sets up DI, loads `appsettings.json`, and wires Serilog for structured log output during test runs. ```csharp using Microsoft.Extensions.DependencyInjection; using RCommon.TestBase; [TestFixture] public class MyServiceTests : TestBootstrapper { private ServiceProvider _provider; [SetUp] public void Setup() { var services = new ServiceCollection(); InitializeBootstrapper(services); // Register application services services.AddTransient(); _provider = services.BuildServiceProvider(); } [Test] public void MyService_CanBeResolved() { var service = _provider.GetRequiredService(); Assert.That(service, Is.Not.Null); } } ``` `InitializeBootstrapper` does the following: 1. Builds an `IConfigurationRoot` from `appsettings.json` (optional, reload on change) 2. Registers `IConfiguration` as a singleton 3. Adds Serilog logging, configured from the app settings file After calling it you can add your own services and build the `ServiceProvider`. ### Mocking HTTP Clients `TestBootstrapper` exposes `CreateMockHttpClient(HttpResponseMessage)` which wraps Moq to return a pre-configured `IHttpClientFactory`. This avoids the need for real network calls in tests: ```csharp using System.Net; using System.Net.Http; using RCommon.TestBase; using Moq; [TestFixture] public class PaymentGatewayTests : TestBootstrapper { [Test] public async Task PostPayment_WhenServerReturns200_ReturnsSuccess() { // Arrange var mockResponse = new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"status\":\"approved\"}") }; var mockFactory = CreateMockHttpClient(mockResponse); var gateway = new PaymentGateway(mockFactory.Object); // Act var result = await gateway.PostPaymentAsync(new PaymentRequest()); // Assert Assert.That(result.IsApproved, Is.True); } [Test] public async Task PostPayment_WhenServerReturns500_ReturnsFailure() { // Arrange var mockResponse = new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError }; var mockFactory = CreateMockHttpClient(mockResponse); var gateway = new PaymentGateway(mockFactory.Object); // Act var result = await gateway.PostPaymentAsync(new PaymentRequest()); // Assert Assert.That(result.IsApproved, Is.False); } } ``` The returned `Mock` is configured so that calls to `CreateClient(string)` return an `HttpClient` backed by the mocked `HttpMessageHandler`. ### TestDataActions `TestDataActions` is a static class that generates realistic fake data using the Bogus library. It is used across the test suite to seed test entities without hand-coding property values. ```csharp using RCommon.TestBase; // Generate a customer with randomized fields var customer = TestDataActions.CreateCustomerStub(); // Generate a customer and customize specific fields var knownCustomer = TestDataActions.CreateCustomerStub(c => { c.FirstName = "Albus"; c.City = "Hogsmeade"; }); // Generate an order var order = TestDataActions.CreateOrderStub(); // Generate an order with a customization var futureOrder = TestDataActions.CreateOrderStub(o => { o.OrderDate = DateTime.UtcNow; o.ShipDate = DateTime.UtcNow.AddDays(3); }); // Generate other entity types var product = TestDataActions.CreateProductStub(); var salesPerson = TestDataActions.CreateSalesPersonStub(); var orderItem = TestDataActions.CreateOrderItemStub(i => i.Quantity = 10); ``` The methods use `Faker` rules to populate realistic values for addresses, names, prices, and dates. The optional `Action` overload lets you override specific fields after the fake data is generated. ## RCommon.TestBase.XUnit `RCommon.TestBase.XUnit` provides an abstract `TestFixture` class for xUnit test projects. It encapsulates the DI container setup, configuration loading, and service resolution lifecycle for xUnit's constructor-based test initialization. ### TestFixture ```csharp using RCommon.TestBase.XUnit; using Microsoft.Extensions.DependencyInjection; namespace MyApp.Tests; public class OrderServiceTests : TestFixture { protected override void ConfigureServices(IServiceCollection services) { // Always call base first — it registers IConfiguration and logging base.ConfigureServices(services); // Register your application and test services services.AddSingleton(new Mock().Object); services.AddTransient(); } [Fact] public void OrderService_CanBeResolved() { var service = GetService(); service.Should().NotBeNull(); } [Fact] public T? TryGetOptional() where T : class { // GetOptionalService returns null rather than throwing if not registered return GetOptionalService(); } } ``` `TestFixture` implements `IDisposable` and tears down the `ServiceProvider` after each test class. The lifecycle is: 1. Constructor runs `BuildConfiguration()` — reads `appsettings.json` from the current directory if present 2. Constructor calls `ConfigureServices(services)` — your override adds services here 3. Constructor builds the `ServiceProvider` 4. Tests call `GetService()` or `GetOptionalService()` to resolve services 5. `Dispose()` disposes the provider when the test class is torn down by xUnit ### Integration Test Example The following example shows how to use `TestFixture` to set up an integration test against an EF Core InMemory database: ```csharp using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using RCommon.TestBase.XUnit; using RCommon.Persistence.EFCore; using Xunit; public class CustomerRepositoryIntegrationTests : TestFixture, IDisposable { protected override void ConfigureServices(IServiceCollection services) { base.ConfigureServices(services); services.AddSingleton(); var builder = new EFCorePersistenceBuilder(services); builder.AddDbContext("AppDb", options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); builder.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb"); } [Fact] public async Task AddAsync_ThenFind_ReturnsSameCustomer() { // Arrange var repo = GetService>(); var customer = new Customer { FirstName = "Hermione", LastName = "Granger" }; // Act await repo.AddAsync(customer); var found = await repo.FindAsync(customer.Id); // Assert found.Should().NotBeNull(); found!.FirstName.Should().Be("Hermione"); } } ``` ## RCommon.TestBase.Data `RCommon.TestBase.Data` provides `TestRepository`, a utility class for seeding and cleaning up entity state in integration tests backed by EF Core. It wraps `DbContext` directly and coordinates entity lifecycle for test setup and teardown. ### TestRepository ```csharp using RCommon.TestBase.Data; using RCommon.TestBase; [TestFixture] public class CustomerRepositoryTests { private TestRepository _testRepo; [SetUp] public void SetUp() { // Initialize with a real or in-memory DbContext var context = /* resolve AppDbContext */; _testRepo = new TestRepository(context); } [TearDown] public void TearDown() { // Remove only the entities created during this test _testRepo.CleanUpSeedData(); } [Test] public async Task FindAsync_ByPrimaryKey_ReturnsMatchingCustomer() { // Arrange — seed a known customer var seeded = _testRepo.Prepare_Can_Find_Async_By_Primary_Key(); // Act var found = await _customerRepository.FindAsync(seeded.Id); // Assert Assert.That(found, Is.Not.Null); Assert.That(found.FirstName, Is.EqualTo("Albus")); } } ``` `TestRepository` provides named `Prepare_*` methods that seed specific test scenarios using `TestDataActions` under the hood: | Method | Seeds | |---|---| | `Prepare_Can_Find_Async_By_Primary_Key()` | One customer with `FirstName = "Albus"` | | `Prepare_Can_Find_Single_Async_With_Expression()` | One customer with `ZipCode = "30062"` | | `Prepare_Can_Find_Async_With_Expression()` | Ten customers with `LastName = "Potter"` | | `Prepare_Can_Get_Count_Async_With_Expression()` | Ten customers with `LastName = "Dumbledore"` | | `Prepare_Can_Get_Any_Async_With_Expression()` | Ten customers with `City = "Hollywood"` | | `Prepare_Can_query_using_paging_with_specific_params()` | 100 customers with `FirstName = "Lisa"` | | `Prepare_Can_query_using_paging_with_specification()` | 100 customers with `FirstName = "Bart"` | | `Prepare_Can_query_using_predicate_builder()` | 100 customers with `FirstName = "Homer"` | | `Prepare_Can_Add_Async()` | Returns an unsaved customer (call AddAsync yourself) | | `Prepare_Can_Add_Graph_Async()` | Returns an unsaved customer with a nested Order collection | | `Prepare_Can_Update_Async()` | One persisted customer ready for update | | `Prepare_Can_Delete_Async()` | One persisted customer ready for deletion | | `Prepare_UnitOfWork_Can_Rollback()` | One persisted customer with `City = "Terabithia"` | ### Bulk Seed and Cleanup For scenarios that require seeding outside the named methods, call `PersistSeedData` directly: ```csharp var customers = Enumerable.Range(1, 50) .Select(_ => TestDataActions.CreateCustomerStub(c => c.State = "GA")) .ToList(); _testRepo.PersistSeedData(customers); // Later, in TearDown: _testRepo.CleanUpSeedData(); ``` `PersistSeedData` saves each entity immediately via `DbContext.SaveChanges()` and registers a delete action for each item. `CleanUpSeedData` runs those delete actions and calls `SaveChanges()` again, leaving the database in the state it was in before the test ran. To reset the database entirely, use `ResetDatabase()`: ```csharp _testRepo.ResetDatabase(); // Executes DELETE statements on OrderItems, Products, Orders, and Customers ``` ## Specification Pattern in Tests The `CustomerSearchSpec` in `RCommon.TestBase.Specifications` shows how to apply paged specifications in integration tests: ```csharp using RCommon.TestBase.Specifications; using RCommon.TestBase.Entities; // Search for customers whose first name starts with "Bart", ordered by first name ascending var spec = new CustomerSearchSpec( customerName: "Bart", orderByExpression: c => c.FirstName, orderByAscending: true, pageIndex: 1, pageSize: 25); var results = await _repository.FindAsync(spec); Assert.That(results.TotalCount, Is.EqualTo(100)); Assert.That(results.PageSize, Is.EqualTo(25)); Assert.That(results.Items.All(c => c.FirstName.StartsWith("Bart")), Is.True); ``` ## API Summary ### TestBootstrapper (RCommon.TestBase) | Member | Description | |---|---| | `InitializeBootstrapper(IServiceCollection)` | Loads `appsettings.json`, registers `IConfiguration` and Serilog logging | | `CreateMockHttpClient(HttpResponseMessage)` | Returns a `Mock` configured to return the given response | | `Configuration` | The `IConfigurationRoot` built from `appsettings.json` | | `ServiceProvider` | The built `ServiceProvider` (set by the subclass after calling `BuildServiceProvider()`) | | `Logger` | An `ILogger` instance available after DI is configured | ### TestFixture (RCommon.TestBase.XUnit) | Member | Description | |---|---| | `BuildConfiguration()` | Reads `appsettings.json` from the current directory; override to add other sources | | `ConfigureServices(IServiceCollection)` | Override to register services; always call `base.ConfigureServices` first | | `GetService()` | Resolves a required service; throws if not registered | | `GetOptionalService()` | Resolves an optional service; returns `null` if not registered | | `ServiceProvider` | The built `IServiceProvider` | | `Configuration` | The `IConfiguration` instance | | `Dispose()` | Disposes the `ServiceProvider`; called by xUnit after the test class | ### TestRepository (RCommon.TestBase.Data) | Member | Description | |---|---| | `PersistSeedData(IList)` | Saves a list of entities and registers delete actions for cleanup | | `CleanUpSeedData()` | Runs registered delete actions and calls `SaveChanges()` | | `ResetDatabase()` | Deletes all rows from `OrderItems`, `Products`, `Orders`, and `Customers` | | `Context` | The underlying `DbContext` | | `EntityDeleteActions` | The list of registered delete actions for cleanup | | `Prepare_Can_Find_Async_By_Primary_Key()` | Seeds a known customer and returns it | | `Prepare_Can_Add_Async()` | Returns an unsaved customer for add tests | | `Prepare_Can_Update_Async()` | Seeds a customer and returns it for update tests | | `Prepare_Can_Delete_Async()` | Seeds a customer and returns it for delete tests | ### TestDataActions (RCommon.TestBase) | Method | Description | |---|---| | `CreateCustomerStub()` | Generates a `Customer` with randomized fields | | `CreateCustomerStub(Action)` | Generates a `Customer` with post-generation customization | | `CreateOrderStub()` | Generates an `Order` with randomized dates | | `CreateOrderStub(Action)` | Generates an `Order` with post-generation customization | | `CreateOrderItemStub(Action)` | Generates an `OrderItem` with a customization | | `CreateProductStub()` | Generates a `Product` with randomized name and description | | `CreateSalesPersonStub()` | Generates a `SalesPerson` with randomized fields | | `CreateSalesPersonStub(Action)` | Generates a `SalesPerson` with post-generation customization | --- ## FluentValidation Source: https://rcommon.com/docs/validation/fluent-validation RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support. # FluentValidation ## Overview `RCommon.FluentValidation` integrates the FluentValidation library with RCommon's validation pipeline. It registers `FluentValidationProvider` as the `IValidationProvider` implementation and wires it into the CQRS command and query buses, so validation runs automatically before handlers execute. Your application code depends only on `IValidationService` (for manual validation) or on the CQRS pipeline behavior (for automatic validation). The FluentValidation-specific types stay confined to startup configuration and the validator classes themselves. ### How validation works 1. You define validators by extending `AbstractValidator` and declaring rules with FluentValidation's fluent API. 2. Validators are registered in the DI container during startup. 3. When a command or query is dispatched (with CQRS validation enabled), `FluentValidationProvider` resolves all `IValidator` instances for that type, runs them in parallel, and collects the results into a `ValidationOutcome`. 4. If there are failures and `throwOnFaults` is `true`, a `ValidationException` is thrown containing a list of `ValidationFault` objects — each carrying the property name, error message, and attempted value. You can also call `IValidationService.ValidateAsync` directly from application services when you need to validate outside the CQRS pipeline. ## Installation ## Configuration Call `WithValidation` inside your `AddRCommon()` block. Use the configuration action to register your validators. ### Scan an assembly for all validators ```csharp using RCommon; using RCommon.ApplicationServices; using RCommon.FluentValidation; builder.Services.AddRCommon() .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining(typeof(MyCommand)); }); ``` `AddValidatorsFromAssemblyContaining` scans the assembly that contains the given type and registers every `AbstractValidator` implementation it finds. ### Scan multiple assemblies ```csharp builder.Services.AddRCommon() .WithValidation(validation => { validation.AddValidatorsFromAssemblies(new[] { typeof(CreateOrderCommand).Assembly, typeof(ShipOrderCommand).Assembly }); }); ``` ### Register a single validator explicitly ```csharp builder.Services.AddRCommon() .WithValidation(validation => { validation.AddValidator(); }); ``` ### Enable automatic CQRS pipeline validation Call `UseWithCqrs` on the validation builder, inside the same `WithValidation(Action)` lambda used to register validators, to activate validation in the command bus, the query bus, or both: ```csharp builder.Services.AddRCommon() .WithValidation(validation => { validation.AddValidatorsFromAssemblyContaining(typeof(MyCommand)); // Enable auto-validation for commands only. validation.UseWithCqrs(opts => { opts.ValidateCommands = true; opts.ValidateQueries = false; }); }); ``` Both `ValidateCommands` and `ValidateQueries` default to `false`. `UseWithCqrs` is declared on `RCommon.ApplicationServices.ValidationBuilderExtensions` and extends `IValidationBuilder`, so it's available on `validation` inside the lambda without any additional `using`. :::warning Migrating from `WithValidation(Action)` An older overload — `.WithValidation(opts => { opts.ValidateCommands = true; })` passing `CqrsValidationOptions` directly to `WithValidation` — is `[Obsolete]`. It's still functional, but collided with `RCommon.ApplicationServices`'s own `WithValidation(Action)` overload in a way that made the zero-arg `.WithValidation()` call ambiguous (CS0121) whenever both packages were referenced. Replace it with `UseWithCqrs` inside the `Action` lambda as shown above — same effect, no ambiguity. ::: :::tip Modular composition `WithValidation` is cache-aware. Both the `RCommon.ApplicationServices` overload (`WithValidation(...)`) and the `RCommon.FluentValidation` extension share the same builder cache, so when multiple modules call `WithValidation(...)` the cached `FluentValidationBuilder` is reused and each module's `AddValidator`, `AddValidatorsFromAssemblyContaining(...)`, and `AddValidatorsFromAssemblies(...)` registrations accumulate on the same instance. Each module can scan its own assembly for validators without coordinating with other modules. `CqrsValidationOptions` is a shared singleton: the last module to call the options overload wins for `ValidateCommands` / `ValidateQueries`. See [Modular Composition](../core-concepts/modular-composition.mdx) for the full conflict matrix. ::: ## Usage ### Defining a validator ```csharp using FluentValidation; public class CreateOrderCommand { public string CustomerId { get; set; } = string.Empty; public IReadOnlyList Lines { get; set; } = []; } public class CreateOrderCommandValidator : AbstractValidator { public CreateOrderCommandValidator() { RuleFor(cmd => cmd.CustomerId) .NotEmpty() .WithMessage("Customer ID is required."); RuleFor(cmd => cmd.Lines) .NotEmpty() .WithMessage("An order must have at least one line."); } } ``` ### Validating manually via IValidationService ```csharp public class OrderApplicationService { private readonly IValidationService _validationService; private readonly IOrderRepository _repository; public OrderApplicationService( IValidationService validationService, IOrderRepository repository) { _validationService = validationService; _repository = repository; } public async Task CreateOrderAsync( CreateOrderCommand command, CancellationToken ct = default) { ValidationOutcome outcome = await _validationService.ValidateAsync(command, throwOnFaults: false, ct); if (outcome.Errors.Any()) { return outcome; } await _repository.AddAsync(Order.From(command), ct); return outcome; } } ``` ### Letting the CQRS pipeline validate automatically When `ValidateCommands` is `true`, the command bus validates the command before dispatching to the handler. If validation fails, a `ValidationException` is thrown before your handler is invoked — no explicit validation code is needed in the handler. ```csharp public class CreateOrderCommandHandler : ICommandHandler { private readonly IOrderRepository _repository; public CreateOrderCommandHandler(IOrderRepository repository) { _repository = repository; } // The pipeline has already validated the command by the time this runs. public async Task HandleAsync(CreateOrderCommand command, CancellationToken ct) { await _repository.AddAsync(Order.From(command), ct); } } ``` ### Handling ValidationException ```csharp try { await commandBus.SendAsync(new CreateOrderCommand()); } catch (ValidationException ex) { foreach (ValidationFault fault in ex.Faults) { Console.WriteLine($"{fault.PropertyName}: {fault.Message}"); } } ``` ## API Summary ### `IRCommonBuilder` extension | Method | Description | |--------|-------------| | `WithValidation()` | Registers the validation provider with default `CqrsValidationOptions`. Declared once, in `RCommon.ApplicationServices`. | | `WithValidation(Action)` | Registers the validation provider and applies the given configuration action against it. Declared in `RCommon.ApplicationServices`. | | `UseWithCqrs(Action)` | Extension on `IValidationBuilder` (call inside the `Action` lambda above) that configures CQRS pipeline validation. Declared in `RCommon.ApplicationServices`. | | `WithValidation(Action)` | **Obsolete.** Declared in `RCommon.FluentValidation`; use `UseWithCqrs` inside `WithValidation(Action)` instead (see the migration note above). | ### `IFluentValidationBuilder` extension methods | Method | Description | |--------|-------------| | `AddValidator()` | Registers a single validator for type `T` with a scoped lifetime. | | `AddValidatorsFromAssembly(assembly, lifetime?, filter?, includeInternal?)` | Scans the given assembly and registers all validators found. | | `AddValidatorsFromAssemblies(assemblies, lifetime?, filter?, includeInternal?)` | Scans multiple assemblies and registers all validators found. | | `AddValidatorsFromAssemblyContaining(type, lifetime?, filter?, includeInternal?)` | Scans the assembly containing the given type. | ### `CqrsValidationOptions` | Property | Default | Description | |----------|---------|-------------| | `ValidateCommands` | `false` | Automatically validate commands before dispatch. | | `ValidateQueries` | `false` | Automatically validate queries before dispatch. | ### `IValidationService` | Method | Description | |--------|-------------| | `ValidateAsync(target, throwOnFaults?, cancellationToken?)` | Validates `target` and returns a `ValidationOutcome`. Throws `ValidationException` when `throwOnFaults` is `true` and there are failures. | ### `ValidationOutcome` Returned by `IValidationService.ValidateAsync`. Contains `IReadOnlyList Errors`. ### `ValidationFault` | Property | Description | |----------|-------------| | `PropertyName` | The name of the property that failed validation. | | `Message` | The human-readable error message. | | `AttemptedValue` | The value that was rejected. |