Backend C# Code Patterns
When implementing backend C# code in EasyPlatform, follow these patterns exactly.
Full Pattern Reference
See the complete code patterns with examples: backend-code-patterns.md
Quick Reference
Pattern Index
| # |
Pattern |
Key Interface/Contract |
| 1 |
Clean Architecture |
Domain → Application → Persistence → Api layers |
| 2 |
Repository |
IPlatformQueryableRootRepository<TEntity, TKey> + static expression extensions |
| 3 |
Repository API |
CreateAsync, GetByIdAsync, GetAllAsync, FirstOrDefaultAsync, CountAsync |
| 4 |
Validation |
PlatformValidationResult.And().AndAsync() fluent chain, never throw |
| 5 |
Cross-Service |
PlatformCqrsEntityEventBusMessageProducer + PlatformApplicationMessageBusConsumer |
| 6 |
Full-Text Search |
searchService.Search(q, text, Entity.SearchColumns()) in query builder |
| 7 |
CQRS Command |
Command + Result + Handler in ONE file, PlatformCqrsCommandApplicationHandler |
| 8 |
Query |
PlatformCqrsPagedQuery + GetQueryBuilder() + parallel count/items |
| 9 |
Side Effects |
Entity Event Handlers in UseCaseEvents/, never in command handlers |
| 10 |
Entity |
RootEntity<T, TKey>, static expressions, [TrackFieldUpdatedDomainEvent], navigation properties |
| 11 |
DTO |
PlatformEntityDto<T, TKey>.MapToEntity(), DTO owns mapping, constructor from entity |
| 12 |
Fluent Helpers |
.With(), .Then(), .EnsureFound(), .EnsureValid(), .ParallelAsync() |
| 13 |
Background Jobs |
PlatformApplicationPagedBackgroundJobExecutor, [PlatformRecurringJob("cron")] |
| 14 |
Message Bus |
PlatformApplicationMessageBusConsumer<TMessage>, TryWaitUntilAsync() for deps |
| 15 |
Data Migration |
PlatformDataMigrationExecutor<TDbContext>, OnlyForDbsCreatedBeforeDate |
| 16 |
Multi-Database |
PlatformEfCorePersistenceModule / PlatformMongoDbPersistenceModule |
Critical Rules
- Repository: Use
IPlatformQueryableRootRepository<TEntity, TKey> - NEVER generic IPlatformRootRepository
- Validation: Use
PlatformValidationResult fluent API (.And(), .AndAsync()) - NEVER throw ValidationException
- Side Effects: Handle in Entity Event Handlers (
UseCaseEvents/) - NEVER in command handlers
- DTO Mapping: DTOs own mapping via
MapToEntity() or MapToObject() - NEVER map in handlers
- Command Structure: Command + Result + Handler in ONE file under
UseCaseCommands/{Feature}/
- Cross-Service: Use RabbitMQ message bus - NEVER direct database access
Anti-Patterns
// ❌ Direct cross-service DB access → ✅ Use message bus
// ❌ Custom repository interface → ✅ Use platform repo + extensions
// ❌ Manual validation throw → ✅ Use PlatformValidationResult fluent API
// ❌ Side effects in handler → ✅ Use entity event handlers
// ❌ DTO mapping in handler → ✅ DTO owns mapping via MapToObject()/MapToEntity()
Templates
CQRS Command Template
public sealed class Save{Entity}Command : PlatformCqrsCommand<Save{Entity}CommandResult>
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public override PlatformValidationResult<IPlatformCqrsRequest> Validate()
=> base.Validate().And(_ => Name.IsNotNullOrEmpty(), "Name required");
}
public sealed class Save{Entity}CommandResult : PlatformCqrsCommandResult
{
public {Entity}Dto Entity { get; set; } = null!;
}
internal sealed class Save{Entity}CommandHandler : PlatformCqrsCommandApplicationHandler<Save{Entity}Command, Save{Entity}CommandResult>
{
protected override async Task<Save{Entity}CommandResult> HandleAsync(Save{Entity}Command req, CancellationToken ct)
{
var entity = req.Id.IsNullOrEmpty()
? req.MapToNewEntity().With(e => e.CreatedBy = RequestContext.UserId())
: await repo.GetByIdAsync(req.Id, ct).Then(e => req.UpdateEntity(e));
await entity.ValidateAsync(repo, ct).EnsureValidAsync();
var saved = await repo.CreateOrUpdateAsync(entity, ct);
return new Save{Entity}CommandResult { Entity = new {Entity}Dto(saved) };
}
}
Detailed Instructions
For task-specific guidance, also reference:
- backend-dotnet.instructions.md - .NET patterns
- cqrs-patterns.instructions.md - CQRS handlers
- entity-development.instructions.md - Entity design
- validation.instructions.md - Validation patterns
- repository.instructions.md - Repository patterns
- message-bus.instructions.md - Message bus
- background-jobs.instructions.md - Background jobs
- migrations.instructions.md - Data migrations
1---2name: backend-csharp-patterns3description: Use when editing C# backend files (.cs) in src/Backend/, src/Platform/, or src/PlatformExampleApp/. Provides CQRS patterns, repository patterns, validation patterns, entity patterns, background jobs, message bus consumers, data migrations, and fluent helpers for EasyPlatform .NET 9 development.4---5
6# Backend C# Code Patterns
7
8When implementing backend C# code in EasyPlatform, follow these patterns exactly.
9
10## Full Pattern Reference
11
12See the complete code patterns with examples: [backend-code-patterns.md](.ai/docs/backend-code-patterns.md)
13
14## Quick Reference
15
16### Pattern Index
17
18| # | Pattern | Key Interface/Contract |
19| --- | ------------------ | -------------------------------------------------------------------------------------------------- |
20| 1 | Clean Architecture | Domain → Application → Persistence → Api layers |
21| 2 | Repository | `IPlatformQueryableRootRepository<TEntity, TKey>` + static expression extensions |
22| 3 | Repository API | `CreateAsync`, `GetByIdAsync`, `GetAllAsync`, `FirstOrDefaultAsync`, `CountAsync` |
23| 4 | Validation | `PlatformValidationResult.And().AndAsync()` fluent chain, never throw |
24| 5 | Cross-Service | `PlatformCqrsEntityEventBusMessageProducer` + `PlatformApplicationMessageBusConsumer` |
25| 6 | Full-Text Search | `searchService.Search(q, text, Entity.SearchColumns())` in query builder |
26| 7 | CQRS Command | Command + Result + Handler in ONE file, `PlatformCqrsCommandApplicationHandler` |
27| 8 | Query | `PlatformCqrsPagedQuery` + `GetQueryBuilder()` + parallel count/items |
28| 9 | Side Effects | Entity Event Handlers in `UseCaseEvents/`, never in command handlers |
29| 10 | Entity | `RootEntity<T, TKey>`, static expressions, `[TrackFieldUpdatedDomainEvent]`, navigation properties |
30| 11 | DTO | `PlatformEntityDto<T, TKey>.MapToEntity()`, DTO owns mapping, constructor from entity |
31| 12 | Fluent Helpers | `.With()`, `.Then()`, `.EnsureFound()`, `.EnsureValid()`, `.ParallelAsync()` |
32| 13 | Background Jobs | `PlatformApplicationPagedBackgroundJobExecutor`, `[PlatformRecurringJob("cron")]` |
33| 14 | Message Bus | `PlatformApplicationMessageBusConsumer<TMessage>`, `TryWaitUntilAsync()` for deps |
34| 15 | Data Migration | `PlatformDataMigrationExecutor<TDbContext>`, `OnlyForDbsCreatedBeforeDate` |
35| 16 | Multi-Database | `PlatformEfCorePersistenceModule` / `PlatformMongoDbPersistenceModule` |
36
37## Critical Rules
38
391. **Repository:** Use `IPlatformQueryableRootRepository<TEntity, TKey>` - NEVER generic `IPlatformRootRepository`
402. **Validation:** Use `PlatformValidationResult` fluent API (`.And()`, `.AndAsync()`) - NEVER `throw ValidationException`
413. **Side Effects:** Handle in Entity Event Handlers (`UseCaseEvents/`) - NEVER in command handlers
424. **DTO Mapping:** DTOs own mapping via `MapToEntity()` or `MapToObject()` - NEVER map in handlers
435. **Command Structure:** Command + Result + Handler in ONE file under `UseCaseCommands/{Feature}/`
446. **Cross-Service:** Use RabbitMQ message bus - NEVER direct database access
45
46## Anti-Patterns
47
48```csharp
49// ❌ Direct cross-service DB access → ✅ Use message bus
50// ❌ Custom repository interface → ✅ Use platform repo + extensions
51// ❌ Manual validation throw → ✅ Use PlatformValidationResult fluent API
52// ❌ Side effects in handler → ✅ Use entity event handlers
53// ❌ DTO mapping in handler → ✅ DTO owns mapping via MapToObject()/MapToEntity()
54```
55
56## Templates
57
58### CQRS Command Template
59
60```csharp
61public sealed class Save{Entity}Command : PlatformCqrsCommand<Save{Entity}CommandResult>
62{
63 public string Id { get; set; } = "";
64 public string Name { get; set; } = "";
65
66 public override PlatformValidationResult<IPlatformCqrsRequest> Validate()
67 => base.Validate().And(_ => Name.IsNotNullOrEmpty(), "Name required");
68}
69
70public sealed class Save{Entity}CommandResult : PlatformCqrsCommandResult
71{
72 public {Entity}Dto Entity { get; set; } = null!;
73}
74
75internal sealed class Save{Entity}CommandHandler : PlatformCqrsCommandApplicationHandler<Save{Entity}Command, Save{Entity}CommandResult>
76{
77 protected override async Task<Save{Entity}CommandResult> HandleAsync(Save{Entity}Command req, CancellationToken ct)
78 {
79 var entity = req.Id.IsNullOrEmpty()
80 ? req.MapToNewEntity().With(e => e.CreatedBy = RequestContext.UserId())
81 : await repo.GetByIdAsync(req.Id, ct).Then(e => req.UpdateEntity(e));
82 await entity.ValidateAsync(repo, ct).EnsureValidAsync();
83 var saved = await repo.CreateOrUpdateAsync(entity, ct);
84 return new Save{Entity}CommandResult { Entity = new {Entity}Dto(saved) };
85 }
86}
87```
88
89## Detailed Instructions
90
91For task-specific guidance, also reference:
92
93- [backend-dotnet.instructions.md](instructions/backend-dotnet.instructions.md) - .NET patterns
94- [cqrs-patterns.instructions.md](instructions/cqrs-patterns.instructions.md) - CQRS handlers
95- [entity-development.instructions.md](instructions/entity-development.instructions.md) - Entity design
96- [validation.instructions.md](instructions/validation.instructions.md) - Validation patterns
97- [repository.instructions.md](instructions/repository.instructions.md) - Repository patterns
98- [message-bus.instructions.md](instructions/message-bus.instructions.md) - Message bus
99- [background-jobs.instructions.md](instructions/background-jobs.instructions.md) - Background jobs
100- [migrations.instructions.md](instructions/migrations.instructions.md) - Data migrations