---
name: backend-business-layer
description: >
  Generates full CQRS stack — Commands, Queries, Handlers, DTOs, Validators,
  Service interface + implementation — on SmartStack NuGet abstractions
  (ICoreDbContext, MediatR, FluentValidation).
phase: development/backend
cli: cli/scaffold-business
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# Business Layer — CQRS + Services

Generates the full application layer following the SmartStack CQRS pattern:
commands, queries, **handlers** (one per command/query), DTOs, FluentValidation
validators, service interface and service implementation.

## When to Use

- Phase 4 (business) of the development workflow
- When adding business logic for a new entity

## CQRS Pattern (output)

### Commands (each with a matching handler)

```csharp
public record CreateEmployeeCommand(string FirstName, string LastName) : IRequest<Guid>;

public class CreateEmployeeCommandHandler : IRequestHandler<CreateEmployeeCommand, Guid>
{
    private readonly IEmployeeService _service;
    public CreateEmployeeCommandHandler(IEmployeeService service) => _service = service;
    public Task<Guid> Handle(CreateEmployeeCommand request, CancellationToken cancellationToken)
        => _service.CreateAsync(request, cancellationToken);
}

public class CreateEmployeeCommandValidator : AbstractValidator<CreateEmployeeCommand>
{
    public CreateEmployeeCommandValidator()
    {
        RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100);
        RuleFor(x => x.LastName).NotEmpty().MaximumLength(100);
    }
}
```

### Update/Delete commands return `IRequest<Unit>`

MediatR 12 convention: void operations still need a response type. We use `Unit`:

```csharp
public record UpdateEmployeeCommand(Guid Id, string FirstName, string LastName) : IRequest<Unit>;

public class UpdateEmployeeCommandHandler : IRequestHandler<UpdateEmployeeCommand, Unit>
{
    private readonly IEmployeeService _service;
    public UpdateEmployeeCommandHandler(IEmployeeService service) => _service = service;
    public async Task<Unit> Handle(UpdateEmployeeCommand request, CancellationToken cancellationToken)
    {
        await _service.UpdateAsync(request, cancellationToken);
        return Unit.Value;
    }
}
```

### Queries

```csharp
public record GetEmployeeQuery(Guid Id) : IRequest<EmployeeDetailDto?>;
public record GetEmployeesQuery(int Page = 1, int PageSize = 20, string? Search = null)
    : IRequest<PaginatedResult<EmployeeListDto>>;

// Handlers delegate to the service (thin)
public class GetEmployeeQueryHandler : IRequestHandler<GetEmployeeQuery, EmployeeDetailDto?> { ... }
public class GetEmployeesQueryHandler : IRequestHandler<GetEmployeesQuery, PaginatedResult<EmployeeListDto>> { ... }
```

### DTOs (immutable records)

```csharp
public record EmployeeListDto(Guid Id, string FirstName, string LastName, DateTime CreatedAt);
public record EmployeeDetailDto(Guid Id, string FirstName, string LastName, /* all fields */, DateTime CreatedAt, DateTime UpdatedAt);
public record CreateEmployeeDto(string FirstName, string LastName);
public record UpdateEmployeeDto(string FirstName, string LastName);
```

### Service (uses `ICoreDbContext` — the SmartStack abstraction)

```csharp
public class EmployeeService : IEmployeeService
{
    private readonly ICoreDbContext _context;
    public EmployeeService(ICoreDbContext context) => _context = context;

    public async Task<Guid> CreateAsync(CreateEmployeeCommand command, CancellationToken ct = default)
    {
        var entity = Employee.Create(command.FirstName, command.LastName);
        _context.Set<Employee>().Add(entity);
        await _context.SaveChangesAsync(ct);
        return entity.Id;
    }
    // UpdateAsync / DeleteAsync / GetByIdAsync / GetAllAsync…
}
```

## ⚠ BLOCKING — Key Rules

**Any generated file violating these rules MUST fail audit and be rewritten.**

1. **Every command/query has a handler** — MediatR fails at runtime otherwise (unresolved `IRequestHandler<>`).
2. **Commands modify state** — return `Guid` (created id) or `Unit` (void).
3. **Queries are read-only** — return DTOs, NEVER domain entities.
4. **Services take `ICoreDbContext`**, not `IApplicationDbContext` or the concrete `CoreDbContext`. This is the SmartStack abstraction boundary.
5. **FluentValidation** — one validator per command, one rule per PRD business rule. Validators are wired via `services.AddValidatorsFromAssembly(...)` in the client's Program.cs; the CLI does NOT generate that DI line (client responsibility).
6. **DTOs are records** — immutable, value equality, nullable-aware.
7. **Service interface** — abstracts CQRS for controller consumption (`IEmployeeService`).
8. **No cross-layer leak** — Application must NOT import from `{AppCode}.Api.*` or `{AppCode}.Infrastructure.*` concrete types (only abstractions).
9. **`CancellationToken ct`** — propagated through every async method signature.
10. **`isKey` fields excluded from Update** — an update command never receives the primary key as a mutable field.

## Files emitted per entity

