@@ -20,173 +20,18 @@ tools/
2020## Running the Solution
2121
2222``` bash
23- cd tools/AppHost && dotnet run
23+ aspire run
2424```
2525
2626Aspire handles database provisioning, migrations, and seeding automatically. API available at ` https://localhost:7255/scalar/v1 ` .
2727
28- ## Key Patterns
28+ ## Further Reading
2929
30- ### Commands (Create/Update/Delete)
31- Location: ` src/Application/UseCases/{Feature}/Commands/{CommandName}/ `
32-
33- ``` csharp
34- public sealed record CreateHeroCommand (string Name , string Alias ) : IRequest <ErrorOr <Guid >>;
35-
36- internal sealed class CreateHeroCommandHandler (IApplicationDbContext dbContext )
37- : IRequestHandler <CreateHeroCommand , ErrorOr <Guid >>
38- {
39- public async Task <ErrorOr <Guid >> Handle (CreateHeroCommand request , CancellationToken ct )
40- {
41- var hero = Hero .Create (request .Name , request .Alias );
42- await dbContext .Heroes .AddAsync (hero , ct );
43- await dbContext .SaveChangesAsync (ct );
44- return hero .Id .Value ;
45- }
46- }
47-
48- internal sealed class CreateHeroCommandValidator : AbstractValidator <CreateHeroCommand >
49- {
50- public CreateHeroCommandValidator () => RuleFor (v => v .Name ).NotEmpty ();
51- }
52- ```
53-
54- ### Queries (Read)
55- Location: ` src/Application/UseCases/{Feature}/Queries/{QueryName}/ `
56-
57- ``` csharp
58- public record GetAllHeroesQuery : IRequest <IReadOnlyList <HeroDto >>;
59- public record HeroDto (Guid Id , string Name , string Alias , int PowerLevel );
60-
61- internal sealed class GetAllHeroesQueryHandler (IApplicationDbContext dbContext )
62- : IRequestHandler <GetAllHeroesQuery , IReadOnlyList <HeroDto >>
63- {
64- public async Task <IReadOnlyList <HeroDto >> Handle (GetAllHeroesQuery request , CancellationToken ct )
65- => await dbContext .Heroes .Select (h => new HeroDto (h .Id .Value , h .Name , h .Alias , h .PowerLevel )).ToListAsync (ct );
66- }
67- ```
68-
69- ### Domain Entities with Strongly-Typed IDs (Vogen)
70- Location: ` src/Domain/{Feature}/ `
71-
72- ``` csharp
73- [ValueObject <Guid >]
74- public readonly partial struct HeroId ;
75-
76- public class Hero : AggregateRoot <HeroId >
77- {
78- private Hero () { } // EF Core constructor
79-
80- public static Hero Create (string name , string alias )
81- => new () { Id = HeroId .From (Guid .CreateVersion7 ()), Name = name , Alias = alias };
82- }
83- ```
84-
85- ### Domain Events
86- Raise events from aggregates to trigger side effects. Events are dispatched after ` SaveChangesAsync() ` .
87-
88- ** Define event** in ` src/Domain/{Feature}/ ` :
89- ``` csharp
90- public sealed record PowerLevelUpdatedEvent (Hero Hero ) : IDomainEvent ;
91- ```
92-
93- ** Raise from aggregate** :
94- ``` csharp
95- public void UpdatePowers (IEnumerable < Power > powers )
96- {
97- _powers .Clear ();
98- foreach (var power in powers ) AddPower (power );
99- AddDomainEvent (new PowerLevelUpdatedEvent (this )); // Raise event
100- }
101- ```
102-
103- ** Handle event** in ` src/Application/UseCases/{Feature}/EventHandlers/ ` :
104- ``` csharp
105- internal sealed class PowerLevelUpdatedEventHandler : INotificationHandler <PowerLevelUpdatedEvent >
106- {
107- public Task Handle (PowerLevelUpdatedEvent notification , CancellationToken ct )
108- {
109- // Side effects: send emails, update read models, integrate with external systems
110- return Task .CompletedTask ;
111- }
112- }
113- ```
114-
115- ### Minimal API Endpoints
116- Location: ` src/WebApi/Endpoints/ `
117-
118- ``` csharp
119- public static void MapHeroEndpoints (this WebApplication app )
120- {
121- var group = app .MapApiGroup (" heroes" );
122-
123- group .MapPost (" /" , async (ISender sender , CreateHeroCommand command , CancellationToken ct ) =>
124- {
125- var result = await sender .Send (command , ct );
126- return result .Match (_ => TypedResults .Created (), CustomResult .Problem );
127- })
128- .WithName (" CreateHero" )
129- .ProducesPost (); // Use extension methods for consistent status codes
130- }
131- ```
132-
133- ### Result Pattern (ErrorOr)
134- Use ` ErrorOr<T> ` for commands, not exceptions. Handle with ` .Match() ` :
135- ``` csharp
136- result .Match (success => TypedResults .Ok (success ), CustomResult .Problem );
137- ```
138-
139- ## Testing
140-
141- ### Integration Tests
142- Location: ` tests/WebApi.IntegrationTests/Endpoints/{Feature}/ `
143-
144- ``` csharp
145- public class CreateHeroCommandTests (TestingDatabaseFixture fixture ) : IntegrationTestBase (fixture )
146- {
147- [Fact ]
148- public async Task Command_ShouldCreateHero ()
149- {
150- var cmd = new CreateHeroCommand (" Clark Kent" , " Superman" , []);
151- var client = GetAnonymousClient ();
152-
153- var result = await client .PostAsJsonAsync (" /api/heroes" , cmd , CancellationToken );
154-
155- result .StatusCode .Should ().Be (HttpStatusCode .Created );
156- var hero = await GetQueryable <Hero >().FirstAsync (CancellationToken );
157- hero .Name .Should ().Be (cmd .Name );
158- }
159- }
160- ```
161-
162- Uses TestContainers + Respawn for real database testing.
163-
164- ### Unit Tests
165- Location: ` tests/Domain.UnitTests/ ` - Test domain logic without EF Core mocking (Specifications pattern).
166-
167- ### Architecture Tests
168- Location: ` tests/Architecture.Tests/ ` - NetArchTest enforces layer dependencies.
169-
170- ## EF Migrations
171-
172- ``` bash
173- # Add migration
174- dotnet ef migrations add YourMigration --project ./src/Infrastructure --startup-project ./src/WebApi --output-dir ./Persistence/Migrations
175-
176- # Migrations apply automatically via Aspire MigrationService
177- ```
178-
179- ## Conventions
180-
181- - ** No AutoMapper** - Use manual mapping with ` Select() ` projections
182- - ** Strongly-typed IDs** - All entities use Vogen ` [ValueObject<Guid>] `
183- - ** Factory methods** - Create aggregates via static ` Create() ` methods, not constructors
184- - ** Specifications** - Query logic in Domain layer (e.g., ` TeamByIdSpec ` )
185- - ** FluentValidation** - Validators in same folder as Command/Query
186- - ** Awesome Assertions** - Use ` Should() ` syntax in tests
187- - ** Code generation** - Reference existing code in ` src/Application/UseCases/Heroes/ ` as patterns
30+ - Implementation patterns (commands, queries, domain entities, endpoints, conventions, migrations) → ` src/AGENTS.md `
31+ - Testing patterns (integration, unit, architecture tests) → ` tests/AGENTS.md `
18832
18933## ADRs
34+
19035Architectural decisions documented in ` docs/adr/ ` . Key decisions:
19136- Results pattern over exceptions
19237- Vogen for strongly-typed IDs
0 commit comments