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
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 block | Actual responsibility | Common misuse |
|---|---|---|
Result / Error | Expected failure and stable classification | Throwing for validation failure |
IAppClock | Current time and persistence conversion | DateTime.Now inside a handler |
ICurrentUser | Trusted authenticated identity snapshot | Trusting DTO TenantId |
ITenantContext | Read-only compatibility projection of current tenant | Storing a second mutable tenant state |
| Seed framework | Ordered, auditable, ORM-neutral seed execution | Unconditional 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.cssrc/Framework/BitzOrcas.Application/Abstractions/Users/src/Framework/BitzOrcas.Infrastructure/Seeders/