Skip to content
bitzorcas
中EN

Reference

Shared Foundation Building Block

Source-verified ownership rules and reference for Result, errors, time, caller, tenancy, and seed infrastructure.

Last updated

Shared exists to provide a small set of stable, ownerless foundation semantics—not merely to reduce duplicate lines. Moving business DTOs, entities, repositories, or generic helpers into Shared turns explicit module dependencies into global hidden coupling.

Ownership decision

yesnoyesnonoyes

Candidate shared type

Contains business language or policy?

Keep in owner module Contracts

Hides IO, time, tenant, or global state?

Use an explicit port composed by Host

Stable and used long-term across modules?

Keep local to the module

Place in a Framework building block

Two current consumers are not sufficient justification. A type whose semantics may evolve with one business module belongs to that owner and is consumed through its Contracts assembly. A true foundation abstraction must also remain dependency-light and never reference Platform or Modules.

Building blockActual responsibilityCommon misuse
Result / ErrorExpected failure and stable classificationThrowing for validation failure
IAppClockCurrent time and persistence conversionDateTime.Now inside a handler
ICurrentUserTrusted authenticated identity snapshotTrusting DTO TenantId
ITenantContextRead-only compatibility projection of current tenantStoring a second mutable tenant state
Seed frameworkOrdered, auditable, ORM-neutral seed executionUnconditional demo users at startup

Result and Error

Domain and Application express expected failure with Result. Error.Code is a stable diagnostic and i18n key, conventionally {Module}.{Scenario}.{Reason}. Description is developer diagnostics and is not end-user copy. ErrorType controls Web-layer HTTP classification.

public static class BillingErrors
{
public static readonly Error AmountInvalid =
Error.Validation("Billing.Refund.AmountInvalid", "Refund amount must be positive.");
public static readonly Error ExceedsPaidAmount =
Error.Failure("Billing.Refund.ExceedsPaidAmount", "Refund cannot exceed the paid amount.");
}
public static Result<Money> CreateRefund(decimal amount, decimal paidAmount)
{
if (amount <= 0)
{
// The stable code joins API, client, and audit; description is not translated UI copy.
return Result.Failure<Money>(BillingErrors.AmountInvalid);
}
if (amount > paidAmount)
{
// An expected rule failure is a Result, not a business exception.
return Result.Failure<Money>(BillingErrors.ExceedsPaidAmount);
}
return Result.Success(new Money(amount, "CNY"));
}

Validation, NotFound, Conflict, Forbidden, Unauthorized, Failure, and Unexpected carry different semantics. Do not select one merely to force an HTTP status. Exceptions remain appropriate for programming defects, corrupt invariants, and unrecoverable infrastructure faults; the global handler closes that boundary.

Time, caller, and tenant

Domain or Application logic that needs “now” injects IAppClock. UtcNow represents an absolute timestamp, Instant supports NodaTime scenarios, and Now follows shared ClockOptions.Kind. Repositories use the symmetric ToStorage / FromStorage boundary. Business code does not infer database time zones.

ICurrentUser.User is a CurrentUser built by authentication infrastructure. Authorization and data scope use EffectiveUserId, while audit uses ActorUserId; they differ during delegation. Tenant identity comes from trusted context and is never overwritten from a request DTO.

public static class ApprovalErrors
{
public static readonly Error CallerRequired =
Error.Unauthorized("Approval.Caller.Required", "An authenticated caller is required.");
}
public sealed class ApproveHandler(ICurrentUser currentUser, IAppClock clock)
{
public Result<Approval> Handle(ApproveCommand command)
{
var caller = currentUser.User;
if (!caller.IsAuthenticated || caller.EffectiveUserId is null)
{
// Missing identity is typed failure; do not read HttpContext or headers here.
return Result.Failure<Approval>(ApprovalErrors.CallerRequired);
}
// Effective user owns the action; actor is separately retained for audit.
return Approval.Create(
command.SubjectId,
caller.EffectiveUserId.Value,
caller.ActorUserId,
clock.UtcNow);
}
}

ITenantContext is only a read-only compatibility facade over ICurrentTenantAccessor.Current, exposing TenantId and optional OfficeId. New mutable scopes use ICurrentTenantAccessor.BeginScope, preventing stale async-flow state and conflicting copies of the current tenant.

Seed framework

ISeedRunner executes serially by Order and then SeedId. Duplicate identifiers, duplicate orders, and missing dependencies fail loudly. Demo steps are skipped in Production and Staging. Cancellation propagates; normal exceptions enter SeedRunReport, and StopOnError decides whether later steps run.

public sealed class CountrySeedStep(ICountrySeedStore store) : ISeedStep
{
public int Order => 120;
public string SeedId => "sys_country";
public int Version => 2;
public string[] DependsOn => [];
public async Task ExecuteAsync(string environment, CancellationToken ct)
{
// Upsert by stable business code; generated database IDs are not execution markers.
await store.UpsertAsync("CN", "China", ct);
await store.UpsertAsync("SG", "Singapore", ct);
// Repeated execution must converge rather than create duplicates.
}
}

BitzOrcas:Seed configures Enabled, AutoRunOnStartup, StopOnError, OverrideRootPath, CsvDelimiter, and SkipSeedIds. Startup execution defaults off. Production seeds contain no fixed password, demo account, or non-rotatable token. Disk CSV override is operational power, so release automation must validate its source and integrity.

Value objects, masking, and audit ports

Value-object format and invariants for email, money, and identifiers belong in Domain and are created through Result. Cross-module masking uses the shared masker instead of copied regexes. Audit is an Application port composed by Host, keeping Domain independent from HTTP, logging, and database implementations.

Shared is not a bucket for every cross-cutting behavior. Cache, lock, eventing, and audit have their own behavior, configuration, and failure policy and remain separate building blocks.

Testing and evolution gate

Foundation changes have a large blast radius. Protect them with public-API tests, dependency-direction tests, and cross-module contract tests. Before admitting a type, identify its compatibility owner, business vocabulary, hidden external state, and whether a narrower owner contract is sufficient.

Tests cover Result invariants, delegated Actor/Effective identity, symmetric clock conversion, seed duplicate/dependency diagnostics, and Demo environment gating. For a breaking change, migrate consumers first and remove the old abstraction only after the global sweep is clean.

Continue with Core, Data Masking, and Persistence.

Source map

  • src/Framework/BitzOrcas.Domain/Results/
  • src/Framework/BitzOrcas.Domain/Abstractions/IAppClock.cs
  • src/Framework/BitzOrcas.Application/Abstractions/Users/
  • src/Framework/BitzOrcas.Infrastructure/Seeders/

100%

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