SaaS cross-repo published language and anti-corruption¶
Contract that governs how the five SaaS bounded-context repositories under C:\Git\ConnectSoft\ConnectSoft.Saas.*Template interoperate. This is the companion ADR to:
- SaaS Template Baseline Checklist
- SaaS platform — solution plan — ServiceModel standard and edge consumers
- Service Model — Layer 3 API boundary (SaaS section)
- SaaS Aggregate Root Assignment
- extended-template-base-submodule-guide
- saas-platform-ddd-blueprint
Products Catalog tenant partition¶
The Products Catalog template stores catalog rows with TenantId and applies NHibernate tenant filtering. Natural keys (product slug, feature key, etc.) are unique per tenant, not globally across all tenants in the database unless you deliberately use one canonical platform tenant for definitions. See ADR 0004 — Tenant-scoped catalog vs company SaaS DDD vocabulary in the Products Catalog template repo.
Non-negotiable rules¶
- No shared domain types across SaaS repos. Domain types live only in their owning repo (
<Context>.DomainModel/<Context>.DomainModel.Impl). - Consumers reference
*.ServiceModelNuGets only. Referencing*.DomainModel,*.EntityModel,*.PersistenceModel.*across repos is forbidden and is enforced in<Context>.ArchitectureTests. - Integration events are a first-class NuGet artifact, published from
<Context>.MessagingModel. Common envelope fields (every event): tenantId(string, required)aggregateId(string, required)aggregateVersion(long)schemaVersion(int, starts at 1)correlationId(guid)causationId(guid, optional)occurredOn(datetime UTC)- Additive versioning.
v1topics for the life of the envelope's backwards-compatible evolution. Breaking changes publish a new topic.v2alongside.v1(dual-publish + consumer opt-in migration). - Transactional delivery via MassTransit built-in outbox. No custom outbox tables in our migrations; outbox schema belongs to MassTransit (configured via BaseTemplate's
AddMassTransitOutboxextension in<Context>.ApplicationModel). - Consumers are idempotent. Cross-repo reactions use MassTransit saga state machines (
MassTransitStateMachine<TState>), not standaloneIConsumer<T>handlers. Saga behaviors delegate to idempotent domain processor methods; duplicate message replays are no-ops. Inbox dedupe uses(sourceContext, eventId)via Redis or consumer persistence. - Anti-corruption layer (ACL) for external providers. Any SaaS repo that consumes external provider semantics (payments, email/SMS, tax) isolates that integration under
<Context>.FlowModel.MassTransit/Adapters/*. External shapes never leak into<Context>.DomainModelor cross-repo events. - gRPC is the preferred high-throughput server-to-server surface. REST is the admin plane and external consumer surface. Both are stable NuGets (
<Context>.ServiceModel.RestApi,<Context>.ServiceModel.Grpc). - Orleans streams are intra-silo only. Cross-service integration always goes through MassTransit. Grains are tenant-scoped (grain key includes
tenantId).
Canonical topic plan¶
| Source repo | Topic | When |
|---|---|---|
| TenantsTemplate | tenants.domain.v1.tenant-created |
Tenant aggregate provisioned |
| TenantsTemplate | tenants.domain.v1.tenant-activated |
Status transition Pending -> Active |
| TenantsTemplate | tenants.domain.v1.tenant-suspended |
Billing / compliance suspension |
| TenantsTemplate | tenants.domain.v1.tenant-deleted |
Soft delete committed |
| TenantsTemplate | tenants.domain.v1.residency-changed |
TenantRegionResidency changed |
| ProductsCatalogTemplate | catalog.domain.v1.product-created |
New product registered |
| ProductsCatalogTemplate | catalog.domain.v1.product-updated |
Product meta updated |
| ProductsCatalogTemplate | catalog.domain.v1.product-retired |
Product lifecycle retired |
| ProductsCatalogTemplate | catalog.domain.v1.edition-added |
New edition within product |
| ProductsCatalogTemplate | catalog.domain.v1.edition-updated |
Edition metadata changed within product |
| ProductsCatalogTemplate | catalog.domain.v1.feature-activated |
Feature enabled in an edition |
| ProductsCatalogTemplate | catalog.domain.v1.feature-deactivated |
Feature disabled in an edition |
| ProductsCatalogTemplate | catalog.entitlements.v1.entitlements-changed |
Fan-out to Entitlements + Billing |
| EntitlementsTemplate | entitlements.v1.effective-entitlements-updated |
Per-tenant effective snapshot changed |
| EntitlementsTemplate | entitlements.v1.feature-overridden |
Tenant feature override applied |
| EntitlementsTemplate | entitlements.v1.assignment-changed |
Edition assignment flipped |
| BillingTemplate | billing.subscriptions.v1.subscription-created |
New subscription |
| BillingTemplate | billing.subscriptions.v1.subscription-upgraded |
Edition upgrade / downgrade committed |
| BillingTemplate | billing.subscriptions.v1.subscription-canceled |
Cancellation or expiration |
| BillingTemplate | billing.invoices.v1.invoice-issued |
Invoice projection finalized |
| BillingTemplate | billing.payments.v1.payment-captured |
Payment provider callback reconciled |
| BillingTemplate | billing.entitlements.v1.entitlements-sync-requested |
Request Entitlements re-materialize for a tenant |
| MeteringTemplate | metering.usage.v1.usage-recorded |
Raw usage event |
| MeteringTemplate | metering.counters.v1.counter-rolled |
Window boundary rolled |
| MeteringTemplate | metering.quota.v1.quota-threshold-crossed |
Soft warning (e.g. 80%) |
| MeteringTemplate | metering.quota.v1.quota-exceeded |
Hard ceiling hit |
flowchart LR
T[TenantsTemplate] -->|tenants.domain.v1.*| E[EntitlementsTemplate]
T -->|tenants.domain.v1.*| B[BillingTemplate]
T -->|tenants.domain.v1.*| M[MeteringTemplate]
P[ProductsCatalogTemplate] -->|catalog.entitlements.v1.entitlements-changed| E
P -->|catalog.domain.v1.*| B
E -->|entitlements.v1.effective-entitlements-updated| B
B -->|billing.entitlements.v1.entitlements-sync-requested| E
B -->|billing.subscriptions.v1.*| M
M -->|metering.quota.v1.*| B
M -->|metering.quota.v1.*| E
Sample seed IDs (E2E demo tenant)¶
Shared DefaultTenantId / seed tenant used across Billing, Metering, Tenants, Entitlements, and Products Catalog acceptance tests:
| Constant | Value | Defined in |
|---|---|---|
| TenantId | 7f4c2b9e-3d1a-4f8e-9c6b-5a0e183d42f1 |
SampleTenantSeed, SampleBillingSeed, SampleMeteringSeed, SampleEntitlementSeed |
| Catalog product (ConnectSoft Cloud Suite) | 01a1b2c3-d4e5-6789-a012-345678900001 |
SampleCatalogSeed / SampleBillingSeed |
| Enterprise edition | 02b2c3d4-e5f6-7890-b012-345678901004 |
SampleCatalogSeed / SampleBillingSeed |
| Sample subscription | 21f1f2a3-b4c5-d6e7-f890-123456780011 |
SampleBillingSeed |
| Sample usage meter | 20a0b1c2-d3e4-5678-f901-234567890abc |
SampleMeteringSeed |
Per-repo seed documentation: docs/examples/sample-*-database.md in each ConnectSoft.Saas.*Template repository.
Edge consumer package matrix¶
Who may reference SaaS ServiceModel NuGets vs auth packages at the platform edge. Browsers and MFEs never reference Application, DomainModel, or PersistenceModel from SaaS repos.
| Consumer | ConnectSoft.Saas.*.ServiceModel* |
Auth / OIDC (Authorization Server, JWT middleware, BFF session) | ConnectSoft.Saas.*.Application / *.DomainModel |
|---|---|---|---|
| API Gateway | Yes — routing, OpenAPI aggregation, downstream typed clients | Yes — token validation, scope checks; see API Gateway Template | No |
ConnectSoft.Blazor.Shell.Saas |
Yes — BFF calls to bounded contexts | Yes — OIDC login, cookie/BFF session; see Authorization Server Template | No |
| Blazor MFE (admin / self-service) | Yes — ServiceModel HTTP/gRPC clients only | Yes — via shell/host OIDC; MFE does not embed identity domain types | No |
| Platform admin tools | Yes — same as MFE edge rule | Yes — operator auth separate from SaaS domain | No |
| Peer SaaS microservice | Yes — see matrix below | Service auth as configured (mTLS, internal JWT) | No |
Auth packages at the edge are identity-plane artifacts (Authorization Server, ASP.NET Core authentication middleware, OIDC client libraries)—not substitutes for ConnectSoft.Saas.<Context>.ServiceModel. Business operations (catalog, billing, entitlements, …) always flow through ServiceModel contracts.
ServiceModel matrix (who consumes whose REST/gRPC NuGet)¶
| Consumer | TenantsSM | CatalogSM | EntitlementsSM | BillingSM | MeteringSM |
|---|---|---|---|---|---|
| TenantsTemplate | — | — | — | — | — |
| ProductsCatalogTemplate | yes | — | — | — | — |
| EntitlementsTemplate | yes | yes | — | yes | — |
| BillingTemplate | yes | yes | yes | — | yes |
| MeteringTemplate | yes | yes | — | yes | — |
| Gateway / Shell / MFEs | yes | yes | yes | yes | yes |
| Platform (admin) | yes | yes | yes | yes | yes |
"yes" means the consumer takes a dependency on that repo's published NuGets:
<Source>.ServiceModel(DTOs + typed clients)<Source>.ServiceModel.RestApi(controllers surface declarations when hosted by the consumer — e.g. Gateway OpenAPI aggregation)<Source>.ServiceModel.Grpc(code-first gRPC server adapter classes implementing the C#[ServiceContract]interfaces from<Source>.ServiceModel; ServiceModel.Grpc — no.proto)<Source>.MessagingModel(integration event DTOs)
Idempotency + inbox¶
- Inbox key:
sha256(sourceContext || eventId || schemaVersion). - Storage: Redis set (TTL 7 days) by default. Repos that persist domain state in SQL may move the inbox into a dedicated
_inboxtable (no outbox — only inbox — and it lives in the repo's own migration, since it is internal state). - Duplicate handling: consumers return "ack" but skip side effects; metric
masstransit_inbox_duplicates_totalrecords the event for observability.
Breaking-change procedure (topic bump)¶
- Publish new topic
.v2alongside.v1(dual publish). - Update consumers at their cadence.
- Announce deprecation of
.v1with a hard date. - Remove
.v1publisher (and topic) once consumer telemetry shows zero traffic for the deprecation window.
Enforcement¶
<Context>.ArchitectureTestsfails when a cross-repo reference violates rule #2.azure-pipelines-messaging-model.ymlruns ajson-schemasmoke test that every event type in<Context>.MessagingModelconforms to the common envelope.azure-pipelines-service-model.ymlre-runs architecture tests (gRPCArchitectureTestsprove zero.protofiles in*.ServiceModel.Grpcand enforce[ServiceContract]/[OperationContract]on all*.ServiceModelinterfaces) + OpenAPI schema validation (REST) to guarantee additive-only diffs within av1major.
Changelog¶
- 2026-05-27 — Added edge consumer package matrix (Gateway / Shell / MFE vs ServiceModel vs auth); anchor IDs for cross-links.
- 2026-05-26 — Added sample seed IDs table; documented saga-based cross-repo consumption; corrected template repo paths.
- 2026-05-19 — Documented tenant-partitioned catalog keys for Products Catalog (link to ADR 0004).
- 2026-05-18 — Added
catalog.domain.v1.edition-updatedto the canonical topic plan (Products Catalog). - 2026-04-28 — Initial version aligned with the five SaaS template repos.