```
src/{Ns}.Application/{Mod}/
├── Commands/Create{E}Command.cs          — IRequest<Guid>
├── Commands/Update{E}Command.cs          — IRequest<Unit>
├── Commands/Delete{E}Command.cs          — IRequest<Unit>
├── Queries/Get{E}Query.cs                — IRequest<{E}DetailDto?>  + Get{Plural}Query
├── Handlers/Create{E}CommandHandler.cs   — delegates to I{E}Service
├── Handlers/Update{E}CommandHandler.cs
├── Handlers/Delete{E}CommandHandler.cs
├── Handlers/Get{E}QueryHandler.cs
├── Handlers/Get{Plural}QueryHandler.cs
├── Validators/Create{E}CommandValidator.cs
├── Validators/Update{E}CommandValidator.cs
├── Interfaces/I{E}Service.cs
└── DTOs/{E}Dtos.cs

src/{Ns}.Infrastructure/Services/{Mod}/
└── {E}Service.cs                         — uses ICoreDbContext
```

## Business Rules → Validators

Each business rule from the PRD becomes a FluentValidation rule:

```csharp
// PRD: "Start date must be before end date"
RuleFor(x => x.StartDate).LessThan(x => x.EndDate)
    .WithMessage("Start date must be before end date");
```

The CLI applies precedence: explicit `rule.expression` → description heuristics
(required / range / pattern / comparison) → TODO comment fallback with the rule id.
Each rule is emitted into **both** `Create{E}CommandValidator` and
`Update{E}CommandValidator` — a business invariant holds on modification too, not
only on creation.

## Post-scaffold business-logic pass (MANDATORY)

`scaffold-business` is deterministic and intentionally leaves the *hard* logic as
explicit, greppable markers rather than guessing. After the CLI has emitted the
CQRS stack, the dev agent (Phase 2 of `ba-develop`) **MUST** complete them — the
API gate is **BLOCKING** on zero residual markers (see `ba-develop/SKILL.md`
§ "After Phase 2 (API)") and audit `DEV-API-008/009` rejects any left behind.

Read the module's `règles-métier.md` (rule expressions + valid/invalid examples)
and its sections' `use-case.md` (main / alternative / exception flows), then:

1. **`// TODO[BR-…]` in a validator** — a business rule the heuristics could not
   translate. Replace it with the real enforcement:
   - field / cross-field, expressible in FluentValidation → write the `RuleFor(...)`
     in *both* `Create{E}CommandValidator` and `Update{E}CommandValidator`.
   - **multi-entity, stateful or temporal** (e.g. "cannot approve a budget whose
     parent is archived", "end date after start date and not in the past") →
     FluentValidation cannot express it. Enforce it inside the **service method**
     that performs the operation, throwing the domain exception the use case
     expects (`ValidationException` or a domain-specific one).
2. **`throw new NotImplementedException()` + `// TODO[UC-…]` in a service method** —
   a non-canonical custom action (anything beyond archive / restore / activate /
   deactivate / duplicate). Implement the body from the linked use case's main
   flow: load the aggregate, apply the state change + invariants, persist, return
   the response DTO. Keep the `// UC-…` trace comment.
3. **Trace** every rule / use case you implement with a `// BR-…` / `// UC-…`
   comment so the audit can confirm coverage.

Never delete a marker without implementing it, and never weaken a test to make a
stub pass. If a rule is genuinely unimplementable from the BA docs, halt and route
back to `create-prd` / the owning BA phase — do not ship a stub.

## Client DI registration (manual, once per project)

Add to the client's `Program.cs` (or `Application/DependencyInjection.cs`):

```csharp
services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly));
services.AddValidatorsFromAssembly(typeof(Program).Assembly);
services.AddScoped<IEmployeeService, EmployeeService>();
// One AddScoped per generated service.
```

## After generation — MANDATORY

Once the CLI has written all files, invoke the audit skill:

```
@.claude/skills/development/audit/SKILL.md
Audit the application layer I just generated in src/{AppCode}.Application/
for the entity {Name}. Check CQRS conventions, validator coverage,
DTO-vs-entity, handler presence, DI registrations.
```

The audit MUST be green before declaring the generation "done".

## Invocation

```bash
npx --prefer-offline tsx skills/development/backend/business-layer/cli/scaffold-business/index.ts \
  --spec '{"name":"Employee","module":"hrm","appCode":"MyApp","fields":[{"name":"firstName","type":"string","required":true,"maxLength":100}],"businessRules":[...],"projectPath":"/path"}'
```

`fields[].isKey` marks a field that is part of the primary key — excluded from
Update commands. Default is `false`.

`fields[].formula` (optional, string) marks a **computed attribute**. When set,
the generator treats the field as read-only :

- `${E}ListDto` and `${E}DetailDto` include it.
- `Create${E}Dto`, `Update${E}Dto`, `Create${E}Command`, `Update${E}Command` and
  the FluentValidation validators **exclude** it (the user never writes the
  value).
- `Get${E}` and `Get${Plural}` LINQ projections inject the formula directly :
  ```csharp
  .Select(x => new BudgetListDto(
      x.Id,
      x.Label,
      x.InitialAmount,
      x.CurrentBalance,
      (x.InitialAmount - x.CurrentBalance) / x.InitialAmount,  // consumptionRate
      x.CreatedAt))
  ```
  → one SQL query, no N+1, no separate analysis endpoint.

The formula syntax is a C# expression referencing **PascalCase property names**
of the SAME entity (`InitialAmount`, `CurrentBalance`, …). The CLI rewrites
each known property reference to `x.<Property>` so EF Core can translate it.
Cross-entity references are forbidden at the BA level (audit DM-016) and never
reach the dev pipeline.

When the PRD slice declares a computed column (e.g. `domain.md` lists
`Budget.consumptionRate (decimal) = (InitialAmount - CurrentBalance) / InitialAmount`),
you MUST forward that formula via the `formula` field of the corresponding
spec entry. Do NOT emit a stub helper or hand-roll the projection — the CLI
output is the single source of truth.
