# SmartStack Domain API Reference

> **Source of truth:** `SmartStack.app/src/SmartStack.Domain/Common/`
> **Loaded by:** step-01 (analyze), step-03a/03b/03c (execute). Released after Layer 2.
> **MCP generates complete implementations** — this file provides guard rails and conventions only.

---

## BaseEntity

```csharp
namespace SmartStack.Domain.Common;

public abstract class BaseEntity
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}
```

**ONLY 3 properties.** No Code, no IsDeleted, no RowVersion, no SoftDelete, no CreatedBy/UpdatedBy.

---

## Interfaces

### ITenantEntity (mandatory tenant isolation)

```csharp
public interface ITenantEntity
{
    Guid TenantId { get; }
}
```

### IAuditableEntity (audit trail)

```csharp
public interface IAuditableEntity
{
    string? CreatedBy { get; set; }
    string? UpdatedBy { get; set; }
}
```

### IOptionalTenantEntity (nullable tenant — cross-tenant)

```csharp
public interface IOptionalTenantEntity
{
    Guid? TenantId { get; }
}
```

### IScopedTenantEntity (tenant + scope visibility)

```csharp
public interface IScopedTenantEntity : IOptionalTenantEntity
{
    EntityScope Scope { get; }
}
```

### EntityScope enum

```csharp
public enum EntityScope
{
    Tenant = 0,   // Visible only to specific tenant (TenantId required)
    Shared = 1,   // Visible to all tenants (TenantId null)
    Platform = 2  // Visible only to platform admins (HasGlobalAccess)
}
```

### Enum Serialization (MANDATORY)

All enum types used in DTOs MUST serialize as strings for frontend compatibility.

**Option A — Per-enum attribute (preferred when few enums):**
```csharp
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EmployeeStatus { Active, OnLeave, Terminated }
```

**Option B — Global configuration (if project has many enums):**
Verify in `Program.cs`: `JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())`

> **Rule:** Before creating enums, check if global `JsonStringEnumConverter` exists. If yes, no per-enum attribute needed. If no, add `[JsonConverter(typeof(JsonStringEnumConverter))]` on every enum used in API responses.

---

## Entity Patterns

### Tenant-scoped (most common)

```csharp
public class {Name} : BaseEntity, ITenantEntity, IAuditableEntity
{
    public Guid TenantId { get; private set; }
    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }

    // Business properties
    public string Code { get; private set; } = null!;
    public string Name { get; private set; } = null!;
    public string? Description { get; private set; }
    public bool IsActive { get; private set; } = true;

    private {Name}() { }

    public static {Name} Create(Guid tenantId, string code, string name)
    {
        if (tenantId == Guid.Empty)
            throw new ArgumentException("TenantId is required", nameof(tenantId));

        return new {Name}
        {
            Id = Guid.NewGuid(),
            TenantId = tenantId,
            Code = code.ToLowerInvariant(),
            Name = name,
            CreatedAt = DateTime.UtcNow
        };
    }

    public void Update(string name, string? description)
    {
        Name = name;
        Description = description;
        UpdatedAt = DateTime.UtcNow;
    }
}
```

### Platform-level (no tenant)

```csharp
public class {Name} : BaseEntity, IAuditableEntity
{
    // No TenantId — platform-wide entity
    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }
    public string Code { get; private set; } = null!;
    public string Name { get; private set; } = null!;
    private {Name}() { }
    // Create(): no tenantId parameter, no Guid.Empty guard
}
```

### Cross-Tenant (IOptionalTenantEntity) — key differences

```csharp
public class {Name} : BaseEntity, IOptionalTenantEntity, IAuditableEntity
{
    public Guid? TenantId { get; private set; }  // nullable — null = shared
    // Create(Guid? tenantId = null, ...) — tenantId nullable
}
```

EF query filter (in CoreDbContext): `builder.HasQueryFilter(e => !ShouldFilterByTenant || e.TenantId == null || e.TenantId == CurrentTenantId);`
Service: no guard clause — `var tenantId = _currentTenant.TenantId;` (null = creating shared data).

### Scoped (IScopedTenantEntity) — key differences

