Skip to content
bitzorcas
中EN

Guide

Sandbox Golden Sample Architecture Breakdown

In-depth architecture breakdown of src/Modules/Sandbox: three-tier physical boundaries, CQRS vertical slices, adapter-neutral aggregate roots, and narrow read stores.

Last updated

Sandbox Golden Sample Architecture Breakdown

Within the BitzOrcas.Modern architectural system, src/Modules/Sandbox serves as the official Golden Use Case business module. It has zero external business dependencies and showcases a standalone domain module strictly adhering to the ADR 0203 physical isolation guidelines under rigorous production standards.

When implementing new business modules under src/Modules/ (such as Litigation or Contracts), Sandbox is the sole officially recommended golden reference pattern.


1. Three-Tier Physical Engineering Topology

The Sandbox module comprises three physical C# project files (.csproj), enforcing strict unidirectional dependencies and layer protection:

Framework Kernel (src/Framework)src/Modules/Sandbox/ (Domain Module Boundary)Host Layer (src/Hosts/BitzOrcas.Api)Wired by DIWired by DIDepends onImplements PortPrimitives

BitzOrcas.Api

BitzOrcas.Sandbox.Contracts
(Contract Layer: Aggregates, DTOs, Error Catalog, Ports)

BitzOrcas.Sandbox.Application
(Application Layer: CQRS Handlers, Validation Rules)

BitzOrcas.Sandbox.Infrastructure
(Infrastructure Layer: Read Stores, QueryShape Adapters)

BitzOrcas.Domain / Application / Infrastructure

Layered Boundaries and Invariants

