Skip to content
bitzorcas
中EN

Concept

Catalog Module Architecture: Multi-Tenant Offering Management

Explore the BitzOrcas.Modern Catalog Subsystem. Learn OfferingAggregate roots, dynamic SKU variants, tiered pricing, and immutable version publishing.

Last updated

In enterprise B2B SaaS and commerce platforms, product and service catalogs are far more than simple online store listings:

  • Configurable Offerings & Add-Ons: SaaS subscriptions contain base user seats, extra storage packages, and dynamic AI token addons;
  • Tiered & Custom Enterprise Pricing: Different tenant organizations negotiate private contract pricing and custom discount matrices;
  • Immutable Version Snapshotting: When orders are placed, the historical offering specification must be permanently frozen to prevent retroactive pricing tampering!

BitzOrcas.Modern Catalog Module implements “Domain Aggregates + Version Snapshots + Dynamic Pricing”:

Catalog Lifecycle and Publishing Topology

1. Product Operator (Manage Offering Specs)

2. CreateOfferingCommand / PublishVersionCommand

3. Vertical Slice Handler

4. OfferingAggregate (Draft -> Approved -> Active)

5. OfferingVersionSnapshot (Freezes Immutable Version)

6. IAggregateRepository

7. CAP Domain Events (catalog.offering_published)

8. Search Consumer (Async Sync to ElasticSearch / Vector Indexes)


Step 1: Core Domain Entity OfferingAggregate

The offering entity adopts the Unified Aggregate pattern, mapping directly to CatOffering:

OfferingAggregate.cs: Service Offerings Table
using System;
using System.ComponentModel;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Catalog.Domain;
public static class CatalogErrors
{
public static readonly Error Archived =
Error.Conflict("Catalog.Archived", "Archived offering cannot publish new versions.");
}
[BitzTable("CatOffering", IsTenant = true, IsSoftDelete = true, Description = "Service Offerings Table")]
[BitzIndex("IX_CatOffering_Code", nameof(Code), nameof(TenantId), IsUnique = true)]
public sealed class OfferingAggregate : TenantAggregateRoot<string>
{
[BitzColumn(Length = 64, IsRequired = true)]
public string Code { get; private set; } = string.Empty;
[BitzColumn(Length = 120, IsRequired = true)]
public string Name { get; private set; } = string.Empty;
[BitzColumn(Length = 2000)]
public string Description { get; private set; } = string.Empty;
// Active version identifier
[BitzColumn(IsRequired = true)]
public int ActiveVersion { get; private set; } = 1;
// Base offering price
[BitzColumn(Precision = 18, Scale = 4, IsRequired = true)]
public decimal BasePrice { get; private set; }
[BitzColumn(IsRequired = true)]
public OfferingStatus Status { get; private set; } = OfferingStatus.Draft;
[Obsolete("For ORM materialization only. Use Create.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public OfferingAggregate()
: base("0")
{
}
// Domain method publishing new offering version
public Result PublishNewVersion(decimal newPrice, string operatorId)
{
// 1. Invariant check: archived offerings cannot be republished
if (Status == OfferingStatus.Archived)
{
return Result.Failure(CatalogErrors.Archived);
}
// 2. State transition
BasePrice = newPrice;
ActiveVersion++;
Status = OfferingStatus.Active;
// 3. Emit published domain event
AddDomainEvent(new OfferingPublishedDomainEvent(Id, Code, ActiveVersion, BasePrice, operatorId));
return Result.Success();
}
}

Step 2: Read Query Slice with QueryShape

List queries use IDeclareQueryShape for zero-boilerplate paginated projections:

ListOfferingsQuery.cs: Offerings Query Slice
using BitzOrcas.Application.Abstractions.Queries;
using BitzOrcas.Domain.Results;
using BitzOrcas.Endpoint.Attributes;
using Mediator;
namespace BitzOrcas.Catalog.Application.Queries;
// 1. Declarative query endpoint: compile-time Minimal API mapping and OpenAPI annotations
[GenerateEndpoint(HttpRoute.Query, "/api/catalog/offerings", Tag = "Catalog")]
// 2. Strongly typed query contract with standardized pagination parameters
public sealed record ListOfferingsQuery(
string? Keyword,
OfferingStatus? Status,
int PageIndex = 1,
int PageSize = 20) : IQuery<Result<PagedList<OfferingDto>>>;

Summary

The Catalog Module guarantees enterprise robustness:

  • Version Snapshotting: Preserves contract fidelity across order lifecycles;
  • Dual ORM Mapping: EF Core handles transactional writes while SqlSugar accelerates high-throughput reads;
  • Event-Driven Search: Real-time asynchronous index synchronization.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%