In traditional layered architectures, adding a single business mutation (such as “closing and archiving a litigation matter”) requires modifying 6 to 8 disparate files: controllers, service interfaces, service implementations, DTOs, mappers, and entity classes. Fragmenting logic across multiple layers not only escalates cognitive overhead, but also causes cross-cutting concerns (such as RBAC authorization, idempotency deduplication, transaction boundaries, and audit trails) to be haphazardly copied or inadvertently omitted.
In BitzOrcas.Modern, we strictly enforce the “One Use Case, One File” architectural principle: Minimal API route metadata, invariant validation rules, declarative RBAC authorization attributes, and the domain command handler are unified in a single, self-contained .cs file, governed end-to-end by the 10-tier application pipeline.
This guide demonstrates building a production-grade command slice using Matter Closure and Archival (CloseMatterCommand) as a canonical golden sample.
Command Slice Structure & Pipeline Flow
Complete Command Slice Golden Sample
Create CloseMatterCommand.cs under src/Modules/Legal/BitzOrcas.Modules.Legal/Application/Matters/:
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 closing and archiving a litigation matter/// </summary>/// <remarks>/// <para>Carries matter identifier, closing settlement notes, and physical archive box tracking number.</para>/// <para>Automatically bound to Minimal API endpoints at compile-time with granular RBAC permissions declared.</para>/// </remarks>[GenerateEndpoint( Route = "api/legal/matters/{matterId}/close", Method = EndpointMethod.Post, RequirePermission = "legal.matters.close", // Declarative RBAC action code Summary = "Close matter and archive dossier", Description = "Formally closes an active litigation matter and records physical dossier archive box metadata")]public sealed record CloseMatterCommand( string MatterId, string ClosingRemarks, string ArchiveBoxNumber) : ICommand<Result>;
/// <summary>/// Typed domain error catalog for matter operations/// </summary>public static class MatterErrors{ public static readonly Error IdRequired = Error.Validation("Matter.IdRequired", "Matter identifier cannot be empty."); public static readonly Error ClosingRemarksInvalid = Error.Validation("Matter.ClosingRemarksInvalid", "Closing remarks are required and cannot exceed 1000 characters."); public static readonly Error ArchiveBoxRequired = Error.Validation("Matter.ArchiveBoxRequired", "Archive box location number is required and cannot exceed 64 characters.");}
/// <summary>/// Tier 1 pure invariant validation rule for closing matter/// </summary>/// <remarks>/// <para>Implements <see cref="IRequestRule{TRequest}"/>, executed at pipeline stage 5 (ValidationBehavior).</para>/// <para>Intercepts invalid payloads before reaching the domain handler, protecting domain models from dirty input.</para>/// </remarks>public sealed class CloseMatterCommandRule : IRequestRule<CloseMatterCommand>{ /// <summary> /// Validates request payload invariants asynchronously /// </summary> /// <param name="command">Command instance.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>A Result indicating validation success or failure.</returns> public ValueTask<Result> ValidateAsync(CloseMatterCommand command, CancellationToken cancellationToken) { // 1. Mandatory identifier invariant if (string.IsNullOrWhiteSpace(command.MatterId)) { return ValueTask.FromResult(Result.Failure(MatterErrors.IdRequired)); }
// 2. Mandatory closing remarks invariant with length bounds if (string.IsNullOrWhiteSpace(command.ClosingRemarks) || command.ClosingRemarks.Length > 1000) { return ValueTask.FromResult(Result.Failure(MatterErrors.ClosingRemarksInvalid)); }
// 3. Mandatory physical archive tracking number invariant if (string.IsNullOrWhiteSpace(command.ArchiveBoxNumber) || command.ArchiveBoxNumber.Length > 64) { return ValueTask.FromResult(Result.Failure(MatterErrors.ArchiveBoxRequired)); }
return ValueTask.FromResult(Result.Success()); }}
/// <summary>/// Command handler orchestrating matter closure/// </summary>/// <remarks>/// <para>Focuses strictly on domain orchestration. Transactions, outbox dispatch, and auditing are handled by the pipeline.</para>/// <para>Dependency standard: Depends solely on the narrow <see cref="ICommandRepository{TAggregateRoot, TId}"/> port.</para>/// </remarks>public sealed class CloseMatterCommandHandler : ICommandHandler<CloseMatterCommand, Result>{ private readonly ICommandRepository<MatterIntake, string> _repository; private readonly ICurrentUser _currentUser;
/// <summary> /// Initializes command handler dependencies /// </summary> /// <param name="repository">Command repository narrow port.</param> /// <param name="currentUser">Current user security context.</param> public CloseMatterCommandHandler( ICommandRepository<MatterIntake, string> repository, ICurrentUser currentUser) { _repository = repository; _currentUser = currentUser; }
/// <summary> /// Executes matter closure orchestration /// </summary> /// <param name="request">Closing command payload.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>Execution result.</returns> public async Task<Result> Handle(CloseMatterCommand 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 aggregate root domain behavior (guards active status, seals state, and registers domain events) var closeResult = matter.CloseMatter( closingRemarks: request.ClosingRemarks, archiveBoxNumber: request.ArchiveBoxNumber, operatorId: _currentUser.Id);
if (closeResult.IsFailure) { return closeResult; }
// 3. Persist updated aggregate root (pipeline UnitOfWorkBehavior commits transaction and dispatches outbox) return await _repository.SaveAsync(matter, cancellationToken); }}4 Core Rules for Writing Command Slices
- One Use Case, One File: Command contracts,
[GenerateEndpoint],IRequestRulevalidation, and Handlers must reside in a single, cohesive.csfile; - Zero Cross-Cutting Concerns in Handlers: Never manually begin/commit database transactions, invoke
_dbContext.SaveChangesAsync(), or record manual audit log rows; - Aggregate Roots Guard Invariants: All business state transitions (e.g.
matter.CloseMatter(remarks, boxNumber, operatorId)) must be encapsulated within domain methods; mutating properties directly via setters from handlers is strictly prohibited; - Depend Exclusively on Narrow
ICommandRepository: Write slices must not inject raw ORM contexts to execute complex read queries; queries and report exports must be offloaded to dedicated read-model stores.