In production software development, continuously delivering new business features into existing domain modules is the engineering team’s most frequent activity. In traditional layered architectures, varying developer habits quickly erode software quality: one engineer constructs raw SQL strings in controllers, another initiates manual database transactions within service layers, and yet another modifies private aggregate fields directly via monolithic “god services.”
BitzOrcas.Modern establishes a strict, industrial-grade 5-step feature delivery pipeline:
- Invariants First: State transitions must be governed explicitly by domain methods on aggregate roots; external mutation of properties is forbidden;
- Single-File Vertical Slice (One Use Case, One File): Contracts, Minimal API route metadata, validation rules, and business handlers are unified into an atomic compilation unit;
- Narrow Command Repository Port (
ICommandRepository): The write path depends solely on a minimal recovery and persistence port, eliminating query object leaks; - Autonomous Pipeline Governance: Authentication, granular RBAC action gates, 3-tier validation, database transactions, and event outbox dispatch are governed transparently by the 10-tier application pipeline;
- Real-Container Acceptance: Acceptance testing is driven by Testcontainers against real SQL Server 2022 and Redis instances, rejecting mock-based fragility.
This guide demonstrates introducing “Matter Litigation Stage Advancement (AdvanceMatterStageCommand)” into the Legal domain module (BitzOrcas.Modules.Legal).
5-Step Feature Delivery Pipeline
Step 1: Extend Domain Aggregate Root Invariants
The aggregate root is the sole guardian of business invariants and consistency boundaries. Modifying entity fields directly from handlers is prohibited; all transitions must be encapsulated as explicit domain methods:
using System;using BitzOrcas.Domain.Entities;using BitzOrcas.Domain.Results;using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.Legal.Domain;
/// <summary>/// Matter operational status enumeration/// </summary>public enum MatterStatus{ Draft = 1, Active = 2, Closed = 3, Archived = 4}
/// <summary>/// Litigation proceeding stage enumeration/// </summary>public enum MatterStage{ /// <summary> /// Formal docket filing and jurisdictional confirmation /// </summary> IntakeAccepted = 1,
/// <summary> /// Discovery and evidentiary exchange /// </summary> EvidenceDiscovery = 2,
/// <summary> /// Courtroom oral hearing and trial /// </summary> TrialHearing = 3,
/// <summary> /// Enforceable judgment and post-trial enforcement /// </summary> Enforcement = 4}
/// <summary>/// Domain event published when a litigation stage is successfully advanced/// </summary>public sealed record MatterStageAdvancedDomainEvent( string MatterId, string MatterCode, string TenantId, int PreviousStage, int NewStage, string OperatorId, DateTime OccurredAtUtc) : IDomainEvent;
/// <summary>/// Typed error catalog for matter stage advancement/// </summary>public static class MatterStageErrors{ public static readonly Error InvalidStatusForAdvance = Error.Conflict( "Matter.InvalidStatusForAdvance", "The case is not in active litigation status. Stage advancement is prohibited."); public static readonly Error StageRegressionForbidden = Error.Validation( "Matter.StageRegressionForbidden", "Target litigation stage must advance forward beyond the current stage."); public static readonly Error StageRemarksRequired = Error.Validation( "Matter.StageRemarksRequired", "Stage transition remarks are required and cannot exceed 500 characters."); public static readonly Error IdRequired = Error.Validation( "Matter.IdRequired", "Matter identifier cannot be empty."); public static readonly Error InvalidTargetStage = Error.Validation( "Matter.InvalidTargetStage", "Target litigation stage value is out of valid range."); public static readonly Error RemarksInvalid = Error.Validation( "Matter.RemarksInvalid", "Transition remarks cannot be empty and cannot exceed 500 characters.");}
public sealed partial class MatterIntake : TenantAggregateRoot<string>{ /// <summary> /// Active matter status /// </summary> [BitzColumn(Description = "Active litigation status")] public MatterStatus Status { get; private set; } = MatterStatus.Active;
/// <summary> /// Active litigation stage /// </summary> [BitzColumn(Description = "Current litigation proceeding stage")] public MatterStage CurrentStage { get; private set; } = MatterStage.IntakeAccepted;
/// <summary> /// Advances the active litigation stage under strict domain invariant rules /// </summary> /// <param name="targetStage">Target stage enumeration value.</param> /// <param name="stageRemarks">Auditable explanation and case notes.</param> /// <param name="operatorId">Identifier of the acting counsel or operator.</param> /// <returns>A Result indicating domain success or invariant failure.</returns> public Result AdvanceStage(MatterStage targetStage, string stageRemarks, string operatorId) { // 1. Guard core status invariant: only active cases (Status == MatterStatus.Active) can advance stages if (Status != MatterStatus.Active) { return Result.Failure(MatterStageErrors.InvalidStatusForAdvance); }
// 2. Guard irreversible progression invariant: stage progression must be strictly forward if ((int)targetStage <= (int)CurrentStage) { return Result.Failure(MatterStageErrors.StageRegressionForbidden); }
// 3. Guard auditable remarks invariant if (string.IsNullOrWhiteSpace(stageRemarks) || stageRemarks.Length > 500) { return Result.Failure(MatterStageErrors.StageRemarksRequired); }
var previousStage = (int)CurrentStage;
// 4. Mutate internal state CurrentStage = targetStage;
// 5. Append domain event (automatically published via transactional outbox upon commit) Raise(new MatterStageAdvancedDomainEvent( MatterId: Id, MatterCode: MatterCode, TenantId: TenantId, PreviousStage: previousStage, NewStage: (int)targetStage, OperatorId: operatorId, OccurredAtUtc: DateTime.UtcNow));
return Result.Success(); }}Step 2: Implement Single-File Vertical Slice (One Use Case, One File)
Create AdvanceMatterStageCommand.cs under src/Modules/Legal/BitzOrcas.Modules.Legal/Application/Matters/.
This file encapsulates command contracts, Minimal API route bindings, pure invariant validation rules, and the business handler:
using System;using System.Threading;using System.Threading.Tasks;using BitzOrcas.Application.Abstractions;using BitzOrcas.Application.Attributes;using BitzOrcas.Application.Security;using BitzOrcas.Application.Validation;using BitzOrcas.Domain.Abstractions;using BitzOrcas.Domain.Results;using BitzOrcas.Domain.Tenancy;using BitzOrcas.Modules.Legal.Domain;
namespace BitzOrcas.Modules.Legal.Application.Matters;
/// <summary>/// Command contract for advancing litigation proceeding stages/// </summary>/// <remarks>/// Automatically bound to Minimal API endpoints at compile time by Roslyn source generators,/// with fine-grained RBAC action permissions declared./// </remarks>[GenerateEndpoint( Route = "api/legal/matters/{matterId}/advance-stage", Method = EndpointMethod.Post, RequirePermission = "legal.matters.advance-stage", // Fine-grained RBAC permission code Summary = "Advance litigation proceeding stage", Description = "Advances civil litigation cases to discovery, courtroom hearing, or enforcement with full audit trails.")]public sealed record AdvanceMatterStageCommand( string MatterId, MatterStage TargetStage, string StageRemarks) : ICommand<Result>;
/// <summary>/// Tier 1 pure validation rule for stage advancement/// </summary>/// <remarks>/// Executed automatically at pipeline stage 5 (ValidationBehavior) before entering the handler./// </remarks>public sealed class AdvanceMatterStageCommandRule : IRequestRule<AdvanceMatterStageCommand>{ /// <summary> /// Validates request payload invariants asynchronously /// </summary> public ValueTask<Result> ValidateAsync(AdvanceMatterStageCommand command, CancellationToken cancellationToken) { // 1. Mandatory case identifier invariant if (string.IsNullOrWhiteSpace(command.MatterId)) { return ValueTask.FromResult(Result.Failure(MatterStageErrors.IdRequired)); }
// 2. Enum boundary invariant if (!Enum.IsDefined(typeof(MatterStage), command.TargetStage)) { return ValueTask.FromResult(Result.Failure(MatterStageErrors.InvalidTargetStage)); }
// 3. Mandatory transition remarks invariant if (string.IsNullOrWhiteSpace(command.StageRemarks) || command.StageRemarks.Length > 500) { return ValueTask.FromResult(Result.Failure(MatterStageErrors.RemarksInvalid)); }
return ValueTask.FromResult(Result.Success()); }}
/// <summary>/// Command handler orchestrating stage advancement/// </summary>/// <remarks>/// Focuses purely on domain orchestration. Transactions, outbox dispatch, and auditing are handled by the pipeline./// Dependency rule: Depends strictly on the narrow <see cref="ICommandRepository{TAggregateRoot, TId}"/> port./// </remarks>public sealed class AdvanceMatterStageCommandHandler : ICommandHandler<AdvanceMatterStageCommand, Result>{ private readonly ICommandRepository<MatterIntake, string> _repository; private readonly ICurrentUser _currentUser;
/// <summary> /// Initializes command handler dependencies /// </summary> public AdvanceMatterStageCommandHandler( ICommandRepository<MatterIntake, string> repository, ICurrentUser currentUser) { _repository = repository; _currentUser = currentUser; }
/// <summary> /// Executes stage advancement orchestration /// </summary> public async Task<Result> Handle(AdvanceMatterStageCommand request, CancellationToken cancellationToken) { // 1. Recover aggregate root from command repository (tenant isolation enforced automatically) var matterResult = await _repository.FindAsync(request.MatterId, cancellationToken); if (matterResult.IsFailure) { return Result.Failure(matterResult.Error); }
var matter = matterResult.GetValueOrThrow();
// 2. Invoke domain aggregate method to advance stage var advanceResult = matter.AdvanceStage( targetStage: request.TargetStage, stageRemarks: request.StageRemarks, operatorId: _currentUser.Id);
if (advanceResult.IsFailure) { return advanceResult; }
// 3. Persist aggregate changes (pipeline UnitOfWorkBehavior commits transaction and dispatches outbox) return await _repository.SaveAsync(matter, cancellationToken); }}Step 3: Implement Testcontainers Integration Tests
Create end-to-end integration tests under tests/BitzOrcas.Integration.Tests/Legal/.
Tests run against real SQL Server 2022 containers pinned by immutable SHA-256 digest:
using System.Net;using System.Net.Http.Json;using System.Threading.Tasks;using BitzOrcas.Domain.Results;using BitzOrcas.Integration.Tests.Fixtures;using BitzOrcas.Modules.Legal.Application.Matters;using BitzOrcas.Modules.Legal.Domain;using FluentAssertions;using Xunit;
namespace BitzOrcas.Integration.Tests.Legal;
/// <summary>/// End-to-end integration tests for litigation stage advancement/// </summary>public sealed class AdvanceMatterStageTests : IClassFixture<BitzOrcasWebApplicationFactory>{ private readonly BitzOrcasWebApplicationFactory _factory;
public AdvanceMatterStageTests(BitzOrcasWebApplicationFactory factory) { _factory = factory; }
[Fact] public async Task AdvanceStage_WithValidTargetStage_ShouldSucceedAndPersist() { // 1. Create client with authenticated tenant context using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-alpha-lawfirm");
// 2. Prepare valid command payload var command = new AdvanceMatterStageCommand( MatterId: "MAT-2026-0001", TargetStage: MatterStage.TrialHearing, StageRemarks: "Discovery phase concluded. Proceeding to courtroom hearing.");
// 3. Dispatch POST request var response = await client.PostAsJsonAsync( "api/legal/matters/MAT-2026-0001/advance-stage", command);
// 4. Assert HTTP 200 OK response.StatusCode.Should().Be(HttpStatusCode.OK);
// 5. Verify unified result contract var result = await response.Content.ReadFromJsonAsync<Result>(); result.Should().NotBeNull(); result!.IsSuccess.Should().BeTrue(); }
[Fact] public async Task AdvanceStage_WithStageRegression_ShouldReturnValidationError() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-alpha-lawfirm");
// Attempt backward regression (target stage is equal to or lower than current stage) var regressionCommand = new AdvanceMatterStageCommand( MatterId: "MAT-2026-0001", TargetStage: MatterStage.IntakeAccepted, StageRemarks: "Attempting to regress back to intake accepted.");
var response = await client.PostAsJsonAsync( "api/legal/matters/MAT-2026-0001/advance-stage", regressionCommand);
// Assert domain invariant rejection (HTTP 400 Bad Request) response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var result = await response.Content.ReadFromJsonAsync<Result>(); result.Should().NotBeNull(); result!.IsFailure.Should().BeTrue(); result.Error.Code.Should().Be("Matter.StageRegressionForbidden"); }}Step 4: Verification and Architecture Guardrails
Execute automated verification commands across the solution:
# 1. Compile entire solution under Release profile (strict nullability and Roslyn generation)dotnet build BitzOrcas.Modern.slnx -c Release
# 2. Run architecture boundary guard testsdotnet test tests/BitzOrcas.Architecture.Tests/ --filter FullyQualifiedName~ModuleBoundaryTests
# 3. Execute containerized integration tests (spins up Testcontainers)dotnet test tests/BitzOrcas.Integration.Tests/ --filter FullyQualifiedName~AdvanceMatterStageTestsSummary
The standard 5-step feature delivery pipeline guarantees architectural excellence:
- Zero Git Merge Conflicts: Business use cases reside entirely within self-contained files;
- Automated Governance: RBAC action checks, parameter validation, tenant isolation, and atomic outbox commits are managed transparently by the 10-tier pipeline;
- Narrow Write Ports: Handlers rely strictly on
ICommandRepository<T, TId>, preventing query object leaks; - Rock-Solid Reliability: Verified via real Testcontainers integration tests without mock illusions.