Skip to content

Showcase Walkthrough

Step-by-step guide to understanding, running, and extending the Pragmatic.Design Showcase.

Showcase is a full-stack reference application that demonstrates every module in the Pragmatic.Design ecosystem. It models an international hotel booking platform with three bounded contexts:

  • Catalog — hotel properties, room types, amenities, cancellation policies.
  • Booking — reservations, guests, room assignments, check-in/check-out workflows.
  • Billing — invoices, payments, line items, cross-boundary financial processing.

The domain is intentionally rich enough to exercise the entire Pragmatic.Design feature set: source-generated actions, entities with state machines, domain events crossing boundary lines, typed error results, i18n, caching, authorization, jobs, messaging, and more.

All source code lives under:

examples/showcase/

Showcase serves three purposes:

  1. Validation — every new Pragmatic feature must have at least one Showcase integration demonstrating it works end-to-end against a real database.
  2. Documentation by example — developers learn Pragmatic patterns by reading Showcase source.
  3. Regression detection — 200+ Showcase tests and 153+ integration tests guard against cross-module breakage.

examples/showcase/
├── src/
│ ├── Showcase.Catalog/ # Catalog boundary (class library)
│ ├── Showcase.Booking/ # Booking boundary (class library)
│ ├── Showcase.Billing/ # Billing boundary (class library)
│ ├── Showcase.Accounts/ # Identity/local auth (class library)
│ ├── Showcase.Host/ # ASP.NET Core host (monolith topology)
│ ├── Showcase.Billing.Host/ # Billing standalone host (distributed topology)
│ └── Showcase.Host.Distributed/# Distributed host (Billing as RemoteBoundary)
├── tests/
│ ├── Showcase.Tests/ # Unit tests (mapping, validation, entities, specs)
│ └── Showcase.IntegrationTests/# E2E tests against PostgreSQL via Testcontainers
├── docker-compose.yml # PostgreSQL 17 for persistent mode
├── docker-compose.messaging.yml # RabbitMQ for messaging transport
├── ENV.md # Dev environment reference
└── Showcase.slnx # Solution file

Two databases demonstrate compliance separation:

DatabaseBoundariesPurpose
ShowcaseAppDatabaseCatalog + BookingOperational — shared for SQL joins
ShowcaseFinancialDatabaseBillingFinancial — isolated for audit/compliance

Billing receives data exclusively via domain events (ReservationConfirmed triggers CreateDraftInvoiceMutation), never via SQL join. This models real-world regulatory separation.

Monolith (Showcase.Host): All three boundaries run in a single process. This is the default and what integration tests target.

Distributed (Showcase.Host.Distributed + Showcase.Billing.Host): Booking and Catalog run in one host. Billing runs as a separate service. Communication uses SG-generated HTTP invokers via [RemoteBoundary<BillingModule>].

Showcase.Host.Distributed/ShowcaseDistributedModule.cs
[Module]
[Include<BookingModule, ShowcaseAppDatabase>]
[Include<CatalogModule, ShowcaseAppDatabase>]
[RemoteBoundary<BillingModule>] // Billing calls go over HTTP
public sealed class ShowcaseDistributedModule;

The SG auto-generates HttpInvoker classes for each Billing action (e.g., CreateInvoiceWithFeesActionHttpInvoker) that serialize the action, POST to the remote host, and deserialize the result.


Terminal window
dotnet run --project examples/showcase/src/Showcase.Host

Open http://localhost:5000/scalar for the Scalar API UI.

This mode uses EF Core InMemory provider. Demo data (3 hotels, 2 guests) is seeded automatically at startup.

Start the database:

Terminal window
docker compose -f examples/showcase/docker-compose.yml up -d

This spins up PostgreSQL 17 on port 5433 (not the default 5432, to avoid conflicts). Configure connection strings in appsettings.Development.json:

