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 187cafb0..0e6d9736 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,7 +17,6 @@
-
@@ -26,6 +25,7 @@
+
@@ -37,8 +37,8 @@
-
-
+
+
@@ -46,6 +46,9 @@
+
+
+
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..9868f67c 100644
--- a/Samples.slnx
+++ b/Samples.slnx
@@ -7,6 +7,7 @@
+