```csharp
public class {Name} : BaseEntity, IScopedTenantEntity, IAuditableEntity
{
    public Guid? TenantId { get; private set; }
    public EntityScope Scope { get; private set; }
    // Create: if scope == Tenant && tenantId == null → throw
}
```

### Person Extension (User-linked)

> **Full reference:** See `references/person-extension-pattern.md` for complete patterns.

| Variant | UserId | Use case |
|---------|--------|----------|
| **Mandatory** | `Guid UserId` | Employee, Manager — always a User |
| **Optional** | `Guid? UserId` | Customer, Contact — may or may not have account |

Key rules:
- **Mandatory:** Unique index on `(TenantId, UserId)` without filter. No person fields on entity.
- **Optional:** Unique index on `(TenantId, UserId)` with `.HasFilter("[UserId] IS NOT NULL")`. Add own `FirstName`/`LastName`/`Email` + `Display*` computed properties.
- **Service:** Always Include User. Verify User exists + no duplicate on CreateAsync.
- **DTO (mandatory):** CreateDto requires `UserId`.
- **DTO (optional):** CreateDto requires person fields when `UserId` is null.

### MCP tenantMode Parameter

When calling `scaffold_extension`, use the `tenantMode` parameter:
- `strict` (default) — ITenantEntity, Guid TenantId (required)
- `optional` — IOptionalTenantEntity, Guid? TenantId (cross-tenant)
- `scoped` — IScopedTenantEntity, Guid? TenantId + EntityScope
- `none` — No tenant interface (platform-level entities)

The old `isSystemEntity: true` still works and maps to `tenantMode: 'none'`.

---

## EF Configuration Pattern

```csharp
public class {Name}Configuration : IEntityTypeConfiguration<{Name}>
{
    public void Configure(EntityTypeBuilder<{Name}> builder)
    {
        builder.ToTable("{prefix}{Name}s", "{schema}");
        builder.HasKey(x => x.Id);

        // Tenant (if ITenantEntity)
        builder.Property(x => x.TenantId).IsRequired();
        builder.HasIndex(x => x.TenantId)
            .HasDatabaseName("IX_{prefix}{Name}s_TenantId");

        // Business properties
        builder.Property(x => x.Code).HasMaxLength(50).IsRequired();
        builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
        builder.Property(x => x.Description).HasMaxLength(500);

        // Audit (from IAuditableEntity)
        builder.Property(x => x.CreatedBy).HasMaxLength(256);
        builder.Property(x => x.UpdatedBy).HasMaxLength(256);

        // Unique indexes
        builder.HasIndex(x => new { x.TenantId, x.Code })
            .IsUnique()
            .HasDatabaseName("IX_{prefix}{Name}s_Tenant_Code");

        // Relationships — use .OnDelete(DeleteBehavior.Restrict)
    }
}
```

---

## Service Pattern (guard rails)

> MCP `scaffold_extension` generates complete services. These are the mandatory conventions.

**Key interfaces:**
- `ICurrentUserService` (`SmartStack.Application.Common.Interfaces.Identity`): `UserId` (Guid?), `Email` (string?), `IsAuthenticated` (bool)
- `ICurrentTenantService` (`SmartStack.Application.Common.Interfaces.Tenants`): `TenantId` (Guid?), `HasTenant` (bool), `TenantSlug` (string?)
- `IExtensionsDbContext` (for client extensions) or `ICoreDbContext` (for platform)

**Guard clause (first line of every method):**
```csharp
var tenantId = _currentTenant.TenantId
    ?? throw new TenantContextRequiredException();
```
Returns clean 400 Bad Request via `GlobalExceptionHandlerMiddleware`.
Uses `TenantContextRequiredException` (400), NOT `UnauthorizedAccessException` (401 — clears frontend token).

**Do NOT use in services:**
- `_currentTenant.TenantId!.Value` — throws 500 instead of clean 400
- `UnauthorizedAccessException("Tenant context is required")` — returns 401, triggers frontend token clearing
- `tenantId: Guid.Empty` — always use validated `_currentTenant.TenantId`
- Queries WITHOUT `.Where(x => x.TenantId == tenantId)` — data leak (OWASP A01)
- `ICurrentUser` (does NOT exist) — use `ICurrentUserService` + `ICurrentTenantService`
- Missing `ILogger<T>` — undiagnosable in production