{
"ConnectionStrings": {
"App": "Host=localhost;Port=5433;Database=showcase_app;Username=pragmatic;Password=Pragmatic@2026!",
"Financial": "Host=localhost;Port=5433;Database=showcase_financial;Username=pragmatic;Password=Pragmatic@2026!"
}
}

Then run with dotnet run --project examples/showcase/src/Showcase.Host.

Terminal window
docker compose -f examples/showcase/docker-compose.messaging.yml up -d

RabbitMQ 3.13-management runs on port 5672 (AMQP) / 15672 (management UI). Without RabbitMQ, Showcase falls back to Channel-based in-process transport.

Unit tests (no external dependencies):

Terminal window
dotnet test examples/showcase/tests/Showcase.Tests/

Integration tests (requires Docker for Testcontainers PostgreSQL):

Terminal window
dotnet test examples/showcase/tests/Showcase.IntegrationTests/

The core business flow from guest registration to payment:

POST /api/v1/guests
Content-Type: application/json
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneNumber": "+1234567890"
}

Returns the guest Guid. (Or use a seeded guest — the app seeds two at startup.)

GET /api/v1/properties?api-version=1.0

Returns hotel properties from the Catalog boundary. Each property has room types with base rates and max occupancy.

POST /api/v1/reservations?api-version=1.0
Content-Type: application/json
{
"request": {
"guestId": "<guest-id>",
"propertyId": "<property-id>",
"roomTypeId": "<room-type-id>",
"checkIn": "2026-04-15T14:00:00Z",
"checkOut": "2026-04-18T14:00:00Z",
"numberOfGuests": 2
}
}

This triggers CreateReservationAction, which:

  1. Validates input (sync: future date, GreaterThan; async: room availability).
  2. Calculates total (nights x base rate).
  3. Creates the entity with status Pending.
  4. Raises ReservationCreated domain event.
POST /api/v1/reservations/{id}/confirm?api-version=1.0

The ConfirmReservationMutation calls Reservation.Confirm(), which:

  1. Validates the state machine transition: Pending -> Confirmed.
  2. Raises ReservationConfirmed domain event.

This is where cross-boundary magic happens.

The ReservationConfirmedHandler in Billing catches the event, calculates tax, and calls billingActions.CreateDraftInvoice(...) to auto-create a draft invoice in the financial database. No cross-boundary SQL. No shared entity references.

Showcase.Billing/EventHandlers/ReservationConfirmedHandler.cs
[MessageHandler]
[Retry(MaxAttempts = 3, Strategy = BackoffStrategy.ExponentialWithJitter)]
public sealed partial class ReservationConfirmedHandler(
IBillingActions billingActions) : IMessageHandler<ReservationConfirmed>
POST /api/v1/invoices/{id}/pay?api-version=1.0
Content-Type: application/json
{ "paymentMethod": "CreditCard", "amount": 450.00 }

MarkInvoicePaidAction transitions the invoice to Paid and raises InvoicePaid.

POST /api/v1/reservations/{id}/checkin?api-version=1.0

CheckInGuestMutation validates Confirmed -> CheckedIn (or PaymentReceived -> CheckedIn) via the state machine, records who checked the guest in, and raises GuestCheckedIn.

The ReservationStatus enum defines all valid transitions:

Pending --> Confirmed --> PaymentReceived --> CheckedIn --> CheckedOut
| | |
+--> Cancelled +--> Cancelled +--> Cancelled
| |
+--> NoShow +--> NoShow

Defined declaratively with [TransitionFrom] attributes on each enum value. The SG generates a TransitionTo() method that enforces these transitions at runtime, returning a VoidResult<IError> on invalid transitions.


Showcase demonstrates the pattern of crossing boundaries via domain events, not shared database access:

[Booking] ReservationConfirmed event
|
v (via InMemoryEventDispatcher or MessageBus)
[Billing] ReservationConfirmedHandler
|
v (calls IBillingActions.CreateDraftInvoice)
[Billing] Invoice entity created in Financial database

