From 6e0af4004f88581af67e47037ad0ddeecf628b15 Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 27 Oct 2026 19:48:50 +0100 Subject: [PATCH 1/3] WIP L1: migrate sample specifications to Specifications 5 and align Library AppHost (#667, #668) --- .github/scripts/run-specs.sh | 35 +++++++++ .github/scripts/run-specs.test.mjs | 76 +++++++++++++++++++ .github/workflows/build.yml | 17 ++++- .../Arc.React.Specs/Arc.React.Specs.csproj | 11 +-- Arc/React/README.md | 2 +- .../Backend.Specs/Backend.Specs.csproj | 10 +-- Chronicle/Backend/README.md | 2 +- .../CrossStore.Specs/CrossStore.Specs.csproj | 10 +-- Chronicle/CrossStore/README.md | 2 +- .../MultiTenancy.Specs.csproj | 10 +-- Chronicle/MultiTenancy/README.md | 2 +- .../OperationsDiagnosis.Specs.csproj | 10 +-- Chronicle/OperationsDiagnosis/README.md | 2 +- .../Processing.Specs/Processing.Specs.csproj | 10 +-- Chronicle/Processing/README.md | 2 +- .../ProjectionStyles.Specs.csproj | 8 +- Chronicle/ProjectionStyles/README.md | 2 +- Chronicle/Quickstart/AspNetCore/README.md | 2 +- .../Common.AspNetCore.Specs.csproj | 8 +- .../when_configuring_mongodb.cs | 4 +- Directory.Packages.props | 22 +++--- GeneratedValues/Shop/README.md | 2 +- .../Shop/Shop.Specs/Shop.Specs.csproj | 11 +-- LearningPath/05-Testing/README.md | 2 +- .../Testing.Specs/Testing.Specs.csproj | 8 +- .../07-ValidationAndAuthorization/README.md | 2 +- .../ValidationAndAuthorization.Specs.csproj | 8 +- .../Production.Specs/Production.Specs.csproj | 8 +- LearningPath/08-Production/README.md | 2 +- LearningPath/README.md | 6 +- Library/Composition/Composition.csproj | 1 - Library/Composition/Program.cs | 25 ++++-- Library/Lending/Lending.csproj | 20 ++--- Library/Lending/Program.cs | 8 ++ Library/Members/Members.csproj | 20 ++--- Library/Members/Program.cs | 8 ++ Library/README.md | 7 +- Samples.slnx | 1 + 38 files changed, 245 insertions(+), 141 deletions(-) create mode 100644 .github/scripts/run-specs.sh create mode 100644 .github/scripts/run-specs.test.mjs diff --git a/.github/scripts/run-specs.sh b/.github/scripts/run-specs.sh new file mode 100644 index 00000000..1d62beb7 --- /dev/null +++ b/.github/scripts/run-specs.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) Cratis. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +# Accept dotnet run arguments, including -- before native xUnit runner options. +set -euo pipefail + +report_directory=$(mktemp -d) +trap 'rm -rf -- "$report_directory"' EXIT +report="$report_directory/results.xml" + +# set -e preserves a native failure without inspecting or replacing its exit code. +dotnet run "$@" -result-xml "$report" + +python3 - "$report" <<'PY' +import re +import sys +import xml.etree.ElementTree as ET + +try: + root = ET.parse(sys.argv[1]).getroot() + if root.tag != "assemblies": + raise ValueError("expected an assemblies root") + assemblies = list(root) + if not assemblies or any(assembly.tag != "assembly" for assembly in assemblies): + raise ValueError("expected assembly results") + totals = [assembly.get("total", "") for assembly in assemblies] + if any(re.fullmatch(r"[0-9]+", total) is None for total in totals): + raise ValueError("expected nonnegative integer assembly totals") + if sum(int(total) for total in totals) == 0: + raise ValueError("no tests selected") +except (OSError, ET.ParseError, ValueError) as error: + print(f"Invalid xUnit report {sys.argv[1]}: {error}", file=sys.stderr) + sys.exit(1) +PY diff --git a/.github/scripts/run-specs.test.mjs b/.github/scripts/run-specs.test.mjs new file mode 100644 index 00000000..5fe24fa6 --- /dev/null +++ b/.github/scripts/run-specs.test.mjs @@ -0,0 +1,76 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { test } from 'node:test'; + +const script = join(dirname(fileURLToPath(import.meta.url)), 'run-specs.sh'); +const runnerArguments = ['--project', 'Safe.Specs.csproj', '--configuration', 'Debug', '--no-build', '--no-restore', '--', '-class', 'Safe.Specs.*']; + +function runWrapper(t, report, nativeExit = 0) { + const directory = mkdtempSync(join(tmpdir(), 'run-specs-test-')); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const temporaryReports = join(directory, 'reports'); + mkdirSync(temporaryReports); + const argumentsFile = join(directory, 'arguments.json'); + writeFileSync(join(directory, 'dotnet'), `#!/usr/bin/env node +const fs = require('node:fs'); +const args = process.argv.slice(2); +fs.writeFileSync(process.env.ARGUMENTS_FILE, JSON.stringify(args)); +if (process.env.REPORT_XML !== undefined) { + fs.writeFileSync(args[args.indexOf('-result-xml') + 1], process.env.REPORT_XML); +} +process.exit(Number(process.env.NATIVE_EXIT)); +`, { mode: 0o755 }); + const env = { + ...process.env, + PATH: `${directory}:${process.env.PATH}`, + TMPDIR: temporaryReports, + ARGUMENTS_FILE: argumentsFile, + NATIVE_EXIT: String(nativeExit) + }; + delete env.REPORT_XML; + if (report !== undefined) env.REPORT_XML = report; + const result = spawnSync('bash', [script, ...runnerArguments], { env, encoding: 'utf8' }); + assert.equal(result.error, undefined); + assert.equal(result.signal, null); + assert.deepEqual(readdirSync(temporaryReports), []); + return { ...result, arguments: JSON.parse(readFileSync(argumentsFile, 'utf8')) }; +} + +test('accepts a positive sum across assemblies and forwards runner arguments', t => { + const result = runWrapper(t, ''); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(result.arguments.slice(0, -2), ['run', ...runnerArguments]); + assert.equal(result.arguments.at(-2), '-result-xml'); +}); + +test('preserves native failure before attempting to parse a missing report', t => { + const result = runWrapper(t, undefined, 42); + assert.equal(result.status, 42); + assert.equal(result.stderr, ''); +}); + +for (const [name, report] of [ + ['missing report', undefined], + ['malformed XML', ''], + ['wrong root', ''], + ['missing assemblies', ''], + ['unexpected child', ''], + ['missing total', ''], + ['negative total', ''], + ['fractional total', ''], + ['nonnumeric total', ''], + ['zero total', ''] +]) { + test(`rejects ${name}`, t => { + const result = runWrapper(t, report); + assert.equal(result.status, 1); + assert.match(result.stderr, /Invalid xUnit report/); + }); +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10038568..dea7398b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,9 @@ jobs: run: dotnet build Samples.slnx --configuration Debug --no-restore - name: Run focused Debug specifications + shell: bash run: | + set -euo pipefail for project in \ Arc/React/Arc.React.Specs/Arc.React.Specs.csproj \ Chronicle/Backend/Backend.Specs/Backend.Specs.csproj \ @@ -53,17 +55,26 @@ jobs: Chronicle/Processing/Processing.Specs/Processing.Specs.csproj \ Chronicle/ProjectionStyles/ProjectionStyles.Specs/ProjectionStyles.Specs.csproj \ Chronicle/Quickstart/Common.AspNetCore.Specs/Common.AspNetCore.Specs.csproj \ - GeneratedValues/Shop/Rendered/Shop.Rendered.csproj \ GeneratedValues/Shop/Shop.Specs/Shop.Specs.csproj \ LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj \ LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj \ LearningPath/08-Production/Production.Specs/Production.Specs.csproj do - dotnet test "$project" --configuration Debug --verbosity normal + bash .github/scripts/run-specs.sh --project "$project" --configuration Debug --no-build --no-restore -- done - name: Run Library Lending in-process specifications - run: dotnet test Library/Lending/Lending.csproj --configuration Debug --no-build --filter "FullyQualifiedName~.for_" --verbosity normal + run: bash .github/scripts/run-specs.sh --project Library/Lending/Lending.csproj --configuration Debug --no-build --no-restore -- --specifications -class "*.for_*" + + - name: Run Library Members in-process specifications + run: bash .github/scripts/run-specs.sh --project Library/Members/Members.csproj --configuration Debug --no-build --no-restore -- --specifications + + # Stage owns this v2 runner until L13/L44 regenerate it. + - name: Run Stage-generated specifications + run: dotnet test GeneratedValues/Shop/Rendered/Shop.Rendered.csproj --configuration Debug --no-build --verbosity normal + + - name: Restore Release dependencies + run: dotnet restore Samples.slnx -p:Configuration=Release - name: Build Release without regenerating proxies run: dotnet build Samples.slnx --configuration Release --no-restore -p:CratisProxiesOutputPath= diff --git a/Arc/React/Arc.React.Specs/Arc.React.Specs.csproj b/Arc/React/Arc.React.Specs/Arc.React.Specs.csproj index c60d6e0f..0089db34 100644 --- a/Arc/React/Arc.React.Specs/Arc.React.Specs.csproj +++ b/Arc/React/Arc.React.Specs/Arc.React.Specs.csproj @@ -1,7 +1,7 @@ false - true + Exe Arc.React.Specs $(NoWarn);CA1001;IDE1006;SA1134 @@ -9,13 +9,8 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/Arc/React/README.md b/Arc/React/README.md index d88465ce..d6990510 100644 --- a/Arc/React/README.md +++ b/Arc/React/README.md @@ -98,7 +98,7 @@ dotnet build Arc/React/Arc.React.csproj -c Debug dotnet build Arc/React/Arc.React.csproj -c Release -p:CratisProxiesOutputPath= # Run the focused backend specifications -dotnet test Arc/React/Arc.React.Specs/Arc.React.Specs.csproj -c Debug +dotnet run --project Arc/React/Arc.React.Specs/Arc.React.Specs.csproj -c Debug -- # Type-check, lint, specify, and bundle React yarn tsc -b Arc/React/.frontend/tsconfig.json diff --git a/Chronicle/Backend/Backend.Specs/Backend.Specs.csproj b/Chronicle/Backend/Backend.Specs/Backend.Specs.csproj index e212e324..0f084642 100644 --- a/Chronicle/Backend/Backend.Specs/Backend.Specs.csproj +++ b/Chronicle/Backend/Backend.Specs/Backend.Specs.csproj @@ -2,19 +2,15 @@ Chronicle.Backend.Specs false + Exe - - + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Chronicle/Backend/README.md b/Chronicle/Backend/README.md index 6860941d..82394750 100644 --- a/Chronicle/Backend/README.md +++ b/Chronicle/Backend/README.md @@ -93,7 +93,7 @@ app.UseCratisChronicle(); ```bash dotnet build Chronicle/Backend/Backend.csproj -dotnet test Chronicle/Backend/Backend.Specs/Backend.Specs.csproj +dotnet run --project Chronicle/Backend/Backend.Specs/Backend.Specs.csproj -- ``` The small specification project checks the append and history behavior without requiring a running Chronicle container. diff --git a/Chronicle/CrossStore/CrossStore.Specs/CrossStore.Specs.csproj b/Chronicle/CrossStore/CrossStore.Specs/CrossStore.Specs.csproj index 3af8cded..5e268af8 100644 --- a/Chronicle/CrossStore/CrossStore.Specs/CrossStore.Specs.csproj +++ b/Chronicle/CrossStore/CrossStore.Specs/CrossStore.Specs.csproj @@ -1,7 +1,7 @@ CrossStore.Specs - true + Exe false @@ -11,11 +11,7 @@ - - - - - all - + + diff --git a/Chronicle/CrossStore/README.md b/Chronicle/CrossStore/README.md index 991f0976..686737eb 100644 --- a/Chronicle/CrossStore/README.md +++ b/Chronicle/CrossStore/README.md @@ -117,7 +117,7 @@ public FulfillmentOrderReceived Requested(OrderRequestedForFulfillment @event) = ```bash dotnet build Chronicle/CrossStore/CrossStore.csproj -dotnet test Chronicle/CrossStore/CrossStore.Specs/CrossStore.Specs.csproj +dotnet run --project Chronicle/CrossStore/CrossStore.Specs/CrossStore.Specs.csproj -- ``` ## Make it yours diff --git a/Chronicle/MultiTenancy/MultiTenancy.Specs/MultiTenancy.Specs.csproj b/Chronicle/MultiTenancy/MultiTenancy.Specs/MultiTenancy.Specs.csproj index 24ec9205..110600b0 100644 --- a/Chronicle/MultiTenancy/MultiTenancy.Specs/MultiTenancy.Specs.csproj +++ b/Chronicle/MultiTenancy/MultiTenancy.Specs/MultiTenancy.Specs.csproj @@ -1,7 +1,7 @@ MultiTenancy.Specs - true + Exe false @@ -12,11 +12,7 @@ - - - - - all - + + diff --git a/Chronicle/MultiTenancy/README.md b/Chronicle/MultiTenancy/README.md index 81ada291..3836737b 100644 --- a/Chronicle/MultiTenancy/README.md +++ b/Chronicle/MultiTenancy/README.md @@ -123,7 +123,7 @@ Build it directly while exploring, or as part of the root Samples solution: ```bash dotnet build Chronicle/MultiTenancy/MultiTenancy.csproj -dotnet test Chronicle/MultiTenancy/MultiTenancy.Specs/MultiTenancy.Specs.csproj +dotnet run --project Chronicle/MultiTenancy/MultiTenancy.Specs/MultiTenancy.Specs.csproj -- ``` The focused namespace-isolation spec uses Chronicle's in-process event scenario, so the test suite needs no container. diff --git a/Chronicle/OperationsDiagnosis/OperationsDiagnosis.Specs/OperationsDiagnosis.Specs.csproj b/Chronicle/OperationsDiagnosis/OperationsDiagnosis.Specs/OperationsDiagnosis.Specs.csproj index ca64e728..7ba5fdb4 100644 --- a/Chronicle/OperationsDiagnosis/OperationsDiagnosis.Specs/OperationsDiagnosis.Specs.csproj +++ b/Chronicle/OperationsDiagnosis/OperationsDiagnosis.Specs/OperationsDiagnosis.Specs.csproj @@ -1,7 +1,7 @@ OperationsDiagnosis.Specs - true + Exe false @@ -11,11 +11,7 @@ - - - - - all - + + diff --git a/Chronicle/OperationsDiagnosis/README.md b/Chronicle/OperationsDiagnosis/README.md index 31fbe031..10102cbf 100644 --- a/Chronicle/OperationsDiagnosis/README.md +++ b/Chronicle/OperationsDiagnosis/README.md @@ -118,7 +118,7 @@ Use **Overview**, **Observers**, **Failures**, and **Event Sequences** to follow ```bash dotnet build Chronicle/OperationsDiagnosis/OperationsDiagnosis.csproj -dotnet test Chronicle/OperationsDiagnosis/OperationsDiagnosis.Specs/OperationsDiagnosis.Specs.csproj +dotnet run --project Chronicle/OperationsDiagnosis/OperationsDiagnosis.Specs/OperationsDiagnosis.Specs.csproj -- ``` ## Make it yours—safely diff --git a/Chronicle/Processing/Processing.Specs/Processing.Specs.csproj b/Chronicle/Processing/Processing.Specs/Processing.Specs.csproj index 815c0004..de31fa18 100644 --- a/Chronicle/Processing/Processing.Specs/Processing.Specs.csproj +++ b/Chronicle/Processing/Processing.Specs/Processing.Specs.csproj @@ -1,7 +1,7 @@ Processing.Specs - true + Exe false @@ -11,11 +11,7 @@ - - - - - all - + + diff --git a/Chronicle/Processing/README.md b/Chronicle/Processing/README.md index e729ec4f..187f72d7 100644 --- a/Chronicle/Processing/README.md +++ b/Chronicle/Processing/README.md @@ -97,7 +97,7 @@ Build it on its own while exploring, or as part of the root Samples solution: ```bash dotnet build Chronicle/Processing/Processing.csproj -dotnet test Chronicle/Processing/Processing.Specs/Processing.Specs.csproj +dotnet run --project Chronicle/Processing/Processing.Specs/Processing.Specs.csproj -- ``` The focused specifications show the projection binding, reducer fold (including an update before opening), and deterministic reactor result without introducing external infrastructure. diff --git a/Chronicle/ProjectionStyles/ProjectionStyles.Specs/ProjectionStyles.Specs.csproj b/Chronicle/ProjectionStyles/ProjectionStyles.Specs/ProjectionStyles.Specs.csproj index 9b676d2e..a82bf2a7 100644 --- a/Chronicle/ProjectionStyles/ProjectionStyles.Specs/ProjectionStyles.Specs.csproj +++ b/Chronicle/ProjectionStyles/ProjectionStyles.Specs/ProjectionStyles.Specs.csproj @@ -1,7 +1,7 @@ ProjectionStyles.Specs - true + Exe false @@ -10,9 +10,7 @@ - - - - + + diff --git a/Chronicle/ProjectionStyles/README.md b/Chronicle/ProjectionStyles/README.md index 694a7db6..bcc7070e 100644 --- a/Chronicle/ProjectionStyles/README.md +++ b/Chronicle/ProjectionStyles/README.md @@ -85,7 +85,7 @@ Both forms process an order's stream: neither can join `ProductRegistered` or `P Install the [.NET 10 SDK](https://dotnet.microsoft.com/download), then run from the repository root: ```bash -dotnet test Chronicle/ProjectionStyles/ProjectionStyles.Specs/ProjectionStyles.Specs.csproj --configuration Debug +dotnet run --project Chronicle/ProjectionStyles/ProjectionStyles.Specs/ProjectionStyles.Specs.csproj --configuration Debug -- ``` [`ProjectionStyles.Specs`](./ProjectionStyles.Specs/) uses `ReadModelScenario` to discover each C# style and drive the same histories in memory: product joins before children, a child-only last event, duplicate adds, quantity adjustment, removal, and cancellation. Additional specs cover placement, shipment, and a rename after the line exists. A direct reducer spec checks an explicit occurrence time and preservation of prior state. diff --git a/Chronicle/Quickstart/AspNetCore/README.md b/Chronicle/Quickstart/AspNetCore/README.md index 0d954620..e6735405 100644 --- a/Chronicle/Quickstart/AspNetCore/README.md +++ b/Chronicle/Quickstart/AspNetCore/README.md @@ -59,7 +59,7 @@ dotnet build AspNetCore.csproj --configuration Debug The read-model collection names and the book's self-reducing transitions are covered by specs in `Common.AspNetCore.Specs`: ```bash -dotnet test ../Common.AspNetCore.Specs/Common.AspNetCore.Specs.csproj +dotnet run --project ../Common.AspNetCore.Specs/Common.AspNetCore.Specs.csproj -- ``` ## Send a couple of events diff --git a/Chronicle/Quickstart/Common.AspNetCore.Specs/Common.AspNetCore.Specs.csproj b/Chronicle/Quickstart/Common.AspNetCore.Specs/Common.AspNetCore.Specs.csproj index 5ed913b6..acbdb82d 100644 --- a/Chronicle/Quickstart/Common.AspNetCore.Specs/Common.AspNetCore.Specs.csproj +++ b/Chronicle/Quickstart/Common.AspNetCore.Specs/Common.AspNetCore.Specs.csproj @@ -4,15 +4,11 @@ false + Exe - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Chronicle/Quickstart/Common.AspNetCore.Specs/when_configuring_mongodb.cs b/Chronicle/Quickstart/Common.AspNetCore.Specs/when_configuring_mongodb.cs index b83c1885..c41e3e6d 100644 --- a/Chronicle/Quickstart/Common.AspNetCore.Specs/when_configuring_mongodb.cs +++ b/Chronicle/Quickstart/Common.AspNetCore.Specs/when_configuring_mongodb.cs @@ -13,6 +13,8 @@ namespace Quickstart.Common.AspNetCore.Specs; public class when_configuring_mongodb { + static when_configuring_mongodb() => MongoDBDefaults.Initialize(); + [Fact] public void should_use_chronicles_read_model_collection_names() { @@ -30,8 +32,6 @@ public void should_use_chronicles_read_model_collection_names() [Fact] public void should_keep_read_model_property_names() { - MongoDBDefaults.Initialize(); - Assert.Equal(nameof(Book.Title), BsonClassMap.LookupClassMap(typeof(Book)).GetMemberMap(nameof(Book.Title)).ElementName); } } diff --git a/Directory.Packages.props b/Directory.Packages.props index a8428220..90b9639e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,8 +4,8 @@ true - - + + @@ -17,15 +17,14 @@ - - - - - - + + + + + @@ -38,14 +37,17 @@ - + - + + + + diff --git a/GeneratedValues/Shop/README.md b/GeneratedValues/Shop/README.md index 3803b385..a4421a35 100644 --- a/GeneratedValues/Shop/README.md +++ b/GeneratedValues/Shop/README.md @@ -123,7 +123,7 @@ Do not edit files under `Rendered/`; change `shop.play` and render again. The re ### 3. Build and specify the hand-written shop ```bash -dotnet test Shop.Specs/Shop.Specs.csproj --configuration Debug +dotnet run --project Shop.Specs/Shop.Specs.csproj --configuration Debug -- ``` ```text diff --git a/GeneratedValues/Shop/Shop.Specs/Shop.Specs.csproj b/GeneratedValues/Shop/Shop.Specs/Shop.Specs.csproj index 1127bda0..0474e90d 100644 --- a/GeneratedValues/Shop/Shop.Specs/Shop.Specs.csproj +++ b/GeneratedValues/Shop/Shop.Specs/Shop.Specs.csproj @@ -1,7 +1,7 @@ false - true + Exe Shop.Specs $(NoWarn);CA1001;CA1515;IDE1006;SA1134 @@ -13,13 +13,8 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/LearningPath/05-Testing/README.md b/LearningPath/05-Testing/README.md index a1a91665..81295b17 100644 --- a/LearningPath/05-Testing/README.md +++ b/LearningPath/05-Testing/README.md @@ -32,7 +32,7 @@ Use a plain call for a pure decision. Add a scenario when you need proof that th You need only the [.NET 10 SDK](https://dotnet.microsoft.com/download). From the repository root: ```bash -dotnet test LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj --configuration Debug +dotnet run --project LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj --configuration Debug -- ``` The runner builds the cart and specs, then reports passed facts with no failures or skips. It does not start the sample API or contact the server configured in `appsettings.json`. Each scenario gets its own state and each spec uses a fresh cart id. diff --git a/LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj b/LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj index 3e9e250b..777fb146 100644 --- a/LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj +++ b/LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj @@ -1,7 +1,7 @@ LearningPath.Testing.Carts - true + Exe false @@ -12,10 +12,8 @@ - - + - - + diff --git a/LearningPath/07-ValidationAndAuthorization/README.md b/LearningPath/07-ValidationAndAuthorization/README.md index e3faaa9c..04aa9a21 100644 --- a/LearningPath/07-ValidationAndAuthorization/README.md +++ b/LearningPath/07-ValidationAndAuthorization/README.md @@ -36,7 +36,7 @@ Authorization runs **before validation**, then Arc resolves handler inputs and c You need the [.NET 10 SDK](https://dotnet.microsoft.com/download). From the repository root: ```bash -dotnet test LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj --configuration Debug +dotnet run --project LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj --configuration Debug -- ``` The specs need no Docker, HTTP host, or Chronicle server. The package versions are pinned in [`Directory.Packages.props`](../../Directory.Packages.props). diff --git a/LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj b/LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj index b5148692..f7932096 100644 --- a/LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj +++ b/LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj @@ -1,7 +1,7 @@ LearningPath.ValidationAndAuthorization.Carts - true + Exe false @@ -12,10 +12,8 @@ - - + - - + diff --git a/LearningPath/08-Production/Production.Specs/Production.Specs.csproj b/LearningPath/08-Production/Production.Specs/Production.Specs.csproj index 2dccf1fb..f651a1ba 100644 --- a/LearningPath/08-Production/Production.Specs/Production.Specs.csproj +++ b/LearningPath/08-Production/Production.Specs/Production.Specs.csproj @@ -1,7 +1,7 @@ LearningPath.Production.Carts - true + Exe false @@ -12,10 +12,8 @@ - - + - - + diff --git a/LearningPath/08-Production/README.md b/LearningPath/08-Production/README.md index e566542e..06cc7908 100644 --- a/LearningPath/08-Production/README.md +++ b/LearningPath/08-Production/README.md @@ -183,7 +183,7 @@ unset customer_token visitor_token CHRONICLE_CERTIFICATE_PASSWORD ENCRYPTION_CER From the repository root, run the copied specs without Docker: ```bash -dotnet test LearningPath/08-Production/Production.Specs/Production.Specs.csproj --configuration Debug +dotnet run --project LearningPath/08-Production/Production.Specs/Production.Specs.csproj --configuration Debug -- ``` This is the last step of the path. To see how event shapes change over time, go back to [step 06 — Changing events over time](../06-EventVersioning/README.md). The [Capstone](../../Capstone/README.md) adds a React screen; this step deliberately stays backend-only. diff --git a/LearningPath/README.md b/LearningPath/README.md index e0af9e8c..b7c4ecc8 100644 --- a/LearningPath/README.md +++ b/LearningPath/README.md @@ -79,12 +79,12 @@ dotnet build LearningPath/02-FirstProjection/FirstProjection.csproj dotnet build LearningPath/03-CommandsAndQueries/CommandsAndQueries.csproj dotnet build LearningPath/04-Reactors/Reactors.csproj dotnet build LearningPath/05-Testing/Testing.csproj -dotnet test LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj --configuration Debug +dotnet run --project LearningPath/05-Testing/Testing.Specs/Testing.Specs.csproj --configuration Debug -- dotnet build LearningPath/06-EventVersioning/EventVersioning.csproj dotnet build LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.csproj -dotnet test LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj --configuration Debug +dotnet run --project LearningPath/07-ValidationAndAuthorization/ValidationAndAuthorization.Specs/ValidationAndAuthorization.Specs.csproj --configuration Debug -- dotnet build LearningPath/08-Production/Production.csproj -dotnet test LearningPath/08-Production/Production.Specs/Production.Specs.csproj --configuration Debug +dotnet run --project LearningPath/08-Production/Production.Specs/Production.Specs.csproj --configuration Debug -- ``` Building and running the specs do not need Chronicle. Running the step applications does. diff --git a/Library/Composition/Composition.csproj b/Library/Composition/Composition.csproj index a8deab0d..7ce61e91 100644 --- a/Library/Composition/Composition.csproj +++ b/Library/Composition/Composition.csproj @@ -9,7 +9,6 @@ - diff --git a/Library/Composition/Program.cs b/Library/Composition/Program.cs index ed07346b..ed5aa67a 100644 --- a/Library/Composition/Program.cs +++ b/Library/Composition/Program.cs @@ -34,6 +34,7 @@ // When no configure callback is supplied AddCratisChronicle uses the development image (embedded MongoDB). // For any explicit database choice we supply a callback to switch to the production image. IResourceBuilder chronicle; +IResourceBuilder? readModels = null; var vaultEndpoint = vault.GetEndpoint("http"); if (string.Equals(databaseType, "postgresql", StringComparison.Ordinal)) @@ -63,17 +64,19 @@ } else { - // mongodb — the latest-development image no longer bundles MongoDB, so spin up a dedicated container - var mongo = builder.AddMongoDB("mongodb", port: 27017).AddDatabase("chronicle-db", databaseName: "chronicle"); + // mongodb — one MongoDB holds both Chronicle's storage and the applications' read models. + // AddCratisChronicleMongoDB starts it as the single-node replica set Chronicle needs for transactions and change streams. + // The development image bundles its own MongoDB, so select the slim development image, which does not. + readModels = builder.AddCratisChronicleMongoDB(); chronicle = builder.AddCratisChronicle("chronicle", c => c - .WithMongoDB(mongo) - .WithHashiCorpVault(vaultEndpoint, "root")); + .WithMongoDB(readModels) + .WithHashiCorpVault(vaultEndpoint, "root")) + .WithImageTag(ChronicleContainerImageTags.DevelopmentSlimTag); } -// Pin to stable host ports. Management (workbench) runs on 8080, gRPC on 35000. +// Pin Chronicle to a stable host port. Port 35000 serves gRPC and, over HTTP/1.1, the Workbench. chronicle .WaitFor(vault) - .WithEndpoint("management", endpoint => endpoint.Port = 8080) .WithEndpoint("grpc", endpoint => endpoint.Port = 35000); // Keycloak for Lending - pre-seeded with librarian and borrower users @@ -105,12 +108,22 @@ .WithEnvironment("Cratis__Chronicle__DefaultSinkTypeId", sinkTypeId) .WaitFor(chronicle); +if (readModels is not null) +{ + lending.WithEnvironment("Cratis__MongoDB__Server", readModels); +} + // Members backend - connected to Chronicle; projection sink type flows from DATABASE_TYPE var members = builder.AddProject("members") .WithReference(chronicle) .WithEnvironment("Cratis__Chronicle__DefaultSinkTypeId", sinkTypeId) .WaitFor(chronicle); +if (readModels is not null) +{ + members.WithEnvironment("Cratis__MongoDB__Server", readModels); +} + // Lending frontend (Vite dev server via yarn, port 9000) var lendingFrontend = builder.AddViteApp("lending-frontend", "../Lending") .WithYarn() diff --git a/Library/Lending/Lending.csproj b/Library/Lending/Lending.csproj index ab6c641b..98587802 100644 --- a/Library/Lending/Lending.csproj +++ b/Library/Lending/Lending.csproj @@ -25,29 +25,25 @@ - - - + + + - true false + false - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - + diff --git a/Library/Lending/Program.cs b/Library/Lending/Program.cs index bf8a8ede..5ac8224f 100644 --- a/Library/Lending/Program.cs +++ b/Library/Lending/Program.cs @@ -5,6 +5,13 @@ using System.Reflection; using AspNetCoreArcBuilderExtensions = Microsoft.AspNetCore.Builder.ArcBuilderExtensions; +#if DEBUG +if (args.FirstOrDefault() == "--specifications") +{ + return await Xunit.Runner.InProc.SystemConsole.ConsoleRunner.Run(args[1..]); +} +#endif + // Force invariant culture for the Backend CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; @@ -55,3 +62,4 @@ app.MapFallbackToFile("/index.html"); await app.RunAsync(); +return 0; diff --git a/Library/Members/Members.csproj b/Library/Members/Members.csproj index c8435ea4..c4c94624 100644 --- a/Library/Members/Members.csproj +++ b/Library/Members/Members.csproj @@ -24,28 +24,24 @@ - - - + + + - true false + false - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - + diff --git a/Library/Members/Program.cs b/Library/Members/Program.cs index 4324e49b..035cdbc3 100644 --- a/Library/Members/Program.cs +++ b/Library/Members/Program.cs @@ -4,6 +4,13 @@ using System.Globalization; using AspNetCoreArcBuilderExtensions = Microsoft.AspNetCore.Builder.ArcBuilderExtensions; +#if DEBUG +if (args.FirstOrDefault() == "--specifications") +{ + return await Xunit.Runner.InProc.SystemConsole.ConsoleRunner.Run(args[1..]); +} +#endif + // Force invariant culture for the Backend CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; @@ -43,3 +50,4 @@ app.MapFallbackToFile("/index.html"); await app.RunAsync(); +return 0; diff --git a/Library/README.md b/Library/README.md index 4651a25c..17524fa2 100644 --- a/Library/README.md +++ b/Library/README.md @@ -32,7 +32,7 @@ The Aspire dashboard opens automatically. When all resources are healthy, use th | Aspire dashboard | | | Lending application | | | Members application | | -| Chronicle Workbench | | +| Chronicle Workbench | | The AppHost starts MongoDB, Chronicle, Vault, two Keycloak realms, both ASP.NET Core backends, both Vite development servers, and the AuthProxy entry points. The credentials below are local sample data only: @@ -123,7 +123,8 @@ From the Samples repository root: ```bash dotnet build Library/Library.slnx --configuration Debug -dotnet test Library/Library.slnx --configuration Debug --no-build +dotnet run --project Library/Lending/Lending.csproj --configuration Debug --no-build -- --specifications -class "*.for_*" +dotnet run --project Library/Members/Members.csproj --configuration Debug --no-build -- --specifications dotnet build Library/Library.slnx --configuration Release -p:CratisProxiesOutputPath= yarn --cwd Library/Lending build yarn --cwd Library/Members build @@ -136,4 +137,4 @@ yarn --cwd Library/Members build - Open Chronicle Workbench and follow `AuthorRegistered` or `BookAddedToInventory` from event to projection. - Compare `Registration.cs`, its generated `RegisterAuthor.ts` proxy, and `AddAuthor.tsx` to see the full-stack type flow. - Follow the static `Reduce` overloads on [`BookAvailability`](./Lending/Lending/Reserving/Reserving.cs) and [`BorrowedBook`](./Members/BorrowedBooks/BorrowedBooks.cs), then inspect the borrowing event contracts on each side. These pure folds are self-reducing read models; keep a separate `IReducerFor` for injected services, replay-instance behavior, or a separate model contract. Arc excludes the methods taking their own model type from query discovery; intended query methods remain on the same record. -- Run the colocated specifications from this directory with `dotnet test Library.slnx`. +- The specifications live beside the code in each app. Run them from this directory with the Debug-built app, `dotnet run --project /.csproj --configuration Debug --no-build -- --specifications`. Lending's `-class "*.for_*"` filter selects its in-process specs; running its full suite also requires Chronicle and MongoDB. diff --git a/Samples.slnx b/Samples.slnx index 37488447..e2d37472 100644 --- a/Samples.slnx +++ b/Samples.slnx @@ -7,6 +7,7 @@ + From e190f2af9e53f9e7d79b032654a620715afc63a3 Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 29 Oct 2026 08:05:41 +0100 Subject: [PATCH 2/3] Pin MongoDB.Driver 3.13.0 and the v3 Chronicle integration package Chronicle.Testing 19.111.0 needs MongoDB.Driver 3.13.0 or later, and Library Lending and Members reference Cratis.Chronicle.XUnit.Integration.v3, which had no central version. --- Directory.Packages.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index cddaf180..0e6d9736 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,6 +25,7 @@ + @@ -36,7 +37,7 @@ - + From f29497f55d92eb80717be63a9ab42eac373b9b1e Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 29 Oct 2026 08:16:57 +0100 Subject: [PATCH 3/3] Indent the Arc.React.Specs entry in Samples.slnx --- Samples.slnx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples.slnx b/Samples.slnx index e2d37472..9868f67c 100644 --- a/Samples.slnx +++ b/Samples.slnx @@ -7,7 +7,7 @@ - +