### FK Inter-Entity Service Rules

When an entity has a FK to another business entity (e.g., `Absence.EmployeeId → Employee`):

- **GetAllAsync / GetByIdAsync:** Use `.Select()` with inline navigation access (e.g., `x.Employee.Code`). Do NOT use `.Include()` — it is ignored when `.Select()` is present. EF Core generates JOINs from navigation paths.
- **CreateAsync / UpdateAsync:** Use `.Include()` AFTER `SaveChangesAsync()` to reload entity with FK navigation for response DTO.
- **FK validation:** Always verify FK target exists in same tenant before create/update.
- **Search:** Include FK display fields in search filter (e.g., `x.Employee.Code.Contains(search)`).
- **Navigation depth:** Limit to 1-2 levels (e.g., `x.Employee.User!.LastName`). Deeper = complex JOINs.

> **IQueryable .Select() constraint:** NEVER call helper methods (e.g., `MapToDto(x)`) inside `.Select()` on IQueryable — EF Core cannot translate to SQL → runtime `InvalidOperationException`. Use inline DTO construction only. Helpers are fine AFTER materialization (`ToListAsync`).

> **FK navigation in `.Select()` is safe:** `.Select(x => new Dto(x.Id, x.Employee.Code, x.Employee.User!.LastName))` — EF Core translates to SQL JOINs.

---

## Controller Pattern (NavRoute)

> `/controller` skill or MCP generates complete controllers. These are the mandatory conventions.

```csharp
[ApiController]
[NavRoute("{app}.{module}")]  // ONLY route attribute — resolves from DB at startup
[Microsoft.AspNetCore.Authorization.Authorize]
[Produces("application/json")]
[Tags("{NamePlural}")]
public class {Name}Controller : ControllerBase
{
    private readonly ISender _mediator;  // MediatR — NOT I{Name}Service

    public {Name}Controller(ISender mediator) => _mediator = mediator;

    [HttpGet]
    [RequirePermission(Permissions.{Module}.{NamePlural}.View)]
    [ProducesResponseType(typeof(List<{Name}ListDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public async Task<ActionResult<List<{Name}ListDto>>> GetAll(CancellationToken ct)
        => Ok(await _mediator.Send(new Get{NamePlural}Query(), ct));

    // GetById, Create (CreatedAtAction), Update, Delete (NoContent)
}
```

**Key conventions:**
- `ISender _mediator` (MediatR) — NOT `I{Name}Service` or `IApplicationDbContext`
- `[Microsoft.AspNetCore.Authorization.Authorize]` — fully qualified (not `[Authorize]` with using)
- `[Tags("...")]` and `[Produces("application/json")]` — always present
- `[RequirePermission(Permissions.{Module}.{Resource}.View)]` — permission constants, not string literals
- `[ProducesResponseType]` with `StatusCodes.*` constants — always include 401/403
- Return types: `List<ListDto>` (GetAll), `DetailDto` (GetById/Create/Update), `IActionResult` (Delete)

**NavRoute rules:**
- `[NavRoute]` is the ONLY route attribute — do NOT combine with `[Route("api/...")]` (causes 404s)
- `[RequirePermission]` on every endpoint — `[Authorize]` alone has no RBAC
- Permission paths must match NavRoute kebab-case: `human-resources.employees.read`
- Namespace: `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
- NavRoute resolves at startup from DB: `administration.users` → `api/administration/users`
- `CustomSegment` support: `[NavRoute("...", CustomSegment = "user/dashboard")]` for custom URL segments

**NavRoute segment rules:**

| Level | Format | Example |
|-------|--------|---------|
| Module | `{app}.{module}` (2 segments) | `administration.users` |
| Section | `{app}.{module}.{section}` (3 segments) | `administration.users.groups` |
| Sub-resource | `{app}.{module}` + `Suffix` | `[NavRoute("administration.ai", Suffix = "prompts")]` |

> ROUTING: 2-segment minimum. `NavigationRouteRegistryBuilder` builds 2-segment paths from DB. 3+ segments use fallback route generator.

**Sub-resource completeness:** If a parent page has a navigate() to a sub-resource route, the frontend MUST include a page for that route. Otherwise → dead link → white screen. Prefer separate controllers (with Suffix) over sub-endpoints in parent controller.

---

## Navigation Seed Data (routes must be full paths)

| Level | Route Format | Example |
|-------|-------------|---------|
| Application | `/{app-kebab}` | `/human-resources` |
| Module | `/{app-kebab}/{module-kebab}` | `/human-resources/employees` |
| Section | `/{app-kebab}/{module-kebab}/{section-kebab}` | `/human-resources/employees/departments` |

**Route special cases:** `list` and `detail` sections are view modes, NOT sub-areas:
- `list` route = module route (e.g., `/human-resources/employees`)
- `detail` route = module route + `/:id`
- Do NOT use: `/employees/list`, `/employees/detail/:id`

**Rules:**
- Routes ALWAYS start with `/`, include full hierarchy, use kebab-case
- Code identifiers: PascalCase in C# (`HumanResources`), kebab-case in routes (`human-resources`)

**ToKebabCase helper:**
```csharp
private static string ToKebabCase(string value)
    => Regex.Replace(value, "([a-z])([A-Z])", "$1-$2").ToLowerInvariant();
