@juangcarmona/lighthouse 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +8 -2
  2. package/assets/dotnet-template/Directory.Build.props +22 -0
  3. package/assets/dotnet-template/Directory.Packages.props +16 -0
  4. package/assets/dotnet-template/Lighthouse.Analyzers/BackendContractAnalyzer.cs +72 -0
  5. package/assets/dotnet-template/Lighthouse.Analyzers/Lighthouse.Analyzers.csproj +12 -0
  6. package/assets/dotnet-template/Lighthouse.Analyzers/packages.lock.json +121 -0
  7. package/assets/dotnet-template/Lighthouse.Cqrs/Handlers.cs +11 -0
  8. package/assets/dotnet-template/Lighthouse.Cqrs/Lighthouse.Cqrs.csproj +5 -0
  9. package/assets/dotnet-template/Lighthouse.Cqrs/packages.lock.json +6 -0
  10. package/assets/dotnet-template/Lighthouse.slnx +17 -0
  11. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/CatalogModule.cs +24 -0
  12. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/CreateEntry.cs +47 -0
  13. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/EntryStore.cs +17 -0
  14. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/GetEntry.cs +30 -0
  15. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Lighthouse.Example.Modules.Catalog.csproj +12 -0
  16. package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/packages.lock.json +10 -0
  17. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Lighthouse.Example.Api.csproj +6 -0
  18. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Program.cs +25 -0
  19. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Properties/launchSettings.json +10 -0
  20. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/packages.lock.json +86 -0
  21. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Lighthouse.Example.AppHost.csproj +13 -0
  22. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Program.cs +3 -0
  23. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Properties/launchSettings.json +15 -0
  24. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/aspire.config.json +5 -0
  25. package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/packages.lock.json +689 -0
  26. package/assets/dotnet-template/global.json +6 -0
  27. package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/BackendContractTests.cs +102 -0
  28. package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/Lighthouse.Analyzers.Tests.csproj +9 -0
  29. package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/packages.lock.json +125 -0
  30. package/assets/dotnet-template/tests/Lighthouse.Example.Tests/CatalogTests.cs +111 -0
  31. package/assets/dotnet-template/tests/Lighthouse.Example.Tests/Lighthouse.Example.Tests.csproj +9 -0
  32. package/assets/dotnet-template/tests/Lighthouse.Example.Tests/packages.lock.json +490 -0
  33. package/assets/profiles/catalog.json +1050 -0
  34. package/assets/profiles/dotnet/README.md +32 -0
  35. package/assets/profiles/dotnet/SKILL.md +18 -0
  36. package/assets/profiles/dotnet/adoption.md +39 -0
  37. package/assets/profiles/dotnet/architecture.md +37 -0
  38. package/assets/profiles/dotnet/guardrails.md +32 -0
  39. package/assets/profiles/dotnet/practices.md +36 -0
  40. package/assets/profiles/dotnet/testing-and-development.md +29 -0
  41. package/assets/profiles/dotnet/use-cases.md +29 -0
  42. package/assets/profiles/mobile-expo/README.md +19 -0
  43. package/assets/profiles/mobile-expo/SKILL.md +14 -0
  44. package/assets/profiles/mobile-expo/adoption.md +11 -0
  45. package/assets/profiles/mobile-expo/guardrails.md +14 -0
  46. package/assets/profiles/mobile-expo/testing.md +13 -0
  47. package/assets/profiles/mobile-maui/README.md +19 -0
  48. package/assets/profiles/mobile-maui/SKILL.md +14 -0
  49. package/assets/profiles/mobile-maui/adoption.md +11 -0
  50. package/assets/profiles/mobile-maui/guardrails.md +13 -0
  51. package/assets/profiles/mobile-maui/testing.md +13 -0
  52. package/assets/profiles/pwa-capability/README.md +19 -0
  53. package/assets/profiles/pwa-capability/SKILL.md +14 -0
  54. package/assets/profiles/pwa-capability/adoption.md +9 -0
  55. package/assets/profiles/pwa-capability/guardrails.md +13 -0
  56. package/assets/profiles/pwa-capability/offline.md +11 -0
  57. package/assets/profiles/pwa-capability/testing.md +15 -0
  58. package/assets/profiles/react-web-platform/README.md +24 -0
  59. package/assets/profiles/react-web-platform/SKILL.md +18 -0
  60. package/assets/profiles/react-web-platform/adoption.md +17 -0
  61. package/assets/profiles/react-web-platform/guardrails.md +17 -0
  62. package/assets/profiles/react-web-platform/structure.md +36 -0
  63. package/assets/profiles/react-web-platform/testing.md +15 -0
  64. package/assets/profiles/web-bff/README.md +22 -0
  65. package/assets/profiles/web-bff/SKILL.md +16 -0
  66. package/assets/profiles/web-bff/adoption.md +9 -0
  67. package/assets/profiles/web-bff/security.md +16 -0
  68. package/assets/profiles/web-bff/testing.md +14 -0
  69. package/assets/schemas/lighthouse-manifest.schema.json +10 -7
  70. package/assets/schemas/profile-catalog.schema.json +329 -0
  71. package/package.json +5 -2
  72. package/src/create-dotnet.mjs +112 -0
  73. package/src/lighthouse.mjs +109 -14
  74. package/src/profiles.mjs +206 -0
  75. package/src/verify-dotnet.mjs +85 -0