Events carry all required data in their payload. ReservationConfirmed includes ReservationId, GuestId, PropertyId, TotalAmount, Currency, CheckIn, CheckOut, and OccurredAt. The Billing handler never reads from the Booking database.

Each boundary declares a [Boundary] class. The SG generates typed action interfaces:

// Declared:
[Boundary]
public partial class BookingBoundary;
// SG generates:
public interface IBookingActions
{
IDomainActionInvoker<CreateReservationAction, Guid> CreateReservation { get; }
IVoidDomainActionInvoker<ConfirmReservationAction> ConfirmReservation { get; }
// ...
}

Modules inject IBillingActions or IBookingActions to invoke cross-boundary logic without knowing implementation details.

5.3 Remote Boundary (Distributed Deployment)

Section titled “5.3 Remote Boundary (Distributed Deployment)”

In Showcase.Host.Distributed, the [RemoteBoundary<BillingModule>] attribute tells the SG to generate HTTP invokers instead of direct DI calls. The generated invoker serializes the action parameters, POSTs to the remote Billing host, and deserializes the result.

Configuration:

{
"Pragmatic": {
"RemoteBoundaries": {
"Billing": {
"BaseUrl": "https://localhost:5010"
}
}
}
}

Booking reads Catalog entities (Property, RoomType) via SQL join, which is valid when both boundaries share the same database:

[Boundary]
[ReadAccess<Property>]
[ReadAccess<RoomType>]
public partial class BookingBoundary;

The Reservation entity demonstrates trait composition:

[Entity<Guid>] // PersistenceId, Id property, setters
[Auditable] // CreatedAt, UpdatedAt, CreatedBy, UpdatedBy
[SoftDelete] // IsDeleted, DeletedAt
[OwnedEntity] // OwnerId, ownership filter
[ConcurrencyAware] // RowVersion for optimistic concurrency
[HasPresets] // Seeded preset data
[StateMachine<ReservationStatus>] // Enforced state transitions
[Relation.ManyToOne<Property>] // FK relationship
[Relation.ManyToOne<RoomType>] // FK relationship
[Relation.OneToMany<RoomAssignment>] // Collection navigation
public partial class Reservation : DomainEventSource, IEntity<Guid>

Each attribute triggers SG code generation. [Auditable] generates IAuditable properties and auto-populates them. [SoftDelete] generates ISoftDelete and adds a global query filter. [StateMachine] generates the TransitionTo() method.

[FastEnum]
public enum ReservationStatus
{
[InitialState]
Pending,
[TransitionFrom(ReservationStatus.Pending)]
Confirmed,
[TransitionFrom(ReservationStatus.Confirmed)]
PaymentReceived,
[TransitionFrom(ReservationStatus.Confirmed)]
[TransitionFrom(ReservationStatus.PaymentReceived)]
CheckedIn,
// ...
}

[FastEnum] generates AOT-safe enum helper methods. [TransitionFrom] attributes define the state graph. The entity’s TransitionTo() returns VoidResult<IError> — invalid transitions produce a typed error, not an exception.

Events are defined as immutable records:

public sealed record ReservationConfirmed(
Guid ReservationId, Guid GuestId, Guid PropertyId,
DateTimeOffset CheckIn, DateTimeOffset CheckOut,
decimal TotalAmount, string Currency,
DateTimeOffset OccurredAt) : DomainEvent(OccurredAt);

Entities raise events via RaiseEvent(). The infrastructure dispatches them after SaveChangesAsync succeeds.

Actions return Result<T, TError> instead of throwing:

public partial class CreateReservationAction : DomainAction<Guid, RoomUnavailableError>
{
public override async Task<Result<Guid, IError>> Execute(CancellationToken ct = default)
{
if (property is null)
return NotFoundError.For<Property, Guid>(id); // Not throw!
// ...
return reservation.Id; // Success
}
}

The SG maps Result.IsFailure to appropriate HTTP status codes (404 for NotFoundError, 409 for conflict, etc.).