```

**GUID rule:** Always `Guid.NewGuid()` for seed data IDs. Idempotence via Code-based lookups at runtime.

| Mistake | Reality |
|---------|---------|
| `"humanresources"` as route | Must be `"/human-resources"` (full path, kebab-case) |
| `"employees"` as route | Must be `"/human-resources/employees"` (includes parent) |
| Fixed/sequential GUIDs | Use `Guid.NewGuid()` |
| Missing translations | Must have 4 languages: fr, en, it, de |
| Missing NavigationApplicationSeedData | Menu invisible without Application level |

---

## DbContext Pattern

```csharp
// In IExtensionsDbContext.cs AND ExtensionsDbContext.cs:
public DbSet<{Name}> {Name}s => Set<{Name}>();
```

## DI Registration

```csharp
services.AddScoped<I{Name}Service, {Name}Service>();
services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
```

---

## DTO Type Mapping

| Property Pattern | .NET Type | JSON Format | Example |
|-----------------|-----------|-------------|---------|
| `*Date`, `StartDate`, `BirthDate` | `DateOnly` | `"2025-03-15"` | `public DateOnly Date { get; set; }` |
| `CreatedAt`, `UpdatedAt` | `DateTime` | `"2025-03-15T10:30:00Z"` | `public DateTime CreatedAt { get; set; }` |
| `*Time`, `StartTime` | `TimeOnly` | `"14:30:00"` | `public TimeOnly StartTime { get; set; }` |
| Duration, hours | `decimal` | `8.5` | `public decimal HoursWorked { get; set; }` |
| FK reference | `Guid` | `"uuid-string"` | `public Guid EmployeeId { get; set; }` |

Do NOT use: `string Date` (use `DateOnly`), `DateTime BirthDate` (use `DateOnly`), `int` for hours (use `decimal`).

---

## PaginatedResult

> **Canonical type — one name, one contract, everywhere.**

```csharp
// SmartStack.Application.Common.Models
public record PaginatedResult<T>(List<T> Items, int TotalCount, int Page, int PageSize)
{
    public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
    public bool HasPreviousPage => Page > 1;
    public bool HasNextPage => Page < TotalPages;
    public static PaginatedResult<T> Empty(int page = 1, int pageSize = 20) => new([], 0, page, pageSize);
}

// Extension method — SmartStack.Application.Common.Extensions
public static Task<PaginatedResult<T>> ToPaginatedResultAsync<T>(
    this IQueryable<T> query, int page = 1, int pageSize = 20, CancellationToken ct = default)
