The rule DSL decides which business field receives a number, under what condition, and how its string is assembled. It is small and testable, but many configuration errors currently degrade silently.
1. Loading candidates
The query filters TableName, Field, IsActive, and IsEnabled. Tenant and soft deletion are applied by generated persistence adapters. Lower Priority runs first.
The selector has final “default fallback” code, but an empty condition already returns true in the loop. A default rule must be last.
2. Actual RuleId
Runtime RuleId is:
{TableName}-{Field}-{RuleName}Setting rows must use exactly this value. A model comment still mentions only TableName+Field and is stale; do not build assets from it.
3. JSON conditions
{ "field": "CaseType", "op": "Eq", "value": "Civil"}The enum uses JsonStringEnumConverter. Field lookup follows the EntityValues dictionary comparer, usually case-sensitive by default, while value comparisons ignore case.
4. Four operators
| Operator | Behavior |
|---|---|
| Eq | actual equals value, case-insensitive |
| Ne | actual differs from value, case-insensitive |
| In | split value by comma, trim, and match |
| Contains | actual contains value, case-insensitive |
A missing property returns false. A null property becomes an empty string, so Eq "" can match null.
5. Condition failure semantics
Invalid JSON returns false without a log. A deserialized null unexpectedly returns true. Blank JSON also returns true. ConditionScope is stored and seeded but unused by selection or caching.
public static class NumberingErrors{ public static readonly Error ConditionInvalid = Error.Validation("Numbering.Condition.Invalid", "Invalid condition");}
// ① Deserialize and validate fields and operators before production storage.var condition = JsonSerializer.Deserialize<SequenceRuleCondition>(json);if (condition is null || string.IsNullOrWhiteSpace(condition.Field)) return Result.Failure(NumberingErrors.ConditionInvalid);
// ② Exercise the rule against representative business snapshots.var matched = condition.IsSatisfiedBy(sampleEntityValues);
// ③ The empty default condition must carry the greatest Priority.EnsureDefaultRuleIsLast(rules);This is a desired publication validator; no current management entry point runs it.
6. Constant
Constant returns Value unchanged. An empty value becomes an empty segment, while string.Join can still leave separators. An unknown SectionType also parses as Constant, converting a spelling mistake into plausible output.
7. Date
Date defaults to yy, explicitly supports yy, yyyy, yyyy-MM, and yyyyMM, and passes other values to DateTime.ToString(format, InvariantCulture).
An invalid format can throw FormatException. There is no time-zone setting; behavior follows IAppClock.Now DateTime semantics.
8. FiscalYear
Fiscal years start on April 1: April–December uses the current two-digit year and January–March the previous one. A Date segment with IsFiscalYear=true also returns two digits and ignores DateFormat.
Source technical debt calls for configurable start month. Arbitrary corporate fiscal calendars are not supported today.
9. Branch
Branch reads a key from FieldSource and maps it through SubstitutionValue JSON. Missing FieldSource, invalid JSON, a missing key, or an unmapped value falls back to Value; empty Value becomes FD.
Every failure is silent and can collapse real branches into one counter prefix.
{ "SH": "SHA", "BJ": "BJA", "GZ": "GZA"}10. BusinessField
BusinessField reads FieldSource and calls ToString(). A missing field falls back to Value; a present null emits an empty string. There is no format, length, character, or sensitive-data validation.
Do not embed customer names, identity numbers, or other sensitive values in business numbers.
11. SequenceNumber
SequenceNumber formats with D{Length}; Length <= 0 defaults to six. StartValue exists in model, seed, and segment mapping, but GetNextSequenceNumberAsync never reads it, so an empty fact partition starts at one.
Multiple SequenceNumber segments repeat the same counter. There is no “exactly one counter segment” validator.
12. Separator and prefix
Assembler walks segments by Sort. Non-number outputs enter both prefix and full parts; number output enters only full. Each list is joined by Separator.
// ① A fixed clock makes the Date segment deterministic.var clock = new FakeClock(new DateTime(2026, 7, 15));
// ② CASE + yy + six digits, separated by hyphens.var (full, prefix) = SequenceSegmentAssembler.AssembleFull( segments, new Dictionary<string, object?>(), clock, sequenceNumber: 1, separator: "-");
// ③ full = CASE-26-000001; prefix = CASE-26.13. Publication gates
Reject duplicate/negative Sort, zero or multiple SequenceNumber segments, unknown types, invalid date formats, invalid Branch JSON, blank field names, a non-last default rule, ambiguous equal priorities, and potential prefix/full values over 128 characters.
Neither the CSV reader nor runtime currently enforces this complete gate.
14. Inspection commands
# Complete pure condition and segment algorithms.sed -n '1,260p' src/Framework/BitzOrcas.Application/Numbering/SequenceRuleSelector.cssed -n '1,300p' src/Framework/BitzOrcas.Application/Numbering/SequenceSegmentAssembler.cs
# StartValue and ConditionScope are carried but have no generation behavior.rg -n "StartValue|ConditionScope" src/Platform/Numbering src/Framework/BitzOrcas.Application/Numbering -g '*.cs'