Showcase defines permission constants, roles, and policies:

Program.cs
app.UseAuthorization(authz =>
{
authz.MapRole<BookingManagerRole>(r => r
.IncludeDefinition<BookingOperator>()
.IncludeDefinition<CatalogReader>());
authz.MapRole<ReceptionistRole>(r => r
.WithPermissions(
BookingPermissions.Reservation.Read,
BookingPermissions.Reservation.Create,
BookingPermissions.Reservation.Update)
.WithPermissions(BookingPermissions.Guest.All));
});

Actions use [RequirePolicy<T>] or [RequirePermission] for endpoint-level authorization. [OwnedEntity] on Reservation adds row-level ownership filtering.

Catalog entities use [Cacheable]:

[Cacheable(Duration = 300)] // 5 minutes
public partial class Property { ... }

Actions can invalidate cache via [InvalidatesCache<Property>].

Background jobs with [RecurringJob] and [Job] attributes. Jobs are SG-generated with retry, timeout, and telemetry.

Message handlers with [MessageHandler] and [Retry]. ReservationConfirmedHandler demonstrates the pattern. Transport is configurable: Channels (in-process), RabbitMQ, or outbox via EF Core.

The CheckInSaga demonstrates multi-step orchestration:

examples/showcase/src/Showcase.Booking/Reservations/Sagas/CheckInSaga.cs
examples/showcase/src/Showcase.Booking/Reservations/Sagas/CheckInState.cs

Translation keys are generated by the SG from [assembly: TranslationKeys]:

T.Property.Name, T.Property.Description
T.Reservation.Status.Pending, T.Reservation.Status.Confirmed, ...
T.Invoice.Status.Draft, T.Invoice.Status.Paid, ...

JSON translation files live in src/Showcase.Host/localization/.


7. Adding a New Feature: “Room Service”

Section titled “7. Adding a New Feature: “Room Service””

Suppose you want to add room service ordering to the Booking boundary. Here is the step-by-step process.

Create examples/showcase/src/Showcase.Booking/RoomService/RoomServiceOrder.cs:

using Pragmatic.Persistence.Entity;
namespace Showcase.Booking.RoomService;
[Entity<Guid>]
[Auditable]
[BelongsTo<BookingBoundary>]
[Relation.ManyToOne<Entities.Reservation>]
public partial class RoomServiceOrder : IEntity<Guid>
{
public Guid ReservationId { get; private set; }
public Entities.Reservation Reservation { get; set; } = null!;
public string ItemDescription { get; private set; } = "";
public decimal Amount { get; private set; }
public RoomServiceStatus Status { get; private set; } = RoomServiceStatus.Pending;
}

Create examples/showcase/src/Showcase.Booking/RoomService/RoomServiceStatus.cs:

using Pragmatic;
namespace Showcase.Booking.RoomService;
[FastEnum]
public enum RoomServiceStatus
{
Pending,
InProgress,
Delivered,
Cancelled
}

Create examples/showcase/src/Showcase.Booking/RoomService/Actions/CreateRoomServiceOrderAction.cs:

using Pragmatic.Actions.Abstractions;
using Pragmatic.Actions.Attributes;
using Pragmatic.Endpoints;
using Pragmatic.Endpoints.Attributes;
using Pragmatic.Persistence.Repository;
using Pragmatic.Result;
using Showcase.Booking.Entities;
namespace Showcase.Booking.RoomService.Actions;
[DomainAction]
[Endpoint(HttpVerb.Post, "api/v1/reservations/{reservationId}/room-service")]
[Validate]
public partial class CreateRoomServiceOrderAction : DomainAction<Guid>
{
private IRepository<RoomServiceOrder, Guid> _orders = null!;
private IReadRepository<Reservation, Guid> _reservations = null!;
public required Guid ReservationId { get; init; }
public required string ItemDescription { get; init; }
public required decimal Amount { get; init; }
public override async Task<Result<Guid, IError>> Execute(CancellationToken ct = default)
{
var reservation = await _reservations.GetByIdAsync(ReservationId, ct).ConfigureAwait(false);
if (reservation is null)
return NotFoundError.For<Reservation, Guid>(ReservationId);
var order = new RoomServiceOrder
{
ReservationId = ReservationId,
// ... set properties via SG-generated setters
};
_orders.Add(order);
return order.Id;
}
}

