Claude-skill-registry easyplatform-backend
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.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/easyplatform-backend" ~/.claude/skills/majiayu000-claude-skill-registry-easyplatform-backend && rm -rf "$T"
manifest:
skills/data/easyplatform-backend/SKILL.mdsource content
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:
-- NEVER throw exceptions for validationIPlatformQueryableRootRepository<T> - Validation:
fluent APIPlatformValidationResult - 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 |
|---|---|
| fluent API |
| Side effects in command handler | Entity Event Handler in |
| Direct cross-service DB access | Message bus |
| DTO mapping in handler | |
| Separate Command/Handler files | ONE file: Command + Result + Handler |
| |
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
, migrations usemaxConcurrentdismissSendEvent: 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