// Implementation: Math.Max(1, page), Math.Clamp(pageSize, 1, 100), CountAsync, Skip/Take, ToListAsync
```

**Frontend type:** `PaginatedResult<T>` with `items`, `totalCount`, `page`, `pageSize`, `totalPages`, `hasPreviousPage`, `hasNextPage`.

| Avoid | Use Instead |
|-------|------------|
| `PagedResult<T>` / `PaginatedResultDto<T>` / `PaginatedResponse<T>` | `PaginatedResult<T>` |
| `currentPage` (property) | `page` |
| `HasPrevious` / `HasNext` | `HasPreviousPage` / `HasNextPage` |

**Rules:** Max pageSize = 100. Default page = 1, pageSize = 20. Search param mandatory (enables EntityLookup). POST-CHECK C12 blocks `List<T>` returns. POST-CHECK C28 blocks non-canonical type names.

---

## Common Mistakes to Avoid

| Mistake | Reality |
|---------|---------|
| `entity.SoftDelete()` | Does NOT exist — no soft delete in BaseEntity |
| `entity.Code` inherited | Code is a business property — add it yourself |
| `e.RowVersion` / `e.IsDeleted` in config | Does NOT exist in BaseEntity |
| `SmartStack.Api.Core.Routing` | Wrong — use `SmartStack.Api.Routing` |
| `SystemEntity` base class | Does NOT exist — use `BaseEntity` for all |
| `[Route("api/...")] + [NavRoute]` | Do not combine — causes 404s. Only `[NavRoute]` needed. |
| `SmartStack.Domain.Common.Interfaces` | Interfaces are in `SmartStack.Domain.Common` directly |
| `[Authorize]` without `[RequirePermission]` | No RBAC enforcement |
| `tenantId: Guid.Empty` | OWASP A01 — use validated `_currentTenant.TenantId` |
| `ICurrentUser` | Does NOT exist — use `ICurrentUserService` + `ICurrentTenantService` |
| `_currentTenant.TenantId!.Value` | Throws 500 — use `?? throw new TenantContextRequiredException()` |
| `UnauthorizedAccessException` for tenant | Returns 401 → clears token. Use `TenantContextRequiredException` (400) |
| `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
| `GetAllAsync()` without search param | ALL GetAll MUST support `?search=` for EntityLookup |
| `PagedResult<T>` | Use `PaginatedResult<T>` |
| Controller injects `DbContext` | Controllers use `ISender _mediator` (MediatR) |
| Controller injects `I{Name}Service` | Use `ISender _mediator` — MediatR dispatches to handlers |
| `[Authorize]` with using statement | Use `[Microsoft.AspNetCore.Authorization.Authorize]` (fully qualified) |
| Missing `[Tags]` / `[Produces]` | Always add `[Tags("...")]` and `[Produces("application/json")]` |
| `[ProducesResponseType(200)]` integer literal | Use `StatusCodes.Status200OK` constants |
| Missing 401/403 ProducesResponseType | Always include `StatusCodes.Status401Unauthorized` and `Status403Forbidden` |
| `[Table]` attribute in Domain entity | Infrastructure concern — move to `IEntityTypeConfiguration<T>` |
| `using Microsoft.EntityFrameworkCore` in Domain | EF Core belongs in Infrastructure — Domain must be persistence-ignorant |
| Controller returns entity | Domain leak — return `ResponseDto` instead |
| `IEmployeeService.cs` in Infrastructure | Interface → Application. Implementation → Infrastructure |
| Service implementation in Application | Implementations → Infrastructure. Application = interfaces only |
| Duplicate person fields on mandatory extension | Use `Display*` from User join |
| Missing `Include(User)` on person extension | Always include User for display fields |
| Missing unique index `(TenantId, UserId)` | Required for person extension entities |

---

## Critical Anti-Patterns (summary)

| # | Anti-Pattern | Risk | Correct Approach |
|---|-------------|------|-----------------|
| 1 | `HasQueryFilter(e => e.TenantId != Guid.Empty)` | OWASP A01 — does NOT isolate tenants | Tenant filter in SERVICE: `.Where(x => x.TenantId == tenantId)` |
| 2 | `List<T>` return on GetAll | No pagination, breaks EntityLookup | `PaginatedResult<T>` with search + page + pageSize |
| 3 | `ITenantEntity` without `IAuditableEntity` | No audit trail | Always pair: `BaseEntity, ITenantEntity, IAuditableEntity` |
| 4 | Code generation via `Count() + 1` | Race condition — duplicate codes | `ICodeGenerator<T>.NextCodeAsync()` (see `references/code-generation.md`) |
| 5 | Only Create validator, no Update | Update accepts invalid data | Always create validators in pairs |
| 6 | `TenantId!.Value` null-forgiving | 500 instead of 400 | `?? throw new TenantContextRequiredException()` |