Create examples/showcase/src/Showcase.Booking/RoomService/Dtos/RoomServiceOrderDto.cs:

using Pragmatic.Mapping.Attributes;
namespace Showcase.Booking.RoomService.Dtos;
[MapFrom<RoomServiceOrder>]
public partial class RoomServiceOrderDto
{
public Guid Id { get; init; }
public Guid ReservationId { get; init; }
public string ItemDescription { get; init; } = "";
public decimal Amount { get; init; }
public RoomServiceStatus Status { get; init; }
}
Terminal window
dotnet build examples/showcase/src/Showcase.Booking/

The unified SG will detect the new entity and action, generating:

  • Entity setters, persistence configuration
  • Action invoker, DI registration
  • Endpoint handler
  • Mapping methods
  • Boundary interface update (adds CreateRoomServiceOrder to IBookingActions)

Create examples/showcase/tests/Showcase.Tests/Unit/RoomService/RoomServiceOrderTests.cs:

public class RoomServiceOrderTests
{
[Fact]
public void Create_WithValidData_SetsProperties()
{
// Test entity creation and initial state
}
}

Add to examples/showcase/tests/Showcase.IntegrationTests/DomainActions/:

[Fact]
public async Task CreateRoomServiceOrder_ForCheckedInReservation_ReturnsCreated()
{
// 1. Create guest, property, room type
// 2. Create and confirm reservation
// 3. Check in guest
// 4. POST room service order
// 5. Assert 201 Created
}
Terminal window
dotnet build examples/showcase/
dotnet test examples/showcase/tests/Showcase.Tests/
dotnet test examples/showcase/tests/Showcase.IntegrationTests/

ProjectTypeCountRequires
Showcase.TestsUnit~200Nothing
Showcase.IntegrationTestsE2E/HTTP~153Docker (Testcontainers)

Three classes power integration tests:

PostgresFixture (tests/Showcase.IntegrationTests/Infrastructure/PostgresFixture.cs):

  • Starts a PostgreSQL 17 Alpine container via Testcontainers.
  • Creates both databases (showcase_app and showcase_financial).
  • Applies EF Core migrations.
  • Shared across all test classes via xUnit CollectionFixture.

ShowcaseWebFactory (tests/Showcase.IntegrationTests/Infrastructure/ShowcaseWebFactory.cs):

  • Extends WebApplicationFactory<Program>.
  • Overrides only connection strings to point at Testcontainers.
  • Everything else (SG-generated DI, endpoints, middleware) runs exactly as in production.

IntegrationTestBase (tests/Showcase.IntegrationTests/Infrastructure/IntegrationTestBase.cs):

  • Provides HttpClient with default tenant/user headers.
  • Includes helper methods: PostAsync<T>, GetAsync<T>, PutAsync, DeleteAsync.
  • CreateClientAs(userId) for testing row-level security with different users.
  • CreateClientWithPermissions(...) for testing authorization with specific permissions.
  • CreateClientWithRoles(...) for testing role-based access.
  • CreateAnonymousClient() for testing unauthenticated access.
  • CreateClientWithJwt(token) for testing JWT authentication.
public class MyFeatureTests(PostgresFixture fixture) : IntegrationTestBase(fixture)
{
[Fact]
public async Task MyAction_ValidInput_ReturnsExpected()
{
// Arrange: create prerequisites
var guestId = await CreateGuest();
var propertyId = await CreateProperty();
// Act: call the endpoint
var response = await PostAsync("/api/v1/my-endpoint?api-version=1.0", new
{
guestId,
propertyId,
// ...
});
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
}
}