package/README.md CHANGED
@@ -6,11 +6,15 @@ This package is the core Lighthouse harness. It provides the `lighthouse` comman
6
6
 
7
7
  Install the exact Lighthouse package artifact selected for the release and run `lighthouse init --name <product>` in a new repository. Add `--profile visual-theme` when the project wants the Lighthouse UI contract. Lighthouse creates the manifest, the `src/`, `docs/`, `infra/` and `scripts/` boundaries, the repository guardrails and the generated `LIGHTHOUSE.md`. Inspect the plan and projection, then run `lighthouse verify` before adding project-specific policy or exceptions.
8
8
 
9
+ For a Lighthouse-owned .NET backend, run `lighthouse create-dotnet <empty-target> --name <PascalCaseProduct>`. The creator seeds the modular backend, Aspire AppHost, CQRS/analyzer sources, tests, profile guidance and deterministic verifier into a new or empty directory. It refuses populated targets and never contacts a remote repository. Run `dotnet restore <Product>.slnx`, then `npm run verify` in the generated product.
10
+
11
+ The manifest can declare `application.shape` and `application.stacks` when the project has enough information to select compatible profiles. `lighthouse profiles --format json` reports selected profiles, applicability, owned surfaces and verification layers without changing the repository.
12
+
9
13
  Use `lighthouse reconcile` to preview missing Lighthouse-managed paths in an existing adoption. It does not write by default; add `--apply` only after reviewing the plan. Existing files whose content differs from the generated projection are conflicts and are never overwritten by reconciliation.
10
14
 
11
15
  Use `lighthouse upgrade` to preview a versioned foundation update. The command is dry-run by default, accepts `--scope harness`, `--scope sdlc` or `--scope all`, preserves customized projections and stops when ownership evidence is missing. Add `--apply` only after reviewing the plan, then run `lighthouse verify`.
12
16
 
13
- The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes, `DESIGN.md` and its agent skill. Selecting `visual-theme` projects the guide and skill into the adopter and enables its deterministic guardrail. Stack profiles own their source templates, guidance and checks.
17
+ The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes, `DESIGN.md` and its agent skill. Selecting `visual-theme` projects the guide and skill into the adopter and enables its deterministic guardrail. Stack profiles own their source templates, guidance and checks; the .NET creator is the packaged entry point for the selected backend profile.
14
18
 
15
19
  ProductShape, ArcShape and OpenSpec keep their own files and commands. Add them through their supported integration routes when the project needs them, and keep their versions and managed paths visible in the project configuration.
16
20
 
@@ -38,7 +42,7 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
38
42
 
39
43
  ## Current command scope
40
44
 
41
- The command supports `init`, `authorities`, `reconcile`, `upgrade`, `capabilities` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
45
+ The command supports `init`, `create-dotnet`, `authorities`, `reconcile`, `upgrade`, `capabilities` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
42
46
 
43
47
  Initialization installs `.husky/commit-msg`, `.husky/pre-push`, `scripts/lighthouse-guard.mjs` and `.github/workflows/lighthouse.yml` when those paths are free. Existing files remain project-owned and are reported as conflicts. Reconciliation can add missing managed paths after adoption, but it does not overwrite a path that has drifted or belongs to another tool.
44
48
 
@@ -52,6 +56,8 @@ The package does not contain the repository gallery, provider-owned integrations
52
56
 
53
57
  The CLI package owns a versioned catalog under `assets/guardrails/`; it describes the guardrails already present in the Lighthouse baseline and their applicability, level, kind, verification method and repair hint.
54
58
 
59
+ The profile catalog under `assets/profiles/` describes the application shapes, stacks, owned surfaces and verification layers for each supported profile. A layer can be required, recommended or optional independently of whether its evidence is hard, soft, deterministic, semantic or human.
60
+
55
61
  `level` and `kind` are independent: `level` is `required`, `recommended` or `optional`, while `kind` is `hard` or `soft`; neither dimension selects the verification method.
