Skip to content
bitzorcas
中EN

Recipe

Practical Guide: Add a Feature to an Existing Module

Master the 5-step feature delivery pipeline in BitzOrcas.Modern: extending domain aggregate invariants, single-file vertical slice, declarative RBAC endpoints, ICommandRepository persistence, and Testcontainers validation.

Last updated

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:

  1. Invariants First: State transitions must be governed explicitly by domain methods on aggregate roots; external mutation of properties is forbidden;
  2. 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;
  3. Narrow Command Repository Port (ICommandRepository): The write path depends solely on a minimal recovery and persistence port, eliminating query object leaks;
  4. 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;
  5. 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

1. Extend Aggregate
(MatterIntake Invariants)

2. Vertical Slice
(AdvanceMatterStageCommand)

3. Route & Permissions
([GenerateEndpoint])

4. Pure Rules
(IRequestRule)

5. Container Tests
(Testcontainers)


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:

src/Modules/Legal/BitzOrcas.Modules.Legal.Domain/MatterIntake.cs (Excerpt)
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:

src/Modules/Legal/BitzOrcas.Modules.Legal/Application/Matters/AdvanceMatterStageCommand.cs
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:

tests/BitzOrcas.Integration.Tests/Legal/AdvanceMatterStageTests.cs
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:

Terminal window
# 1. Compile entire solution under Release profile (strict nullability and Roslyn generation)
dotnet build BitzOrcas.Modern.slnx -c Release
# 2. Run architecture boundary guard tests
dotnet test tests/BitzOrcas.Architecture.Tests/ --filter FullyQualifiedName~ModuleBoundaryTests
# 3. Execute containerized integration tests (spins up Testcontainers)
dotnet test tests/BitzOrcas.Integration.Tests/ --filter FullyQualifiedName~AdvanceMatterStageTests

Summary

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.

100%

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