In traditional layered architectures, development teams inevitably succumb to “boilerplate glue hell”: adding a simple litigation matter intake feature requires jumping across 6 to 8 disparate files—MatterController, IMatterService, MatterServiceImpl, CreateMatterDto, MatterMapper, MatterEntity, IMatterRepository, and MatterRepositoryImpl. Even minor schema updates cascade across every horizontal layer, escalating cognitive load and causing frequent Git merge conflicts across collaborating engineers.
BitzOrcas.Modern replaces this outdated horizontal layering with Vertical Slice Architecture. Adhering to the principle of “One Use Case, One File,” Minimal API endpoint route declarations, declarative authorization metadata, command contracts, 4-tier validation rules, and business handlers are unified into self-contained compilation units.
This tutorial guides you through building a production-ready vertical slice grounded in a real-world Law Firm Civil Litigation Matter Intake scenario.
Vertical Slice Request Lifecycle
Step 1: Create the Domain Unified Aggregate Root
In BitzOrcas.Modern, domain models inherit directly from the framework’s TenantAggregateRoot<TId> base class, which provides:
Id: Globally unique entity identity;TenantId: Multi-tenant physical/logical partition key (viaITenantEntity);- Audit Envelopes:
CreateTime,CreateBy,UpdateTime,UpdateBy(implementingIAuditableEntity); - Soft Deletion & Concurrency:
IsDeleted,DeleteTime,RowVersion(implementingISoftDeleteandIConcurrencyTracked); - Domain Event Collection: Built-in
Raise(IDomainEvent)method.
Create domain error contracts and the aggregate root model, decorated with vendor-neutral [BitzTable] and [BitzColumn] attributes:
using BitzOrcas.Domain.Results;
namespace BitzOrcas.Modules.Legal.Domain;
/// <summary>/// Static domain error catalog for legal matter intake./// </summary>public static class LegalErrors{ /// <summary> /// Client and opposing party conflict of interest /// </summary> public static readonly Error AdversaryConflict = Error.Conflict("Legal.AdversaryConflict", "Client entity and opposing party cannot be identical.");
/// <summary> /// Claim amount must be positive /// </summary> public static readonly Error InvalidClaimAmount = Error.Validation("Legal.InvalidClaimAmount", "Litigation claim amount must be strictly greater than zero.");
/// <summary> /// Tenant context is required /// </summary> public static readonly Error TenantRequired = Error.Unauthorized("Tenant.Required", "Active tenant context missing or unresolved by tenancy chain.");
/// <summary> /// Matter title is invalid /// </summary> public static readonly Error InvalidTitle = Error.Validation("Legal.Validation.InvalidTitle", "Matter title cannot be empty or exceed 200 characters.");
/// <summary> /// Both retaining client and opposing party are required /// </summary> public static readonly Error PartiesRequired = Error.Validation("Legal.Validation.PartiesRequired", "Both retaining client and opposing party are mandatory.");}using System;using System.ComponentModel;using BitzOrcas.Domain.Entities;using BitzOrcas.Domain.Results;using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.Legal.Domain;
/// <summary>/// Matter intake unified aggregate root./// </summary>/// <remarks>/// Inherits TenantAggregateRoot to acquire tenancy isolation, auditing, and soft deletion./// Roslyn incremental source generators emit dual ORM (SqlSugar / EF Core) mappings at compile time./// </remarks>[BitzTable("LegalMatterIntake", IsTenant = true, IsSoftDelete = true, Description = "Civil Litigation Matter Intake")]public sealed class MatterIntake : TenantAggregateRoot<string>{ private const int CodeMaxLength = 32; private const int TitleMaxLength = 200; private const int PartyNameMaxLength = 100;
/// <summary> /// External business matter code (e.g. MAT-20260923-0001). /// </summary> [BitzColumn(Length = CodeMaxLength, IsRequired = true, IsUnique = true, Description = "Matter Code")] public string MatterCode { get; private set; } = string.Empty;
/// <summary> /// Descriptive title of the legal dispute. /// </summary> [BitzColumn(Length = TitleMaxLength, IsRequired = true, Description = "Matter Title")] public string MatterTitle { get; private set; } = string.Empty;
/// <summary> /// Retaining client legal entity name. /// </summary> [BitzColumn(Length = PartyNameMaxLength, IsRequired = true, Description = "Client Legal Name")] public string ClientName { get; private set; } = string.Empty;
/// <summary> /// Opposing party name used for adversary conflict of interest checks. /// </summary> [BitzColumn(Length = PartyNameMaxLength, IsRequired = true, Description = "Opposing Party Name")] public string OpposingParty { get; private set; } = string.Empty;
/// <summary> /// Monetary claim value in dispute. /// </summary> [BitzColumn(Precision = 18, Scale = 2, IsRequired = true, Description = "Claim Amount")] public decimal ClaimAmount { get; private set; }
/// <summary> /// Parameterless constructor reserved strictly for ORM materialization. /// </summary> [Obsolete("For ORM materialization only. Use Create.", error: true)] [EditorBrowsable(EditorBrowsableState.Never)] public MatterIntake() : base("0") { }
private MatterIntake( string id, string matterCode, string matterTitle, string clientName, string opposingParty, decimal claimAmount, string tenantId) : base(id) { MatterCode = matterCode; MatterTitle = matterTitle; ClientName = clientName; OpposingParty = opposingParty; ClaimAmount = claimAmount; TenantId = tenantId; }
/// <summary> /// Factory method enforcing domain business invariants prior to instantiation. /// </summary> public static Result<MatterIntake> Create( string id, string matterCode, string matterTitle, string clientName, string opposingParty, decimal claimAmount, string tenantId) { // Invariant 1: Client and adversary cannot be the same entity if (string.Equals(clientName.Trim(), opposingParty.Trim(), StringComparison.OrdinalIgnoreCase)) { return Result<MatterIntake>.Failure(LegalErrors.AdversaryConflict); }
// Invariant 2: Monetary claim value must be strictly positive if (claimAmount <= 0) { return Result<MatterIntake>.Failure(LegalErrors.InvalidClaimAmount); }
var intake = new MatterIntake( id, matterCode.Trim(), matterTitle.Trim(), clientName.Trim(), opposingParty.Trim(), claimAmount, tenantId);
return Result<MatterIntake>.Success(intake); }}Step 2: Implement the Command and Handler (One File Use Case)
In BitzOrcas.Modern, Minimal API endpoint generation, declarative authorization rules, command definitions, and handlers reside within a unified slice file.
Create CreateMatterIntakeCommand.cs in src/Modules/Business/Legal/Application/Commands/CreateMatterIntake/:
using BitzOrcas.Application.Abstractions.Tenancy;using BitzOrcas.Application.Authorization;using BitzOrcas.Domain.Abstractions;using BitzOrcas.Domain.Results;using BitzOrcas.Endpoint.Attributes;using BitzOrcas.Modules.Legal.Domain;using Mediator;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;
/// <summary>/// Matter intake command model./// </summary>/// <remarks>/// Decorated with [GenerateEndpoint] for compile-time Minimal API route synthesis./// Implements IAuthorizedRequest for transparent RBAC policy enforcement by the pipeline./// </remarks>[GenerateEndpoint(HttpRoute.Post, "/api/legal/matters", Tag = "LegalMatter")]public sealed record CreateMatterIntakeCommand( string MatterTitle, string ClientName, string OpposingParty, decimal ClaimAmount) : ICommand<Result<string>>, IAuthorizedRequest{ /// <summary> /// Protected resource: Legal domain Matter resource. /// </summary> public ResourceDescriptor Resource { get; } = new("Legal", "Matter");
/// <summary> /// Declared operational action: Create. /// </summary> public AuthorizationAction Action { get; } = AuthorizationAction.Create;}
/// <summary>/// Matter intake orchestration handler./// </summary>/// <remarks>/// Injects generic narrow command repository ICommandRepository without leaking ORM internals./// Transactions are managed transparently by the 10-tier application pipeline./// </remarks>public sealed class CreateMatterIntakeCommandHandler( ICommandRepository<MatterIntake, string> repository, ICurrentTenant currentTenant) : ICommandHandler<CreateMatterIntakeCommand, Result<string>>{ /// <summary> /// Executes matter intake workflow. /// </summary> public async ValueTask<Result<string>> Handle( CreateMatterIntakeCommand request, CancellationToken cancellationToken) { // 1. Verify valid multi-tenant execution context var tenant = currentTenant.Tenant; if (!tenant.IsAvailable) { return Result<string>.Failure(LegalErrors.TenantRequired); }
// 2. Generate deterministic entity ID and business tracking code var matterId = Guid.NewGuid().ToString("N"); var matterCode = $"MAT-{DateTime.UtcNow:yyyyMMdd}-{matterId[..6].ToUpperInvariant()}";
// 3. Delegate invariant validation to domain aggregate factory var createResult = MatterIntake.Create( matterId, matterCode, request.MatterTitle, request.ClientName, request.OpposingParty, request.ClaimAmount, tenant.EffectiveTenantId);
if (createResult.IsFailure) { return Result<string>.Failure(createResult.Error); }
var matter = createResult.GetValueOrThrow();
// 4. Save aggregate through ICommandRepository; transaction pipeline commits atomically var saveResult = await repository.SaveAsync(matter, cancellationToken); if (saveResult.IsFailure) { return Result<string>.Failure(saveResult.Error); }
// 5. Return new entity ID return Result<string>.Success(matter.Id); }}Step 3: Implement Tier 1 Static Validation Rules (IRequestRule)
BitzOrcas enforces a 4-tier validation hierarchy. Basic invariant checks—such as string length bounds and non-null constraints—are encapsulated in an IRequestRule<TRequest> executed automatically before reaching the handler:
using BitzOrcas.Application.Validation;using BitzOrcas.Domain.Results;using BitzOrcas.Modules.Legal.Domain;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;
/// <summary>/// Tier 1 invariant validation rule for matter intake command./// </summary>public sealed class CreateMatterIntakeCommandRule : IRequestRule<CreateMatterIntakeCommand>{ public ValueTask<Result> ValidateAsync(CreateMatterIntakeCommand request, CancellationToken cancellationToken) { // Validate required title and length if (string.IsNullOrWhiteSpace(request.MatterTitle) || request.MatterTitle.Length > 200) { return ValueTask.FromResult(Result.Failure(LegalErrors.InvalidTitle)); }
// Validate party names if (string.IsNullOrWhiteSpace(request.ClientName) || string.IsNullOrWhiteSpace(request.OpposingParty)) { return ValueTask.FromResult(Result.Failure(LegalErrors.PartiesRequired)); }
// Validate positive claim amount if (request.ClaimAmount <= 0) { return ValueTask.FromResult(Result.Failure(LegalErrors.InvalidClaimAmount)); }
return ValueTask.FromResult(Result.Success()); }}Step 4: End-to-End Integration Testing (Machine Verification)
Add integration tests in tests/BitzOrcas.Integration.Tests/Legal/ to assert slice execution through the real HTTP pipeline:
using System.Net;using System.Net.Http.Json;using BitzOrcas.Domain.Results;using BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;using BitzOrcas.Modules.Legal.Domain;using Shouldly;using Xunit;
public sealed class MatterIntakeSliceTests : IClassFixture<CustomWebApplicationFactory>{ private readonly HttpClient _client;
public MatterIntakeSliceTests(CustomWebApplicationFactory factory) { // Obtain authenticated HTTP client populated with valid test JWT credentials _client = factory.CreateAuthenticatedClient(tenantId: "1000001", role: "SuperAdmin"); }
[Fact] public async Task CreateMatter_WithValidPayload_ShouldReturnSuccessWithGeneratedId() { // 1. Arrange realistic litigation matter intake payload var command = new CreateMatterIntakeCommand( MatterTitle: "Cross-Border Trade Secret Infringement Injunction", ClientName: "Apex Semiconductor Technologies Corp.", OpposingParty: "Former VP of Engineering & Entity B Corp", ClaimAmount: 15_000_000.00m);
// 2. Dispatch POST request through Minimal API pipeline var response = await _client.PostAsJsonAsync("/api/legal/matters", command);
// 3. Machine assertion: assert HTTP 200 OK response.StatusCode.ShouldBe(HttpStatusCode.OK);
// 4. Assert unified Result<string> response contains valid identifier var result = await response.Content.ReadFromJsonAsync<Result<string>>(); result.ShouldNotBeNull(); result.IsSuccess.ShouldBeTrue(); result.Value.ShouldNotBeNullOrWhiteSpace(); }
[Fact] public async Task CreateMatter_WithSameClientAndOpponent_ShouldFailWithAdversaryConflictError() { // 1. Construct conflicted payload with identical client and opponent var conflictCommand = new CreateMatterIntakeCommand( MatterTitle: "Shareholder Resolution Validity Injunction", ClientName: "Holdings Group LLC", OpposingParty: "Holdings Group LLC", ClaimAmount: 1_000_000.00m);
// 2. Dispatch request var response = await _client.PostAsJsonAsync("/api/legal/matters", conflictCommand);
// 3. Assert failure: pipeline short-circuits with RFC 9457 Problem Details response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); var result = await response.Content.ReadFromJsonAsync<Result<string>>(); result.ShouldNotBeNull(); result.IsFailure.ShouldBeTrue(); result.Error.Code.ShouldBe(LegalErrors.AdversaryConflict.Code); }}Architectural Review & Benefits
Following this tutorial, your vertical slice achieves enterprise-grade quality:
- Zero Boilerplate: Eliminated glue classes (
Controller,Service,Repository); logic is encapsulated within the use case itself. - Zero-Reflection Assembly:
[GenerateEndpoint]and[BitzTable]execute at compile time, guaranteeing full compatibility with .NET 10 Native AOT trimming. - Transparent Governance: Logging, RBAC authorization, multi-tier validation, database transactions, and CAP Outbox publishing are handled implicitly by the 10-tier application pipeline.