Skip to content
Back to skills

Easyplatform Backend

ASecurity

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.

  • 666 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added February 8, 2026
developmentbashexpresstestingapidatabasebackend

Works with

  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill easyplatform-backend --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Easyplatform Backend?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Easyplatform Backend
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-easyplatform-backend/badge)](https://www.skillsdirectory.com/skills/majiayu000-easyplatform-backend)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

1. **Repository:** `IPlatformQueryableRootRepository<T>` -- NEVER throw exceptions for validation
2. **Validation:** `PlatformValidationResult` fluent API
3. **Side Effects:** Entity Event Handlers -- NEVER in command handlers
4. **DTO Mapping:** DTOs own mapping via `PlatformEntityDto<T,K>.MapToEntity()`
5. **Cross-Service:** Message bus -- NEVER direct database access

---

## §1. CQRS Commands

**File:** `UseCaseCommands/{Feature}/Save{Entity}Command.cs` (Command + Result + Handler in ONE file)

```csharp
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](references/cqrs-patterns.md) — full CQRS command patterns

---

## §2. CQRS Queries

**File:** `UseCaseQueries/{Feature}/Get{Entity}ListQuery.cs`

```csharp
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](references/cqrs-patterns.md) — full CQRS query patterns

---

## §3. Entity Development

**File:** `{Service}.Domain/Entities/{Entity}.cs`

```csharp
[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](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`

```csharp
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](references/side-effects-patterns.md) — side effects patterns

## §5-7. Migrations, Jobs, Message Bus

**⚠️ MUST READ:** [migration-patterns.md](references/migration-patterns.md) | [job-patterns.md](references/job-patterns.md) | [messaging-patterns.md](references/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 use `dismissSendEvent: 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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.