Numbering implements Framework’s ISequenceNumberGenerator, selects rules from three tenant-scoped tables, assembles a string, and writes an allocation fact. It does not generate database primary keys and has no rule-management use case or administration UI, but it does expose a read-only governance query endpoint (see §12) that lets operators inspect rules and counter facts.
1. Current product surface
Beyond Infrastructure, the module now has Contracts and Application projects to expose the read-only governance query (see §12). Public ports and pure algorithms live in BitzOrcas.Application.Numbering; the API Host registers the implementation through AddBitzOrcasNumberingPlatform().
2. Input and output
// ① TableName + Field bind a rule; they are not permission to run dynamic SQL.var request = new SequenceNumberRequest( TableName: "DemoCase", Field: "SerialId", EntityValues: new Dictionary<string, object?> { ["OfficeId"] = "SH", ["CaseType"] = "Civil" }, IsEnabled: true, ParentSequenceNumber: null, BusinessSerialContext: null);
// ② Call the shared port; result contains string, counter, and prefix.Result<SequenceNumberResult> result = await sequenceNumbers.GenerateAsync(request, cancellationToken);The request has no TenantId. Tenant context is applied by the underlying generated IEntitySet<T> adapters.
3. Three owner tables
| Table | Responsibility |
|---|---|
SysSequenceNumberRule | condition, priority, separator, compensation for TableName/Field |
SysSequenceNumberRuleSetting | ordered segments under a RuleId |
SysSequenceNumberGenerateRecord | allocated full string, number, prefix, activation, and parent facts |
All inherit BizEntityBase and declare tenant and soft-delete metadata. They are rule/counter fact exceptions, not a template for restoring one entity/mapper per table.
4. Rule selection
The generator queries the same TableName/Field with IsActive && IsEnabled, orders candidates by Priority, and delegates to SequenceRuleSelector. Conditions support eq, ne, in, and contains against EntityValues.
An empty condition evaluates true inside the main loop, so the default must have the largest Priority. Otherwise it consumes rules that follow it.
5. Six segment types
| SectionType | Output |
|---|---|
| Constant | literal Value |
| Date | current time in a .NET format; optionally fixed April fiscal logic |
| FiscalYear | two-digit fiscal year |
| Branch | JSON map from FieldSource, falling back to Value/FD |
| BusinessField | EntityValues field, falling back to Value |
| SequenceNumber | number padded to Length, default six |
An unknown SectionType silently becomes Constant instead of returning a configuration error.
6. Shipped rules
Only two demo rules ship:
DemoCase.SerialId→CASE-{yy}-{seq6};DemoInvoice.SerialId→INV-{yyyy}-{seq6}.
Both rules and six settings use TenantId 1000001. They are sample baseline data, not rules that automatically cover arbitrary business tables.
7. Counters and prefixes
Prefix is every non-SequenceNumber output joined by Separator, for example CASE-26. The generator reads the maximum number for Tenant + TableName + Field + Prefix, then adds one.
A changed Date, FiscalYear, Branch, or BusinessField prefix naturally starts a separate counter. There is no separate daily/monthly/yearly reset-policy field.
8. Uniqueness boundary
The database unique index is TenantId + TableName + Field + SequenceNumberStr. Concurrent calls can read the same MAX; one insert wins and the other recognizes a database exception by message text and retries.
Recognition matches several English phrases and SQL Server numbers. It is a brittle provider compatibility layer; unmatched exceptions escape.
9. Business-table continuation
Optional IBusinessSerialContext addresses two migrations: continue from business MAX when the fact table is empty, and check whether a generated full value already exists.
The business owner implements the callback; Numbering never constructs dynamic table SQL. The source tree currently contains no production business module calling ISequenceNumberGenerator; consumption evidence is primarily tests.
10. Gap compensation
With IsCompensate=true, the first attempt searches the smallest inactive fact under the same prefix, updates its activation flag, and returns it. No skipped value simply falls through to a new allocation.
Compensation does not call business-table duplicate checking and has no atomic claim condition. Concurrent calls may select the same inactive row.
11. Guarantees and non-guarantees
| Current guarantee | Do not assume |
|---|---|
| three tenant/soft-delete metadata tables | management API, permission, or audit |
| conditional rules and six segment algorithms | Feature enforcement in GenerateAsync |
dual-ORM IEntitySet implementation | atomic increment or distributed lock |
| unique full-string index | StartValue is honored |
| optional business continuation/check | compensation is an atomic claim |
| three unique-conflict retries | every provider error is recognized |
12. Governance query endpoint
GET /api/numbering/rules (resource numbering/rules, Action Read) is an owner-local read-only governance report. Query parameters: Search (optional), Status (optional, values all/active/inactive/conditional, default all).
Returns NumberingGovernanceReport:
Rules: at most 200 rule summaries (overrun setsTruncated=true). EachNumberingRuleSummarycarriesRuleKey(shaped{Table}-{Field}-{RuleName}),Compensate,Conditional,Condition(controlled, with no runtime business values),Priority,ConditionScope, a human-readableTemplate(such as{date:yyyyMMdd},{fiscalYear},{branch},{field:...},{seq:n}), and per-segment detail (Segments, with aSubstitutionConfiguredflag; the substitution body itself is not returned);- global counts:
TotalRules,ActiveRules,ConditionalRules,CompensatingRules; - counter-fact totals:
GeneratedRecords,ActivatedRecords,HeldRecords(generated but not activated); GeneratedAt.
The read store NumberingGovernanceReadStore implements INumberingGovernanceReadStore and deliberately returns only capacity statistics and rule templates, not generated business numbers, parent numbers, or branch maps. It degrades to the fail-closed UnavailableNumberingGovernanceReadStore when not registered. Error codes: Numbering.Governance.StoreUnavailable (ServiceUnavailable), Numbering.Governance.InvalidQuery (Validation).
This endpoint is the same kind of owner-local read-only projection as the MasterData governance query: it answers which rules exist and how many numbers were generated, not whether a rule is correct or a sequence is gap-free.
13. Handbook map
- Conditional rules and segment DSL
- Counters, recovery, compensation, and concurrency
- Persistence, seeding, and integration
- Testing, operations, and commercial GA
14. Source inspection
# Generator, rule selection, and segment algorithms.rg -n "GenerateAsync|SelectRule|AssembleFull" src/Platform/Numbering src/Framework/BitzOrcas.Application/Numbering -g '*.cs'
# Governance query endpoint and read-only report.rg -n "GetNumberingGovernance|NumberingGovernanceReport|NumberingGovernanceReadStore" \ src/Platform/Numbering -g '*.cs'
# Current production consumption; expect registration but no business request construction.rg -n "ISequenceNumberGenerator|SequenceNumberRequest" src -g '*.cs' --glob '!**/bin/**' --glob '!**/obj/**'