name: easyplatform-backend description: Complete Easy.Platform backend development for EasyPlatform. Covers CQRS commands/queries, entities, validation, migrations, background jobs, and message bus. Use for any .NET backend task in this monorepo. infer: true allowed-tools: Read, Write, Edit, Grep, Glob, Bash
Easy.Platform Backend Development
Complete backend development patterns for EasyPlatform .NET 9 microservices.
Quick Decision Tree
[Backend Task]
├── API endpoint?
│ ├── Creates/Updates/Deletes data → CQRS Command (§1)
│ └── Reads data → CQRS Query (§2)
├── Business entity? → Entity Development (§3)
├── Side effects (notifications, emails)? → Entity Event Handler (§4) - NEVER in command handlers!
├── Data transformation/backfill? → Migration (§5)
├── Scheduled/recurring task? → Background Job (§6)
└── Cross-service sync? → Message Bus (§7) - NEVER direct DB access!
File Organization
{Service}.Application/
├── UseCaseCommands/{Feature}/Save{Entity}Command.cs # Command+Handler+Result
├── UseCaseQueries/{Feature}/Get{Entity}ListQuery.cs # Query+Handler+Result
├── UseCaseEvents/{Feature}/*EntityEventHandler.cs # Side effects
├── BackgroundJobs/{Feature}/*Job.cs # Scheduled tasks
├── MessageBusProducers/*Producer.cs # Outbound events
├── MessageBusConsumers/{Entity}/*Consumer.cs # Inbound events
└── DataMigrations/*DataMigration.cs # Data migrations
{Service}.Domain/
└── Entities/{Entity}.cs # Domain entities
Critical Rules
- Repository:
IPlatformQueryableRootRepository<T>-- NEVER throw exceptions for validation - Validation:
PlatformValidationResultfluent API - Side Effects: Entity Event Handlers -- NEVER in command handlers
- DTO Mapping: DTOs own mapping via
PlatformEntityDto<T,K>.MapToEntity() - Cross-Service: Message bus -- NEVER direct database access
§1. CQRS Commands
File: UseCaseCommands/{Feature}/Save{Entity}Command.cs (Command + Result + Handler in ONE file)
public sealed class SaveEmployeeCommand : PlatformCqrsCommand<SaveEmployeeCommandResult>
{
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 SaveEmployeeCommandResult : PlatformCqrsCommandResult
{
public EmployeeDto Entity { get; set; } = null!;
}
internal sealed class SaveEmployeeCommandHandler :
PlatformCqrsCommandApplicationHandler<SaveEmployeeCommand, SaveEmployeeCommandResult>
{
protected override async Task<SaveEmployeeCommandResult> HandleAsync(
SaveEmployeeCommand req, CancellationToken ct)
{
var entity = req.Id.IsNullOrEmpty()
? req.MapToNewEntity().With(e => e.CreatedBy = RequestContext.UserId())
: await repository.GetByIdAsync(req.Id, ct)
.EnsureFound().Then(e => req.UpdateEntity(e));
await entity.ValidateAsync(repository, ct).EnsureValidAsync();
await repository.CreateOrUpdateAsync(entity, ct);
return new SaveEmployeeCommandResult { Entity = new EmployeeDto(entity) };
}
}
⚠️ MUST READ: references/cqrs-patterns.md — full CQRS command patterns
§2. CQRS Queries
File: UseCaseQueries/{Feature}/Get{Entity}ListQuery.cs
public sealed class GetEmployeeListQuery : PlatformCqrsPagedQuery<GetEmployeeListQueryResult, EmployeeDto>
{
public List<Status> Statuses { get; set; } = [];
public string? SearchText { get; set; }
}
internal sealed class GetEmployeeListQueryHandler :
PlatformCqrsQueryApplicationHandler<GetEmployeeListQuery, GetEmployeeListQueryResult>
{
protected override async Task<GetEmployeeListQueryResult> HandleAsync(
GetEmployeeListQuery req, CancellationToken ct)
{
var qb = repository.GetQueryBuilder((uow, q) => q
.Where(e => e.CompanyId == RequestContext.CurrentCompanyId())
.WhereIf(req.Statuses.Any(), e => req.Statuses.Contains(e.Status))
.PipeIf(req.SearchText.IsNotNullOrEmpty(), q =>
searchService.Search(q, req.SearchText, Employee.DefaultFullTextSearchColumns())));
var (total, items) = await (
repository.CountAsync((uow, q) => qb(uow, q), ct),
repository.GetAllAsync((uow, q) => qb(uow, q)
.OrderByDescending(e => e.CreatedDate).PageBy(req.SkipCount, req.MaxResultCount), ct)
);
return new GetEmployeeListQueryResult(items.SelectList(e => new EmployeeDto(e)), total, req);
}
}
⚠️ MUST READ: references/cqrs-patterns.md — full CQRS query patterns
§3. Entity Development
File: {Service}.Domain/Entities/{Entity}.cs
[TrackFieldUpdatedDomainEvent]
public sealed class Employee : RootAuditedEntity<Employee, string, string>
{
[TrackFieldUpdatedDomainEvent] public string Name { get; set; } = "";
public string CompanyId { get; set; } = "";
[ComputedEntityProperty]
public string DisplayName { get => $"{Code} - {Name}"; set { } }
public static Expression<Func<Employee, bool>> OfCompanyExpr(string companyId) => e => e.CompanyId == companyId;
public static Expression<Func<Employee, bool>> UniqueExpr(string companyId, string code) => e => e.CompanyId == companyId && e.Code == code;
public static Expression<Func<Employee, object?>>[] DefaultFullTextSearchColumns() => [e => e.Name, e => e.Code];
public async Task<PlatformValidationResult> ValidateAsync(IRepository<Employee> repo, CancellationToken ct)
=> await PlatformValidationResult.Valid()
.And(() => Name.IsNotNullOrEmpty(), "Name required")
.AndNotAsync(() => repo.AnyAsync(e => e.Id != Id && e.Code == Code, ct), "Code exists");
}
⚠️ MUST READ: references/entity-patterns.md — full entity patterns
§4. Entity Event Handlers (Side Effects)
CRITICAL: NEVER call side effects in command handlers!
File: UseCaseEvents/{Feature}/Send{Action}On{Event}{Entity}EntityEventHandler.cs
internal sealed class SendNotificationOnCreateEmployeeEntityEventHandler
: PlatformCqrsEntityEventApplicationHandler<Employee>
{
public override async Task<bool> HandleWhen(PlatformCqrsEntityEvent<Employee> @event)
=> !@event.RequestContext.IsSeedingTestingData()
&& @event.CrudAction == PlatformCqrsEntityEventCrudAction.Created;
protected override async Task HandleAsync(PlatformCqrsEntityEvent<Employee> @event, CancellationToken ct)
=> await notificationService.SendAsync(@event.EntityData.Id);
}
⚠️ MUST READ: references/side-effects-patterns.md — side effects patterns
§5-7. Migrations, Jobs, Message Bus
⚠️ MUST READ: migration-patterns.md | job-patterns.md | messaging-patterns.md
Anti-Patterns
| Don't | Do |
| --------------------------------- | ----------------------------------------------- |
| throw new ValidationException() | PlatformValidationResult fluent API |
| Side effects in command handler | Entity Event Handler in UseCaseEvents/ |
| Direct cross-service DB access | Message bus |
| DTO mapping in handler | PlatformEntityDto.MapToEntity() |
| Separate Command/Handler files | ONE file: Command + Result + Handler |
| protected bool HandleWhen() | public override async Task<bool> HandleWhen() |
Checklist
- [ ] Service-specific repository, fluent validation (
.And(),.AndAsync()) - [ ] No side effects in command handlers, DTO mapping in DTO class
- [ ] Cross-service uses message bus, jobs have
maxConcurrent, migrations usedismissSendEvent: true
IMPORTANT Task Planning Notes
- Always plan and break many small todo tasks
- Always add a final review todo task to review the works done at the end to find any fix or enhancement needed
Expert Next.js App Router
Developpement
Un skill qui transforme Claude en expert Next.js App Router.
Générateur de README
Developpement
Crée des README.md professionnels et complets pour vos projets.
Rédacteur de Documentation API
Developpement
Génère de la documentation API complète au format OpenAPI/Swagger.