Integration tests are organized by concern:

FolderWhat It Tests
DomainActions/Action execution, composite actions, versioning
EntityPersistence/CRUD, soft delete, state machines, advanced entities
Endpoints/HTTP routing, OpenAPI, grid search, allow anonymous
Validation/Sync/async validation, custom validators
CrossCutting/Concurrency, feature flags, events, boundaries, i18n, authorization, data ownership, filters, remote boundary
Queries/Query filters, filter modes, WithoutFilter
Authorization/JWT auth, permission enforcement
Identity/Account endpoints (register, login)
Jobs/Job infrastructure, store integration

Testing authorization denial:

[Fact]
public async Task RefundInvoice_WithoutPermission_Returns403()
{
using var client = CreateClientWithPermissions("billing.invoice.read");
var response = await PostWithClientAsync(client, $"/api/v1/invoices/{id}/refund", new { });
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}

Testing row-level security:

[Fact]
public async Task GetReservation_AsOtherUser_ReturnsFiltered()
{
using var otherUser = CreateClientAs("other-user-id");
var response = await otherUser.GetAsync($"/api/v1/reservations/{id}");
response.StatusCode.Should().Be(HttpStatusCode.NotFound); // ownership filter
}

Testing domain events:

[Fact]
public async Task ConfirmReservation_CreatesInvoice()
{
// Create + confirm reservation
await PostAsync($"/api/v1/reservations/{id}/confirm?api-version=1.0", new { });
// Verify invoice was created in Billing (via ReservationConfirmedHandler)
var invoices = await GetAsync<List<InvoiceDto>>("/api/v1/invoices?api-version=1.0");
invoices.Should().ContainSingle(i => i.ReservationId == id);
}
Terminal window
# Only unit tests
dotnet test examples/showcase/tests/Showcase.Tests/ --filter "Category!=Integration"
# Only integration tests
dotnet test examples/showcase/tests/Showcase.IntegrationTests/
# Specific test class
dotnet test examples/showcase/tests/Showcase.IntegrationTests/ --filter "FullyQualifiedName~DomainActionTests"
# Specific test method
dotnet test examples/showcase/tests/Showcase.IntegrationTests/ --filter "CreateReservation_ValidInput_ReturnsCreated"

FilePurpose
src/Showcase.Catalog/CatalogBoundary.csCatalog boundary declaration
src/Showcase.Booking/BookingBoundary.csBooking boundary (+ ReadAccess to Catalog)
src/Showcase.Billing/BillingBoundary.csBilling boundary
FilePurpose
src/Showcase.Host/Program.csPragmaticApp.RunAsync with builder config
src/Showcase.Host/ShowcaseStartupStep.csBusiness services and HTTP pipeline
FilePurpose
src/Showcase.Booking/Reservations/Reservation.csMain entity with full trait composition
src/Showcase.Booking/Reservations/Enums/ReservationStatus.csState machine enum
src/Showcase.Booking/Reservations/Actions/CreateReservationAction.csDomain action with validation, versioning, feature flags
src/Showcase.Billing/EventHandlers/ReservationConfirmedHandler.csCross-boundary event handler
src/Showcase.Booking/Events/ReservationConfirmed.csEvent record carrying all needed data
FilePurpose
tests/Showcase.IntegrationTests/Infrastructure/PostgresFixture.csTestcontainers PostgreSQL
tests/Showcase.IntegrationTests/Infrastructure/ShowcaseWebFactory.csWebApplicationFactory override
tests/Showcase.IntegrationTests/Infrastructure/IntegrationTestBase.csBase class with HTTP helpers
FilePurpose
src/Showcase.Host.Distributed/ShowcaseDistributedModule.csModule with [RemoteBoundary<BillingModule>]
src/Showcase.Billing.Host/Program.csStandalone Billing host