Project DirectoryAllowed ContentsExplicitly Forbidden Contents
BitzOrcas.Sandbox.ContractsDomain aggregate root (Domain/Note.cs), external DTOs (NoteDto.cs), strongly typed read ports (INoteReadStore.cs), module error catalog (SandboxErrors.cs), permission constants (SandboxPermissions.cs)Specific ORM namespaces, database packages, HTTP context references, or reverse dependencies on Application or Infrastructure.
BitzOrcas.Sandbox.ApplicationCQRS commands and handlers (Commands/Notes/*), queries and handlers (Queries/Notes/*), FluentValidation rules, assembly marker (SandboxApplicationAssembly.cs)Inline raw SQL queries, cross-module physical repositories; write operations must depend only on ORM-neutral narrow command ports.
BitzOrcas.Sandbox.InfrastructureImplementation of contract ports (NoteReadStore.cs), IQueryShapeExecutorFactory pushdown projections, external client integrationsPrivate business rules bypassing Contracts; all adapters must be registered via typed attributes.

2. Domain Aggregate Root & Metadata Isolation: Note.cs

In legacy enterprise codebases, developers often decorate domain entities with ORM-specific attributes (such as [SugarTable("T_xxx")] or [Table]), hardcode magic prefixes like T_, and expose public set accessors, degrading domain models into anemic data holders.

BitzOrcas.Modern enforces adapter-neutral metadata rules:

  1. Unified Aggregate Root Base: Inherits TenantAggregateRoot<string>, standardizing tenant keys and entity IDs on distributed snowflake string values, with automatic tenancy filtering, auditing, and soft deletion.
  2. Standardized Table Naming: Uses adapter-neutral compile-time metadata [BitzTable("SandboxNote", ...)] from BitzOrcas.Persistence.Metadata. The table name is cleanly declared as SandboxNote without arbitrary T_ prefixes.
  3. Encapsulated State: Business properties use private set, preventing external code from mutating fields outside aggregate domain boundaries.
  4. Self-Validating Factory Method: Invariants are evaluated inside the Note.Create static factory, returning a typed Result<Note>.
  5. ORM Bypass Guard: The parameterless constructor is sealed with [Obsolete("For ORM materialization only. Use Create.", error: true)] and [EditorBrowsable(EditorBrowsableState.Never)], causing compile-time errors if business code attempts direct instantiation.
using System.ComponentModel;
using BitzOrcas.Domain.Contracts;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Domain.Tenancy;
using BitzOrcas.Persistence.Metadata;
using BitzOrcas.Sandbox.Contracts;
namespace BitzOrcas.Sandbox.Domain;
/// <summary>
/// Note owner-private aggregate root
/// </summary>
/// <remarks>
/// Demonstrates aggregate invariants and cross-cutting contracts (ITenantEntity/ISoftDelete/IAuditableEntity/IConcurrencyTracked).
/// Persistent IDs and TenantId both use string; auditing, soft-delete, and concurrency fields are provided by the tenant aggregate base.
/// This class declares adapter-neutral compile-time metadata; the persistence layer materializes this aggregate directly.
/// </remarks>
[BitzTable("SandboxNote", IsTenant = true, IsSoftDelete = true, Description = "Sandbox Note")]
public sealed class Note : TenantAggregateRoot<string>
{
/// <summary>
/// Maximum allowed title character length
/// </summary>
public const int TitleMaxLength = 200;
/// <summary>
/// Note title
/// </summary>
[BitzColumn(ColumnName = "Name", Length = TitleMaxLength, IsRequired = true)]
public string Title { get; private set; }
/// <summary>
/// Parameterless constructor reserved exclusively for ORM materialization
/// </summary>
/// <remarks>
/// Establishes valid placeholder state for persistence hydration. ORM engines require a public parameterless constructor;
/// ObsoleteAttribute with error contract blocks business code at compile time,
/// while EditorBrowsableAttribute hides it from IDE completion lists.
/// </remarks>
[Obsolete("For ORM materialization only. Use Create.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public Note()
: base("0")
{
Title = "Materialized";
}
/// <summary>
/// Private constructor invoked by the factory method
/// </summary>
private Note(string id, string title, string tenantId)
: base(id)
{
Title = title;
TenantId = tenantId;
}
/// <summary>
/// Factory method to construct and validate a Note aggregate
/// </summary>
/// <param name="title">Raw input title.</param>
/// <param name="tenantId">Trusted tenant identifier from security context.</param>
/// <returns>Success with aggregate instance; failure with typed domain error.</returns>
public static Result<Note> Create(string? title, string? tenantId)
{
var normalizedTitle = title?.Trim();
if (string.IsNullOrWhiteSpace(normalizedTitle) || normalizedTitle.Length > TitleMaxLength)
{
return Result<Note>.Failure(SandboxErrors.NoteTitleInvalid);
}
if (string.IsNullOrWhiteSpace(tenantId) || !TenancyDefaults.IsValid(tenantId))
{
return Result<Note>.Failure(SandboxErrors.NoteTenantInvalid);
}
var note = new Note("0", normalizedTitle, tenantId);
return Result<Note>.Success(note);
}
}

3. Strongly Typed Error Catalog: SandboxErrors.cs

BitzOrcas forbids throwing unstructured business exceptions or relying on magic strings. Operational failures are declared statically within the contract layer, explicitly typed as Validation, NotFound, Unauthorized, or Failure:

using BitzOrcas.Domain.Results;
namespace BitzOrcas.Sandbox.Contracts;
/// <summary>
/// Stable strongly typed error catalog for the Sandbox module
/// </summary>
public static class SandboxErrors
{
/// <summary>
/// Note title invalid (empty or exceeds 200 characters)
/// </summary>
public static readonly Error NoteTitleInvalid = Error.Validation("Sandbox.Note.TitleInvalid");
/// <summary>
/// Note tenant identifier invalid
/// </summary>
public static readonly Error NoteTenantInvalid = Error.Validation("Sandbox.Note.TenantInvalid");
/// <summary>
/// Current execution context missing trusted tenant information
/// </summary>
public static readonly Error TenantRequired = Error.Unauthorized("Sandbox.Tenant.Required");
/// <summary>
/// Requested Note does not exist
/// </summary>
public static readonly Error NoteNotFound = Error.NotFound("Sandbox.Note.NotFound");
/// <summary>
/// Page index out of valid range (must start from 1)
/// </summary>
public static readonly Error NoteInvalidPage = Error.Validation("Sandbox.Note.InvalidPage");
/// <summary>
/// Page size out of valid range
/// </summary>
public static readonly Error NoteInvalidPageSize = Error.Validation("Sandbox.Note.InvalidPageSize");
/// <summary>
/// Search query string exceeds length quota
/// </summary>
public static readonly Error NoteSearchTextInvalid = Error.Validation("Sandbox.Note.SearchTextInvalid");
/// <summary>
/// Query execution failed on underlying persistence store
/// </summary>
public static readonly Error NoteQueryFailed = Error.Failure("Sandbox.Note.QueryFailed");
}

4. CQRS Write Path Single-File Vertical Slice: CreateNoteCommand.cs

The write path strictly follows Single-File Vertical Slice Architecture: command contract, HTTP route generator attribute, MCP tool registration, authorization descriptors, and execution handler are unified in one compilation unit.

The handler depends only on the ORM-neutral narrow repository port ICommandRepository<Note, string>:

using BitzOrcas.Application.Abstractions.Tenancy;
using BitzOrcas.Application.Authorization;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Results;
using BitzOrcas.Endpoint.Attributes;
using BitzOrcas.Mcp.Attributes;
using BitzOrcas.Sandbox.Contracts;
using BitzOrcas.Sandbox.Domain;
using Mediator;
namespace BitzOrcas.Sandbox.Application.Commands.Notes;
/// <summary>
/// Create Note command contract
/// </summary>
[GenerateEndpoint(HttpRoute.Post, "/api/notes/", Tag = "Notes")]
[GenerateMcpTool("CreateNote", "Create a new note with validated title")]
public sealed record CreateNoteCommand(string Title) : ICommand<Result<string>>, IAuthorizedRequest
{
/// <summary>
/// Protected resource: sandbox module note resource
/// </summary>
public ResourceDescriptor Resource { get; } = new(
SandboxPermissions.Module,
SandboxPermissions.NoteResource);
/// <summary>
/// Authorization action: Create
/// </summary>
public AuthorizationAction Action { get; } = AuthorizationAction.Create;
}
/// <summary>
/// Application orchestration handler for creating Note instances
/// </summary>
public sealed class CreateNoteCommandHandler(
ICommandRepository<Note, string> repository,
ICurrentTenant currentTenant) : ICommandHandler<CreateNoteCommand, Result<string>>
{
/// <summary>
/// Validates invariants and persists the Note aggregate in current transaction scope
/// </summary>
public async ValueTask<Result<string>> Handle(
CreateNoteCommand request,
CancellationToken cancellationToken)
{
var tenant = currentTenant.Tenant;
if (!tenant.IsAvailable)
{
return Result<string>.Failure(SandboxErrors.TenantRequired);
}
// Delegate invariant verification to domain aggregate factory
var noteResult = Note.Create(request.Title, tenant.EffectiveTenantId);
if (noteResult.IsFailure)
{
return Result<string>.Failure(noteResult.Error);
}
var note = noteResult.GetValueOrThrow();
var saveResult = await repository.SaveAsync(note, cancellationToken);
return saveResult.IsFailure
? Result<string>.Failure(saveResult.Error)
: Result<string>.Success(note.Id);
}
}

5. CQRS Read Path Narrow Port & Persistence Adapter

To prevent repository interface bloat and avoid leaking raw ORM entities or IQueryable to API consumers, BitzOrcas enforces narrow read ports with pushdown execution:

  1. Contracts Exposes Narrow Read Ports: INoteReadStore returns only immutable NoteDto and PagedResult<NoteDto>.
  2. Infrastructure Carries Concrete Implementation: Marked with [RegisterPersistenceAdapter<INoteReadStore>] for automatic DI registration.
  3. QueryShape Pushdown: List queries leverage IQueryShapeExecutorFactory to push filtering and pagination down to the database engine with automatic tenant boundary safety.

Contract Narrow Port Definition (INoteReadStore.cs)

using BitzOrcas.Domain.Abstractions.Queries;
using BitzOrcas.Domain.Results;
namespace BitzOrcas.Sandbox.Contracts;
/// <summary>
/// Note narrow read-model store port
/// </summary>
public interface INoteReadStore
{
/// <summary>
/// Reads Note read model by identifier
/// </summary>
Task<Result<NoteDto>> GetByIdAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Searches Note read models by criteria for current tenant
/// </summary>
Task<Result<PagedResult<NoteDto>>> SearchAsync(
NoteListFields fields,
PaginationParams paging,
SortRequest? sort,
CancellationToken cancellationToken = default);
}

Infrastructure Persistence Adapter (NoteReadStore.cs)

using BitzOrcas.Application.Abstractions.Tenancy;
using BitzOrcas.DI.Attributes;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Abstractions.Queries;
using BitzOrcas.Domain.Results;
using BitzOrcas.Infrastructure.Queries;
using BitzOrcas.Sandbox.Contracts;
using BitzOrcas.Sandbox.Domain;
using Microsoft.Extensions.Logging;
namespace BitzOrcas.Sandbox.Infrastructure;
/// <summary>
/// Persistence adapter implementing Note read-model store
/// </summary>
[RegisterPersistenceAdapter<INoteReadStore>]
public sealed class NoteReadStore(
ICommandRepository<Note, string> notes,
IQueryShapeExecutorFactory executorFactory,
ICurrentTenant currentTenant,
ILogger<NoteReadStore> logger) : INoteReadStore
{
public async Task<Result<NoteDto>> GetByIdAsync(string id, CancellationToken cancellationToken = default)
{
var noteResult = await notes.FindAsync(id, cancellationToken).ConfigureAwait(false);
if (noteResult.IsFailure)
{
return noteResult.Error.Type == ErrorType.NotFound
? Result.Failure<NoteDto>(SandboxErrors.NoteNotFound)
: Result.Failure<NoteDto>(noteResult.Error);
}
var note = noteResult.GetValueOrThrow();
return Result.Success(new NoteDto(note.Id, note.Title));
}
public async Task<Result<PagedResult<NoteDto>>> SearchAsync(
NoteListFields fields,
PaginationParams paging,
SortRequest? sort,
CancellationToken cancellationToken = default)
{
var tenant = currentTenant.Tenant;
if (!tenant.IsAvailable)
{
return Result.Failure<PagedResult<NoteDto>>(SandboxErrors.TenantRequired);
}
try
{
var input = NoteListInput.From(fields, paging);
return await input.ExecutePageAsync(
executorFactory,
static row => new NoteDto(row.NoteId, row.Title),
sorts: sort.WithTieBreaker(nameof(NoteListInput.NoteId)),
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception exception) when (exception is not OutOfMemoryException)
{
logger.LogError(
"Sandbox Note query failed with {ExceptionType}.",
exception.GetType().Name);
return Result.Failure<PagedResult<NoteDto>>(SandboxErrors.NoteQueryFailed);
}
}
}

100%

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