56
62
 
57
63
  `lighthouse verify` keeps its existing human-readable success and failure output; `lighthouse verify --format json` exposes the same run for agents and CI without changing the default route.
@@ -0,0 +1,22 @@
1
+ <Project>
2
+ <PropertyGroup>
3
+ <TargetFramework>net10.0</TargetFramework>
4
+ <LangVersion>14.0</LangVersion>
5
+ <Nullable>enable</Nullable>
6
+ <ImplicitUsings>enable</ImplicitUsings>
7
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8
+ <EnableNETAnalyzers>true</EnableNETAnalyzers>
9
+ <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
10
+ <AnalysisLevel>10.0-recommended</AnalysisLevel>
11
+ <NuGetAudit>true</NuGetAudit>
12
+ <NuGetAuditMode>all</NuGetAuditMode>
13
+ <NuGetAuditLevel>low</NuGetAuditLevel>
14
+ <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
15
+ <IsPackable>false</IsPackable>
16
+ <Authors>juangcarmona</Authors>
17
+ <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
18
+ </PropertyGroup>
19
+ <ItemGroup>
20
+ <CompilerVisibleProperty Include="LighthouseModule" />
21
+ </ItemGroup>
22
+ </Project>
@@ -0,0 +1,16 @@
1
+ <Project>
2
+ <PropertyGroup>
3
+ <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4
+ </PropertyGroup>
5
+ <ItemGroup>
6
+ <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
7
+ <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
8
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
9
+ <PackageVersion Include="xunit" Version="2.9.3" />
10
+ <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
11
+ <PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.13" />
12
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.13" />
13
+ <PackageVersion Include="Aspire.Hosting.AppHost" Version="13.6.1" />
14
+ <PackageVersion Include="MessagePack" Version="2.5.303" />
15
+ </ItemGroup>
16
+ </Project>
@@ -0,0 +1,72 @@
1
+ using System.Collections.Immutable;
2
+ using Microsoft.CodeAnalysis;
3
+ using Microsoft.CodeAnalysis.Diagnostics;
4
+
5
+ namespace JuanGCarmona.Lighthouse.Analyzers;
6
+
7
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
8
+ public sealed class BackendContractAnalyzer : DiagnosticAnalyzer
9
+ {
10
+ private static readonly DiagnosticDescriptor HandlerContract = new(
11
+ "LH1001", "Keep CQRS handlers internal and sealed",
12
+ "Handler '{0}' must be declared internal sealed", "Lighthouse.Architecture",
13
+ DiagnosticSeverity.Error, isEnabledByDefault: true,
14
+ description: "A module exposes its API and contracts; handlers remain implementation details.");
15
+
16
+ private static readonly DiagnosticDescriptor ModuleReference = new(
17
+ "LH1002", "Use contracts across module boundaries",
18
+ "Module '{0}' references implementation assembly '{1}'; depend on its Contracts assembly",
19
+ "Lighthouse.Architecture", DiagnosticSeverity.Error, isEnabledByDefault: true,
20
+ description: "Cross-module dependencies must terminate at a separately named Contracts assembly.",
21
+ customTags: new[] { WellKnownDiagnosticTags.CompilationEnd });
22
+
23
+ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
24
+ ImmutableArray.Create(HandlerContract, ModuleReference);
25
+
26
+ public override void Initialize(AnalysisContext context)
27
+ {
28
+ context.EnableConcurrentExecution();
29
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
30
+ context.RegisterCompilationStartAction(start =>
31
+ {
32
+ if (!start.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(
33
+ "build_property.LighthouseModule", out var enabled)
34
+ || !string.Equals(enabled, "true", StringComparison.OrdinalIgnoreCase))
35
+ return;
36
+
37
+ start.RegisterSymbolAction(CheckHandler, SymbolKind.NamedType);
38
+ start.RegisterCompilationEndAction(CheckDependencies);
39
+ });
40
+ }
41
+
42
+ private static void CheckHandler(SymbolAnalysisContext context)
43
+ {
44
+ var type = (INamedTypeSymbol)context.Symbol;
45
+ if (type.TypeKind != TypeKind.Class || !type.AllInterfaces.Any(IsHandler)) return;
46
+ if (type.DeclaredAccessibility == Accessibility.Internal && type.IsSealed) return;
47
+ var location = type.Locations.FirstOrDefault(x => x.IsInSource);
48
+ if (location is not null)
49
+ context.ReportDiagnostic(Diagnostic.Create(HandlerContract, location, type.Name));
50
+ }
51
+
52
+ private static bool IsHandler(INamedTypeSymbol contract) =>
53
+ contract.ContainingNamespace.ToDisplayString() == "JuanGCarmona.Lighthouse.Cqrs"
54
+ && contract.Arity == 2
55
+ && (contract.Name == "ICommandHandler" || contract.Name == "IQueryHandler");
56
+
57
+ private static void CheckDependencies(CompilationAnalysisContext context)
58
+ {
59
+ var name = context.Compilation.AssemblyName;
60
+ const string separator = ".Modules.";
61
+ var boundary = name?.IndexOf(separator, StringComparison.Ordinal) ?? -1;
62
+ if (name is null || boundary < 0 || name.EndsWith(".Contracts", StringComparison.Ordinal)) return;
63
+ var family = name.Substring(0, boundary + separator.Length);
64
+ foreach (var dependency in context.Compilation.SourceModule.ReferencedAssemblySymbols)
65
+ {
66
+ if (dependency.Name.StartsWith(family, StringComparison.Ordinal)
67
+ && dependency.Name != name
68
+ && !dependency.Name.EndsWith(".Contracts", StringComparison.Ordinal))
69
+ context.ReportDiagnostic(Diagnostic.Create(ModuleReference, Location.None, name, dependency.Name));
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,12 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <TargetFramework>netstandard2.0</TargetFramework>
4
+ <RootNamespace>JuanGCarmona.Lighthouse.Analyzers</RootNamespace>
5
+ <PackageId>JuanGCarmona.Lighthouse.Analyzers</PackageId>
6
+ <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
7
+ </PropertyGroup>
8
+ <ItemGroup>
9
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
10
+ <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
11
+ </ItemGroup>
12
+ </Project>
@@ -0,0 +1,121 @@
1
+ {
2
+ "version": 2,
3
+ "dependencies": {
4
+ ".NETStandard,Version=v2.0": {
5
+ "Microsoft.CodeAnalysis.Analyzers": {
6
+ "type": "Direct",
7
+ "requested": "[3.11.0, )",
8
+ "resolved": "3.11.0",
9
+ "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg=="
10
+ },
11
+ "Microsoft.CodeAnalysis.CSharp": {
12
+ "type": "Direct",
13
+ "requested": "[5.0.0, )",
14
+ "resolved": "5.0.0",
15
+ "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==",
16
+ "dependencies": {
17
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
18
+ "Microsoft.CodeAnalysis.Common": "[5.0.0]",
19
+ "System.Buffers": "4.6.0",
20
+ "System.Collections.Immutable": "9.0.0",
21
+ "System.Memory": "4.6.0",
22
+ "System.Numerics.Vectors": "4.6.0",
23
+ "System.Reflection.Metadata": "9.0.0",
24
+ "System.Runtime.CompilerServices.Unsafe": "6.1.0",
25
+ "System.Text.Encoding.CodePages": "8.0.0",
26
+ "System.Threading.Tasks.Extensions": "4.6.0"
27
+ }
28
+ },
29
+ "NETStandard.Library": {
30
+ "type": "Direct",
31
+ "requested": "[2.0.3, )",
32
+ "resolved": "2.0.3",
33
+ "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
34
+ "dependencies": {
35
+ "Microsoft.NETCore.Platforms": "1.1.0"
36
+ }
37
+ },
38
+ "Microsoft.CodeAnalysis.Common": {
39
+ "type": "Transitive",
40
+ "resolved": "5.0.0",
41
+ "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==",
42
+ "dependencies": {
43
+ "Microsoft.CodeAnalysis.Analyzers": "3.11.0",
44
+ "System.Buffers": "4.6.0",
45
+ "System.Collections.Immutable": "9.0.0",
46
+ "System.Memory": "4.6.0",
47
+ "System.Numerics.Vectors": "4.6.0",
48
+ "System.Reflection.Metadata": "9.0.0",
49
+ "System.Runtime.CompilerServices.Unsafe": "6.1.0",
50
+ "System.Text.Encoding.CodePages": "8.0.0",
51
+ "System.Threading.Tasks.Extensions": "4.6.0"
52
+ }
53
+ },
54
+ "Microsoft.NETCore.Platforms": {
55
+ "type": "Transitive",
56
+ "resolved": "1.1.0",
57
+ "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
58
+ },
59
+ "System.Buffers": {
60
+ "type": "Transitive",
61
+ "resolved": "4.6.0",
62
+ "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
63
+ },
64
+ "System.Collections.Immutable": {
65
+ "type": "Transitive",
66
+ "resolved": "9.0.0",
67
+ "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==",
68
+ "dependencies": {
69
+ "System.Memory": "4.5.5",
70
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0"
71
+ }
72
+ },
73
+ "System.Memory": {
74
+ "type": "Transitive",
75
+ "resolved": "4.6.0",
76
+ "contentHash": "OEkbBQoklHngJ8UD8ez2AERSk2g+/qpAaSWWCBFbpH727HxDq5ydVkuncBaKcKfwRqXGWx64dS6G1SUScMsitg==",
77
+ "dependencies": {
78
+ "System.Buffers": "4.6.0",
79
+ "System.Numerics.Vectors": "4.6.0",
80
+ "System.Runtime.CompilerServices.Unsafe": "6.1.0"
81
+ }
82
+ },
83
+ "System.Numerics.Vectors": {
84
+ "type": "Transitive",
85
+ "resolved": "4.6.0",
86
+ "contentHash": "t+SoieZsRuEyiw/J+qXUbolyO219tKQQI0+2/YI+Qv7YdGValA6WiuokrNKqjrTNsy5ABWU11bdKOzUdheteXg=="
87
+ },
88
+ "System.Reflection.Metadata": {
89
+ "type": "Transitive",
90
+ "resolved": "9.0.0",
91
+ "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==",
92
+ "dependencies": {
93
+ "System.Collections.Immutable": "9.0.0",
94
+ "System.Memory": "4.5.5"
95
+ }
96
+ },
97
+ "System.Runtime.CompilerServices.Unsafe": {
98
+ "type": "Transitive",
99
+ "resolved": "6.1.0",
100
+ "contentHash": "5o/HZxx6RVqYlhKSq8/zronDkALJZUT2Vz0hx43f0gwe8mwlM0y2nYlqdBwLMzr262Bwvpikeb/yEwkAa5PADg=="
101
+ },
102
+ "System.Text.Encoding.CodePages": {
103
+ "type": "Transitive",
104
+ "resolved": "8.0.0",
105
+ "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==",
106
+ "dependencies": {
107
+ "System.Memory": "4.5.5",
108
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0"
109
+ }
110
+ },
111
+ "System.Threading.Tasks.Extensions": {
112
+ "type": "Transitive",
113
+ "resolved": "4.6.0",
114
+ "contentHash": "I5G6Y8jb0xRtGUC9Lahy7FUvlYlnGMMkbuKAQBy8Jb7Y6Yn8OlBEiUOY0PqZ0hy6Ua8poVA1ui1tAIiXNxGdsg==",
115
+ "dependencies": {
116
+ "System.Runtime.CompilerServices.Unsafe": "6.1.0"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,11 @@
1
+ namespace JuanGCarmona.Lighthouse.Cqrs;
2
+
3
+ public interface ICommandHandler<in TCommand, TResponse>
4
+ {
5
+ Task<TResponse> Handle(TCommand command, CancellationToken cancellationToken);
6
+ }
7
+
8
+ public interface IQueryHandler<in TQuery, TResponse>
9
+ {
10
+ Task<TResponse> Handle(TQuery query, CancellationToken cancellationToken);
11
+ }
@@ -0,0 +1,5 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <RootNamespace>JuanGCarmona.Lighthouse.Cqrs</RootNamespace>
4
+ </PropertyGroup>
5
+ </Project>
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 2,
3
+ "dependencies": {
4
+ "net10.0": {}
5
+ }
6
+ }
@@ -0,0 +1,17 @@
1
+ <Solution>
2
+ <Folder Name="/examples/src/modules/">
3
+ <Project Path="examples/src/modules/Lighthouse.Example.Modules.Catalog/Lighthouse.Example.Modules.Catalog.csproj" />
4
+ </Folder>
5
+ <Folder Name="/examples/src/root/">
6
+ <Project Path="examples/src/root/Lighthouse.Example.Api/Lighthouse.Example.Api.csproj" />
7
+ <Project Path="examples/src/root/Lighthouse.Example.AppHost/Lighthouse.Example.AppHost.csproj" />
8
+ </Folder>
9
+ <Folder Name="/tests/">
10
+ <Project Path="tests/Lighthouse.Analyzers.Tests/Lighthouse.Analyzers.Tests.csproj" />
11
+ <Project Path="tests/Lighthouse.Example.Tests/Lighthouse.Example.Tests.csproj" />
12
+ </Folder>
13
+ <Folder Name="/Lighthouse/">
14
+ <Project Path="Lighthouse.Analyzers/Lighthouse.Analyzers.csproj" />
15
+ <Project Path="Lighthouse.Cqrs/Lighthouse.Cqrs.csproj" />
16
+ </Folder>
17
+ </Solution>
@@ -0,0 +1,24 @@
1
+ using JuanGCarmona.Lighthouse.Cqrs;
2
+ using JuanGCarmona.Lighthouse.Example.Modules.Catalog.Features.Entries;
3
+ using Microsoft.AspNetCore.Routing;
4
+ using Microsoft.Extensions.DependencyInjection;
5
+
6
+ namespace JuanGCarmona.Lighthouse.Example.Modules.Catalog;
7
+
8
+ public static class CatalogModule
9
+ {
10
+ public static IServiceCollection AddCatalog(this IServiceCollection services)
11
+ {
12
+ services.AddSingleton<EntryStore>();
13
+ services.AddScoped<ICommandHandler<CreateEntry.Command, CreateEntry.Outcome>, CreateEntry.Handler>();
14
+ services.AddScoped<IQueryHandler<GetEntry.Query, GetEntry.Response?>, GetEntry.Handler>();
15
+ return services;
16
+ }
17
+
18
+ public static IEndpointRouteBuilder MapCatalog(this IEndpointRouteBuilder endpoints)
19
+ {
20
+ CreateEntry.Map(endpoints);
21
+ GetEntry.Map(endpoints);
22
+ return endpoints;
23
+ }
24
+ }
@@ -0,0 +1,47 @@
1
+ using JuanGCarmona.Lighthouse.Cqrs;
2
+ using Microsoft.AspNetCore.Builder;
3
+ using Microsoft.AspNetCore.Http;
4
+ using Microsoft.AspNetCore.Routing;
5
+
6
+ namespace JuanGCarmona.Lighthouse.Example.Modules.Catalog.Features.Entries;
7
+
8
+ internal static class CreateEntry
9
+ {
10
+ internal static void Map(IEndpointRouteBuilder routes) => routes
11
+ .MapPost("/api/entries", async (Request request, ICommandHandler<Command, Outcome> handler, CancellationToken token) =>
12
+ {
13
+ var outcome = await handler.Handle(new Command(request.Name), token);
14
+ return outcome.Value is { } value
15
+ ? Results.Created($"/api/entries/{value.Id}", value)
16
+ : Results.ValidationProblem(outcome.Errors);
17
+ })
18
+ .RequireAuthorization("catalog.write");
19
+
20
+ internal sealed record Request(string? Name);
21
+ internal sealed record Response(Guid Id, string Name);
22
+ internal sealed record Command(string? Name);
23
+ internal sealed record Outcome(Response? Value, Dictionary<string, string[]> Errors);
24
+
25
+ internal static class Validator
26
+ {
27
+ internal static Dictionary<string, string[]> Validate(Command command)
28
+ {
29
+ var errors = new Dictionary<string, string[]>();
30
+ if (string.IsNullOrWhiteSpace(command.Name) || command.Name.Trim().Length > 80)
31
+ errors["name"] = ["Use a name between 1 and 80 characters."];
32
+ return errors;
33
+ }
34
+ }
35
+
36
+ internal sealed class Handler(EntryStore store) : ICommandHandler<Command, Outcome>
37
+ {
38
+ public Task<Outcome> Handle(Command command, CancellationToken cancellationToken)
39
+ {
40
+ cancellationToken.ThrowIfCancellationRequested();
41
+ var errors = Validator.Validate(command);
42
+ if (errors.Count > 0) return Task.FromResult(new Outcome(null, errors));
43
+ var entry = store.Add(command.Name!.Trim());
44
+ return Task.FromResult(new Outcome(new Response(entry.Id, entry.Name), errors));
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,17 @@
1
+ using System.Collections.Concurrent;
2
+
3
+ namespace JuanGCarmona.Lighthouse.Example.Modules.Catalog.Features.Entries;
4
+
5
+ internal sealed record Entry(Guid Id, string Name);
6
+
7
+ internal sealed class EntryStore
8
+ {
9
+ private readonly ConcurrentDictionary<Guid, Entry> entries = new();
10
+ internal Entry Add(string name)
11
+ {
12
+ var entry = new Entry(Guid.CreateVersion7(), name);
13
+ entries[entry.Id] = entry;
14
+ return entry;
15
+ }
16
+ internal Entry? Find(Guid id) => entries.GetValueOrDefault(id);
17
+ }
@@ -0,0 +1,30 @@
1
+ using JuanGCarmona.Lighthouse.Cqrs;
2
+ using Microsoft.AspNetCore.Builder;
3
+ using Microsoft.AspNetCore.Http;
4
+ using Microsoft.AspNetCore.Routing;
5
+
6
+ namespace JuanGCarmona.Lighthouse.Example.Modules.Catalog.Features.Entries;
7
+
8
+ internal static class GetEntry
9
+ {
10
+ internal static void Map(IEndpointRouteBuilder routes) => routes
11
+ .MapGet("/api/entries/{id:guid}", async (Guid id, IQueryHandler<Query, Response?> handler, CancellationToken token) =>
12
+ {
13
+ var value = await handler.Handle(new Query(id), token);
14
+ return value is null ? Results.NotFound() : Results.Ok(value);
15
+ })
16
+ .RequireAuthorization();
17
+
18
+ internal sealed record Query(Guid Id);
19
+ internal sealed record Response(Guid Id, string Name);
20
+
21
+ internal sealed class Handler(EntryStore store) : IQueryHandler<Query, Response?>
22
+ {
23
+ public Task<Response?> Handle(Query query, CancellationToken cancellationToken)
24
+ {
25
+ cancellationToken.ThrowIfCancellationRequested();
26
+ var value = store.Find(query.Id);
27
+ return Task.FromResult(value is null ? null : new Response(value.Id, value.Name));
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,12 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <AssemblyName>JuanGCarmona.Lighthouse.Example.Modules.Catalog</AssemblyName>
4
+ <RootNamespace>JuanGCarmona.Lighthouse.Example.Modules.Catalog</RootNamespace>
5
+ <LighthouseModule>true</LighthouseModule>
6
+ </PropertyGroup>
7
+ <ItemGroup>
8
+ <FrameworkReference Include="Microsoft.AspNetCore.App" />
9
+ <ProjectReference Include="../../../../Lighthouse.Cqrs/Lighthouse.Cqrs.csproj" />
10
+ <ProjectReference Include="../../../../Lighthouse.Analyzers/Lighthouse.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
11
+ </ItemGroup>
12
+ </Project>
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 2,
3
+ "dependencies": {
4
+ "net10.0": {
5
+ "lighthouse.cqrs": {
6
+ "type": "Project"
7
+ }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,6 @@
1
+ <Project Sdk="Microsoft.NET.Sdk.Web">
2
+ <ItemGroup>
3
+ <ProjectReference Include="../../modules/Lighthouse.Example.Modules.Catalog/Lighthouse.Example.Modules.Catalog.csproj" />
4
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
5
+ </ItemGroup>
6
+ </Project>
@@ -0,0 +1,25 @@
1
+ using JuanGCarmona.Lighthouse.Example.Modules.Catalog;
2
+ using Microsoft.AspNetCore.Authentication.JwtBearer;
3
+
4
+ namespace JuanGCarmona.Lighthouse.Example.Api;
5
+
6
+ public partial class Program
7
+ {
8
+ public static void Main(string[] args)
9
+ {
10
+ var builder = WebApplication.CreateBuilder(args);
11
+ builder.Services.AddCatalog();
12
+ builder.Services.AddProblemDetails();
13
+ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
14
+ .AddJwtBearer(options => builder.Configuration.GetSection("Authentication").Bind(options));
15
+ builder.Services.AddAuthorization(options => options.AddPolicy("catalog.write",
16
+ policy => policy.RequireAuthenticatedUser().RequireClaim("permission", "catalog.write")));
17
+ var app = builder.Build();
18
+ app.UseExceptionHandler();
19
+ app.UseAuthentication();
20
+ app.UseAuthorization();
21
+ app.MapGet("/health", () => Results.Ok(new { status = "ready" }));
22
+ app.MapCatalog();
23
+ app.Run();
24
+ }
25
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "profiles": {
3
+ "http": {
4
+ "commandName": "Project",
5
+ "launchBrowser": false,
6
+ "applicationUrl": "http://127.0.0.1:5097",
7
+ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ "version": 2,
3
+ "dependencies": {
4
+ "net10.0": {
5
+ "Microsoft.AspNetCore.Authentication.JwtBearer": {
6
+ "type": "Direct",
7
+ "requested": "[10.0.13, )",
8
+ "resolved": "10.0.13",
9
+ "contentHash": "cEPhQWsaUKymSdQ+NB2rF4GnWtjofAcRrzr2aGPzbjSFyeDtIlBzrbX1Qeh/OAHjvdwBe05SbsukoSbz27AD6g==",
10
+ "dependencies": {
11
+ "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.19.2"
12
+ }
13
+ },
14
+ "Microsoft.Bcl.Cryptography": {
15
+ "type": "Transitive",
16
+ "resolved": "10.0.2",
17
+ "contentHash": "LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw=="
18
+ },
19
+ "Microsoft.IdentityModel.Abstractions": {
20
+ "type": "Transitive",
21
+ "resolved": "8.19.2",
22
+ "contentHash": "HJbo/lnSfNHUfphPRT910poQc4T2/9+8svFLvzuaYHGAOJ2Tu+oEDqpX0BVP3BJ4OuUM1kylEKyaiX2fCAK3Cw=="
23
+ },
24
+ "Microsoft.IdentityModel.JsonWebTokens": {
25
+ "type": "Transitive",
26
+ "resolved": "8.19.2",
27
+ "contentHash": "ui3fuBT4fs8kdKfBthI4NzLYIBIneEVS8UrL1JVBzAn80UiKmngBBi0BEByE7n/9c+EElcfFlCMFFTqpkBSLNA==",
28
+ "dependencies": {
29
+ "Microsoft.IdentityModel.Tokens": "8.19.2"
30
+ }
31
+ },
32
+ "Microsoft.IdentityModel.Logging": {
33
+ "type": "Transitive",
34
+ "resolved": "8.19.2",
35
+ "contentHash": "r5YLDIxGOnkVJHrqXv/iD1FM1CgGrQOdriXuvuWvTPmKbnGANhEysq9XKmN6IHjf2a+9bAGEpnoRBsAQlvJU5w==",
36
+ "dependencies": {
37
+ "Microsoft.IdentityModel.Abstractions": "8.19.2"
38
+ }
39
+ },
40
+ "Microsoft.IdentityModel.Protocols": {
41
+ "type": "Transitive",
42
+ "resolved": "8.19.2",
43
+ "contentHash": "sGxSsSrZXNmca6D+jHH2rVRyo2nNRd/g4H9CFbPmLLq0xgoH1U0orLWE5minfijw7+zq49tBs7txenbfAErRoQ==",
44
+ "dependencies": {
45
+ "Microsoft.IdentityModel.Tokens": "8.19.2"
46
+ }
47
+ },
48
+ "Microsoft.IdentityModel.Protocols.OpenIdConnect": {
49
+ "type": "Transitive",
50
+ "resolved": "8.19.2",
51
+ "contentHash": "1XOcyY36cVymzE3qKdzKaUEZ4Pzt7ZpSa14JZoPPK1NLFUkQDs85TCqpV6XDo0YjFXj6nVK00AfOHppjghjhtw==",
52
+ "dependencies": {
53
+ "Microsoft.IdentityModel.Protocols": "8.19.2",
54
+ "System.IdentityModel.Tokens.Jwt": "8.19.2"
55
+ }
56
+ },
57
+ "Microsoft.IdentityModel.Tokens": {
58
+ "type": "Transitive",
59
+ "resolved": "8.19.2",
60
+ "contentHash": "GtPC1S02uH1gOO4fQ+zRysIicKmEXaYFP8PIkdJYXqMyruYhopre4ozVHp0XiDSA0+GJvOZH9prxCPvgBMg4Ww==",
61
+ "dependencies": {
62
+ "Microsoft.Bcl.Cryptography": "10.0.2",
63
+ "Microsoft.IdentityModel.Logging": "8.19.2"
64
+ }
65
+ },
66
+ "System.IdentityModel.Tokens.Jwt": {
67
+ "type": "Transitive",
68
+ "resolved": "8.19.2",
69
+ "contentHash": "gqhDC/icByKEutygpr+OFgAmjwTVowyzFjWB8K0q1ww8uFj5a1BQNL+QvijUl9uhq4p8OdDwcAIrjVC+eC9FVA==",
70
+ "dependencies": {
71
+ "Microsoft.IdentityModel.JsonWebTokens": "8.19.2",
72
+ "Microsoft.IdentityModel.Tokens": "8.19.2"
73
+ }
74
+ },
75
+ "JuanGCarmona.Lighthouse.Example.Modules.Catalog": {
76
+ "type": "Project",
77
+ "dependencies": {
78
+ "Lighthouse.Cqrs": "[1.0.0, )"
79
+ }
80
+ },
81
+ "lighthouse.cqrs": {
82
+ "type": "Project"
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,13 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <Sdk Name="Aspire.AppHost.Sdk" Version="13.6.1" />
3
+ <PropertyGroup>
4
+ <OutputType>Exe</OutputType>
5
+ <IsAspireHost>true</IsAspireHost>
6
+ <AspireUseCliBundle>true</AspireUseCliBundle>
7
+ </PropertyGroup>
8
+ <ItemGroup>
9
+ <PackageReference Include="Aspire.Hosting.AppHost" />
10
+ <PackageReference Include="MessagePack" PrivateAssets="all" />
11
+ <ProjectReference Include="../Lighthouse.Example.Api/Lighthouse.Example.Api.csproj" />
12
+ </ItemGroup>
13
+ </Project>
@@ -0,0 +1,3 @@
1
+ var builder = DistributedApplication.CreateBuilder(args);
2
+ builder.AddProject<Projects.Lighthouse_Example_Api>("api");
3
+ builder.Build().Run();