# SmartStack Domain API Reference

> **Source of truth:** `SmartStack.app/src/SmartStack.Domain/Common/`
> **Loaded by:** step-01 (analyze), step-03 (execute)

---

## 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)

```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)
}
```

---

## Entity Pattern (tenant-scoped, most common)

```csharp
using SmartStack.Domain.Common;

namespace {ProjectName}.Domain.Entities.{App}.{Module};

public class {Name} : BaseEntity, ITenantEntity, IAuditableEntity
{
    // ITenantEntity
    public Guid TenantId { get; private set; }

    // IAuditableEntity
    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }

    // Business properties (add your own)
    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;
    }
}
```

---

## Entity Pattern (platform-level, no tenant)

```csharp
public class {Name} : BaseEntity, IAuditableEntity
{
    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!;

    private {Name}() { }

    public static {Name} Create(string code, string name)
    {
        return new {Name}
        {
            Id = Guid.NewGuid(),
            Code = code.ToLowerInvariant(),
            Name = name,
            CreatedAt = DateTime.UtcNow
        };
    }
}
```

### Entity Pattern — Cross-Tenant (IOptionalTenantEntity)

For entities that can be shared across tenants (e.g., Department, Currency). TenantId is nullable — null means shared, Guid means tenant-specific. The user decides the scope at creation time.

```csharp
public class {Name} : BaseEntity, IOptionalTenantEntity, IAuditableEntity
{
    // TenantId nullable — null = shared across all tenants
    public Guid? TenantId { get; private set; }

    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }

    // Business properties
    public string Code { get; private set; } = string.Empty;
    public string Name { get; private set; } = string.Empty;

    private {Name}() { }

    /// <param name="tenantId">null = shared (cross-tenant), Guid = tenant-specific</param>
    public static {Name} Create(Guid? tenantId = null, string code, string name)
    {
        return new {Name}
        {
            Id = Guid.NewGuid(),
            TenantId = tenantId,
            Code = code.ToLowerInvariant(),
            Name = name,
            CreatedAt = DateTime.UtcNow
        };
    }
}
```

**EF Core global query filter (already in SmartStack.app CoreDbContext):**
```csharp
builder.HasQueryFilter(e => !ShouldFilterByTenant || e.TenantId == null || e.TenantId == CurrentTenantId);
```
This automatically includes shared (null) + current tenant data in all queries.

**Service pattern for optional tenant:**
```csharp
// No guard clause — tenantId is nullable
var tenantId = _currentTenant.TenantId; // null = creating shared data
var entity = Department.Create(tenantId, dto.Code, dto.Name);
```

### Entity Pattern — Scoped (IScopedTenantEntity)

For entities with explicit visibility control via EntityScope enum (Tenant, Shared, Platform).

```csharp
public class {Name} : BaseEntity, IScopedTenantEntity, IAuditableEntity
{
    public Guid? TenantId { get; private set; }
    public EntityScope Scope { get; private set; }

    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }

    private {Name}() { }

    public static {Name} Create(Guid? tenantId = null, EntityScope scope = EntityScope.Tenant)
    {
        if (scope == EntityScope.Tenant && tenantId == null)
            throw new ArgumentException("TenantId is required when scope is Tenant");

        return new {Name}
        {
            Id = Guid.NewGuid(),
            TenantId = tenantId,
            Scope = scope,
            CreatedAt = DateTime.UtcNow
        };
    }
}
```

### Entity Pattern — Person Extension (User-linked roles)

For entities representing a "person role" of a User (Employee, Customer, Contact). Links entity to User instead of duplicating personal data.

> **Full reference:** See `references/person-extension-pattern.md` for complete entity, config, service, DTO, and frontend patterns with C# templates, decision guide, and examples.

**Two variants:**

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

**IPersonExtension interface:**
```csharp
public interface IPersonExtension
{
    Guid? UserId { get; }
    User? User { get; }
}
```

**Mandatory variant (entity):**
```csharp
public class Employee : BaseEntity, ITenantEntity, IAuditableEntity, IPersonExtension
{
    public Guid TenantId { get; private set; }
    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }

    // Person Extension — mandatory UserId
    public Guid UserId { get; private set; }
    public User? User { get; private set; }

    // Business properties ONLY — no duplicate person fields
    public string Code { get; private set; } = null!;
    public DateOnly HireDate { get; private set; }
}
```

**Optional variant (entity):**
```csharp
public class Customer : BaseEntity, ITenantEntity, IAuditableEntity, IPersonExtension
{
    public Guid TenantId { get; private set; }
    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }

    // Person Extension — optional UserId
    public Guid? UserId { get; private set; }
    public User? User { get; private set; }

    // Person fields (used only when UserId is null)
    public string FirstName { get; private set; } = null!;
    public string LastName { get; private set; } = null!;
    public string? Email { get; private set; }

    // Computed — resolve from User if linked, else own fields
    public string DisplayFirstName => User?.FirstName ?? FirstName;
    public string DisplayLastName => User?.LastName ?? LastName;
    public string? DisplayEmail => User?.Email ?? Email;
}
```

**Key rules:**
- **EF Config (mandatory):** Unique index on `(TenantId, UserId)` without filter
- **EF Config (optional):** Unique index on `(TenantId, UserId)` with `.HasFilter("[UserId] IS NOT NULL")`
- **Service:** Include User on all queries. Verify User exists and no duplicate on CreateAsync.
- **DTO:** Mandatory CreateDto requires `UserId`. 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
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

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
        // builder.HasMany(x => x.Children)
        //     .WithOne(x => x.Parent)
        //     .HasForeignKey(x => x.ParentId)
        //     .OnDelete(DeleteBehavior.Restrict);

        // Seed data (if applicable)
        // builder.HasData({Name}SeedData.GetSeedData());
    }
}
```

---

## Service Pattern (tenant-scoped, required)

> All services must inject `ICurrentUserService` + `ICurrentTenantService` and filter by `TenantId`. Missing TenantId is an OWASP A01 vulnerability.

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SmartStack.Application.Common.Interfaces.Identity;
using SmartStack.Application.Common.Interfaces.Tenants;
using SmartStack.Application.Common.Interfaces.Persistence;

namespace {ProjectName}.Infrastructure.Services.{App}.{Module};

public class {Name}Service : I{Name}Service
{
    private readonly IExtensionsDbContext _db;
    private readonly ICurrentUserService _currentUser;
    private readonly ICurrentTenantService _currentTenant;
    private readonly ILogger<{Name}Service> _logger;

    public {Name}Service(
        IExtensionsDbContext db,
        ICurrentUserService currentUser,
        ICurrentTenantService currentTenant,
        ILogger<{Name}Service> logger)
    {
        _db = db;
        _currentUser = currentUser;
        _currentTenant = currentTenant;
        _logger = logger;
    }

    public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
        string? search = null,
        int page = 1,
        int pageSize = 20,
        CancellationToken ct = default)
    {
        // Guard clause — throws 400 if no tenant context (e.g., missing X-Tenant-Slug header)
        var tenantId = _currentTenant.TenantId
            ?? throw new TenantContextRequiredException();

        var query = _db.{Name}s
            .Where(x => x.TenantId == tenantId)  // Required tenant filter
            .AsNoTracking();

        // Search filter — enables EntityLookup on frontend
        if (!string.IsNullOrWhiteSpace(search))
        {
            query = query.Where(x =>
                x.Name.Contains(search) ||
                x.Code.Contains(search));
        }

        var totalCount = await query.CountAsync(ct);
        var items = await query
            .OrderBy(x => x.Name)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
            .ToListAsync(ct);

        return new PaginatedResult<{Name}ResponseDto>(items, totalCount, page, pageSize);
    }

    public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
    {
        var tenantId = _currentTenant.TenantId
            ?? throw new TenantContextRequiredException();

        return await _db.{Name}s
            .Where(x => x.Id == id && x.TenantId == tenantId)  // Required
            .AsNoTracking()
            .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
            .FirstOrDefaultAsync(ct);
    }

    public async Task<{Name}ResponseDto> CreateAsync(Create{Name}Dto dto, CancellationToken ct)
    {
        var tenantId = _currentTenant.TenantId
            ?? throw new TenantContextRequiredException();

        var entity = {Name}.Create(
            tenantId: tenantId,  // Required — never Guid.Empty
            code: dto.Code,
            name: dto.Name);

        entity.CreatedBy = _currentUser.UserId?.ToString();

        _db.{Name}s.Add(entity);
        await _db.SaveChangesAsync(ct);

        _logger.LogInformation("Created {Entity} {Id} for tenant {TenantId}",
            nameof({Name}), entity.Id, tenantId);

        return new {Name}ResponseDto(entity.Id, entity.Code, entity.Name, entity.CreatedAt);
    }

    public async Task DeleteAsync(Guid id, CancellationToken ct)
    {
        var tenantId = _currentTenant.TenantId
            ?? throw new TenantContextRequiredException();

        var entity = await _db.{Name}s
            .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, ct)
            ?? throw new KeyNotFoundException($"{Name} {id} not found");

        _db.{Name}s.Remove(entity);
        await _db.SaveChangesAsync(ct);
    }
}
```

**Key interfaces (from SmartStack NuGet package):**
- `ICurrentUserService` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `UserId` (Guid?), `Email` (string?), `IsAuthenticated` (bool)
- `ICurrentTenantService` (from `SmartStack.Application.Common.Interfaces.Tenants`): provides `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();
```
This converts a null TenantId into a clean 400 Bad Request response via `GlobalExceptionHandlerMiddleware`.
**IMPORTANT:** Uses `TenantContextRequiredException` (400), NOT `UnauthorizedAccessException` (401). A missing tenant is a bad request, not an auth failure — the JWT is valid, `[Authorize]` passed.

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

---

## Controller Pattern (NavRoute)

```csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SmartStack.Api.Routing;
using SmartStack.Api.Authorization;

namespace {ProjectName}.Api.Controllers.{App};

[ApiController]
[NavRoute("{app}.{module}")]
[Authorize]
public class {Name}Controller : ControllerBase
{
    private readonly I{Name}Service _service;
    private readonly ILogger<{Name}Controller> _logger;

    public {Name}Controller(I{Name}Service service, ILogger<{Name}Controller> logger)
    {
        _service = service;
        _logger = logger;
    }

    [HttpGet]
    [RequirePermission(Permissions.{Module}.Read)]
    public async Task<ActionResult<PaginatedResult<{Name}ResponseDto>>> GetAll(
        [FromQuery] string? search = null,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        CancellationToken ct = default)
        => Ok(await _service.GetAllAsync(search, page, pageSize, ct));

    [HttpGet("{id:guid}")]
    [RequirePermission(Permissions.{Module}.Read)]
    public async Task<ActionResult<{Name}ResponseDto>> GetById(Guid id, CancellationToken ct)
    {
        var result = await _service.GetByIdAsync(id, ct);
        return result is null ? NotFound() : Ok(result);
    }

    [HttpPost]
    [RequirePermission(Permissions.{Module}.Create)]
    public async Task<ActionResult<{Name}ResponseDto>> Create([FromBody] Create{Name}Dto dto, CancellationToken ct)
    {
        var result = await _service.CreateAsync(dto, ct);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }

    [HttpPut("{id:guid}")]
    [RequirePermission(Permissions.{Module}.Update)]
    public async Task<ActionResult<{Name}ResponseDto>> Update(Guid id, [FromBody] Update{Name}Dto dto, CancellationToken ct)
    {
        var result = await _service.UpdateAsync(id, dto, ct);
        return result is null ? NotFound() : Ok(result);
    }

    [HttpDelete("{id:guid}")]
    [RequirePermission(Permissions.{Module}.Delete)]
    public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
    {
        await _service.DeleteAsync(id, ct);
        return NoContent();
    }
}
```

Route attribute rules:
- `[NavRoute]` is the ONLY route attribute needed — it resolves routes dynamically from Navigation entities at startup
- Do not use `[Route("api/...")]` alongside `[NavRoute]` — causes route conflicts and 404s at runtime
- Do not use `[Route("api/[controller]")]` — this is standard ASP.NET Core, NOT SmartStack
- If a controller has `[NavRoute]`, there must be NO `[Route]` attribute on the class

Use `[RequirePermission(Permissions.{Module}.{Action})]` on every endpoint — do not use `[Authorize]` alone (no RBAC enforcement).

Permission paths must use identical segments to NavRoute codes (kebab-case):
- NavRoute: `human-resources.employees` → Permission: `human-resources.employees.read`
- NavRoute: `human-resources.employees.leaves` → Permission: `human-resources.employees.leaves.read`
- Do not use: `humanresources.employees.read` (no kebab-case — mismatches NavRoute)
- SmartStack.app convention: `support-client.my-tickets.read` (always kebab-case)

### Section-Level Controller (NavRoute with 3 segments)

When a module has sections, each section gets its own controller with a 3-segment navRoute:

```csharp
// Section-level controller: navRoute has 3 segments
[ApiController]
[NavRoute("{app}.{module}.{section}")]
[Authorize]
public class {Section}Controller : ControllerBase
{
    // Example: human-resources.employees.departments
    [HttpGet]
    [RequirePermission(Permissions.{Section}.Read)]
    public async Task<ActionResult<PaginatedResult<{Section}ResponseDto>>> GetAll(
        [FromQuery] string? search = null,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        CancellationToken ct = default)
        => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
}
```

**NavRoute segment rules (minimum 2 segments):**
| Level | NavRoute format | Example |
|-------|----------------|---------|
| Module | `{app}.{module}` (2 segments) | `administration.users` |
| Section | `{app}.{module}.{section}` (3 segments) | `administration.users.groups` |
| Sub-resource (Suffix) | `{app}.{module}` + Suffix | `administration.ai` + `Suffix = "prompts"` |

> **POST-CONTEXT REMOVAL:** The old "context" level (1st segment) has been removed.
> Before: `platform.administration.users` (3 segments). After: `administration.users` (2 segments).
> The NavigationRouteRegistryBuilder builds 2-segment paths from DB (application.module).
> Controllers with 3+ segments use the fallback route generator (dots → slashes).

**Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)

**NavRoute resolves at startup from DB:** `administration.users` → `api/administration/users`

### Sub-Resource Pattern (NavRoute Suffix)

When an entity is a child of another entity (e.g., AiPrompts under AI), use `[NavRoute(..., Suffix = "...")]`:

```csharp
// Sub-resource controller: prompts are nested under AI module
[ApiController]
[NavRoute("administration.ai", Suffix = "prompts")]
[Authorize]
public class AiPromptsController : ControllerBase
{
    [HttpGet]
    [RequirePermission(Permissions.Ai.Prompts.Read)]
    public async Task<ActionResult<PaginatedResult<AiPromptResponseDto>>> GetAll(...)
        => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
}
```

**Alternative pattern** (sub-resource endpoints within parent controller):
```csharp
// LeaveTypes as endpoints within LeavesController
[HttpGet("types")]
[RequirePermission(Permissions.Leaves.Read)]
public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAllLeaveTypes(...)
```

> Sub-resource frontend completeness:
> If a parent page has a button (e.g., "Manage Leave Types") that `navigate()`s to a sub-resource route,
> the frontend must include a page component for that route. Otherwise → dead link → white screen.
> - Either create a dedicated sub-resource ListPage (e.g., `LeaveTypesPage.tsx`)
> - Or do not include the navigate() button if pages won't be created
> - Prefer separate controllers (with Suffix) over sub-endpoints in parent controller — easier to route

---

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

> **The navigation seed data defines menu routes stored in DB. These routes MUST be full paths starting with `/`.**
> Short routes (e.g., `humanresources`) cause 400 Bad Request on application-tracking.

### Route Convention

| 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` |
| Resource | `/{app-kebab}/{module-kebab}/{section-kebab}/{resource-kebab}` | `/human-resources/employees/departments/export` |

**Route special cases (list and detail sections):**
> The `list` and `detail` sections are NOT functional sub-areas — they are view modes of the module itself.
> Their navigation routes must NOT add extra segments:
> - `list` section route = module route (e.g., `/human-resources/employees`)
> - `detail` section route = module route + `/:id` (e.g., `/human-resources/employees/:id`)
> - Do not use: `/employees/list`, `/employees/detail/:id`
> - Other sections (dashboard, approve, import, etc.) = module route + `/{section-kebab}` (normal behavior)

**Rules:**
- Routes ALWAYS start with `/`
- Routes ALWAYS include the full hierarchy from application to current level
- Routes ALWAYS use kebab-case (NOT PascalCase, NOT camelCase)
- Code identifiers stay PascalCase in C# (`HumanResources`) but routes are kebab-case (`human-resources`)

### ToKebabCase Helper (include in SeedConstants or SeedDataProvider)

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

### GUID Generation Rule

```csharp
// ALWAYS use Guid.NewGuid() for ALL seed data IDs
// Avoids conflicts between projects/tenants/environments
// Idempotence is handled by Code-based lookups at runtime
// Navigation entities are resolved by Code, not by fixed GUIDs
```

### Navigation Seed Data Example

```csharp
// Application: /human-resources
var app = NavigationApplication.Create(
    "human-resources", "Human Resources", "HR Management",
    "Users", IconType.Lucide,
    "/human-resources",  // FULL PATH — starts with /, kebab-case
    10);

// Module: /human-resources/employees
var module = NavigationModule.Create(
    app.Id, "employees", "Employees", "Employee management",
    "UserCheck", IconType.Lucide,
    "/human-resources/employees",  // FULL PATH — includes parent
    10);

// Section: /human-resources/employees/departments
var section = NavigationSection.Create(
    module.Id, "departments", "Departments", "Manage departments",
    "Building2", IconType.Lucide,
    "/human-resources/employees/departments",  // FULL PATH
    10);
```

### Avoid in Seed Data

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

---

## DbContext Pattern (extensions)

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

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

---

## DI Registration Pattern

```csharp
// In DependencyInjection.cs or ServiceCollectionExtensions.cs:
services.AddScoped<I{Name}Service, {Name}Service>();
services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
```

---

## DTO Type Mapping

> **Use the correct .NET type for each property.** Incorrect types cause runtime parsing errors.

| Property Pattern | .NET Type | JSON Format | Example |
|-----------------|-----------|-------------|---------|
| `*Date`, `StartDate`, `EndDate`, `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 in DTOs:
- `string Date` / `string StartDate` — use `DateOnly`
- `string Time` — use `TimeOnly`
- `DateTime BirthDate` — use `DateOnly` (no time component needed)
- `int` for hours/duration — use `decimal` for fractional values

---

## 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` in config | Does NOT exist in BaseEntity |
| `e.IsDeleted` filter | Does NOT exist — no soft delete |
| `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 (resolves route from DB at startup). Remove `[Route]` when `[NavRoute]` is present. |
| `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
| `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
| `tenantId: Guid.Empty` in services | OWASP A01 — always use validated `_currentTenant.TenantId` |
| Service without `ICurrentTenantService` | All tenant data leaks — inject `ICurrentTenantService` |
| `ICurrentUser` in service code | Does NOT exist — use `ICurrentUserService` + `ICurrentTenantService` |
| `_currentTenant.TenantId!.Value` | Crashes with 500 — use `?? throw new TenantContextRequiredException()` |
| `UnauthorizedAccessException("Tenant context is required")` | Returns 401 → clears frontend token. Use `TenantContextRequiredException()` (400) |
| Route `"humanresources"` in seed data | Must be full path `"/human-resources"` |
| Route without leading `/` | All routes must start with `/` |
| `humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `human-resources.employees.read` |
| `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
| `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
| `string Date` in DTO | Date-only fields must use `DateOnly`, not `string` |
| `DateTime` for date-only | Use `DateOnly` when no time component needed |
| FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
| `PagedResult<T>` / `PaginatedResultDto<T>` | Use `PaginatedResult<T>` instead |
| Controller injects `DbContext` | Create an Application service and inject it instead (Clean Architecture) |
| Domain entity has `[Table]` attribute | Infrastructure concern in Domain — move to `IEntityTypeConfiguration<T>` in Infrastructure |
| `using Microsoft.EntityFrameworkCore` in Domain | EF Core belongs in Infrastructure, not Domain — Domain must be persistence-ignorant |
| Controller returns `Employee` entity | Domain leak in API response — return `EmployeeResponseDto` instead |
| `IEmployeeService.cs` in Infrastructure | Interface belongs in Application — move to `Application/Interfaces/` |
| Service implementation in Application layer | Implementations belong in Infrastructure — Application only contains interfaces |
| Controller injects `IRepository<T>` directly | Controllers use Application services, not repositories — add a service layer |
| `FirstName`/`LastName`/`Email` on mandatory person extension entity | These come from User — use `Display*` computed properties from the User join |
| Missing `Include(x => x.User)` on person extension service | Person extension entities MUST always include User for display fields |
| Missing unique index on `(TenantId, UserId)` for person extension | One person-role per User per Tenant — add unique constraint in EF config |

---

## External Application & Data Export Pattern

### Entities

| Entity | Table | Description |
|--------|-------|-------------|
| `ExternalApplication` | `auth_ExternalApplications` | Machine-to-machine API account (ClientId, ClientSecret, IsActive, IP whitelist) |
| `ExternalApplicationRole` | `auth_ExternalApplicationRoles` | Role assignment per app (AppId, RoleId, optional TenantId) |
| `ExternalApplicationExportAccess` | `auth_ExternalApplicationExportAccesses` | Per-app access to specific export endpoint (IsEnabled, RateLimitPerMinute, MaxPageSize) |
| `DataExportEndpoint` | `auth_DataExportEndpoints` | Registry of available export APIs (Code, RouteTemplate, RequiredPermission, EntityType) |
| `ExternalAppAuditLog` | `auth_ExternalAppAuditLogs` | Audit trail for all API calls (Authentication, DataExport actions) |

### Architecture (3-layer security)

1. **Authentication** — JWT assertion signed with ClientSecret → ExternalApplicationAuthService validates → generates SmartStack JWT with permissions resolved via ExternalApplicationRole → Role → RolePermission chain
2. **Authorization** — RequirePermissionFilter checks JWT claims (e.g., `administration.users.export`)
3. **Access Control** — DataExportAccessMiddleware verifies per-app endpoint access in ExternalApplicationExportAccess table

### Rate Limiting

ExternalAppRateLimitPolicy resolves limits: app override → endpoint default → 60/min fallback.
Partition key: `{clientId}:{endpointCode}`.

### Seed Data

DataExportEndpoints are seeded in DataExportEndpointConfiguration.cs with FK to NavigationApplication and NavigationModule. Each endpoint maps to a controller in `Controllers/DataExport/v1/`.

### Controller Pattern

Export controllers: `[Route("api/v1/export")]` + `[RequirePermission]` + `[EnableRateLimiting]`

**Query Parameters (all export endpoints except Navigation):**
- `tenantId` (UUID, required) — Scopes the export to the specified tenant's data

Example: `GET /api/v1/export/users?tenantId=550e8400-e29b-41d4-a716-446655440000&page=1&pageSize=50`

Management controllers: `[NavRoute("api.accounts")]` with CustomSegment

---

## PaginatedResult Pattern

> **Canonical type for ALL paginated responses.** One name, one contract, everywhere.

### Definition (Backend — `SmartStack.Application.Common.Models`)

```csharp
namespace 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

```csharp
namespace SmartStack.Application.Common.Extensions;

public static class QueryableExtensions
{
    public const int MaxPageSize = 100;

    public static async Task<PaginatedResult<T>> ToPaginatedResultAsync<T>(
        this IQueryable<T> query,
        int page = 1,
        int pageSize = 20,
        CancellationToken ct = default)
    {
        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 1, MaxPageSize);

        var totalCount = await query.CountAsync(ct);
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(ct);

        return new PaginatedResult<T>(items, totalCount, page, pageSize);
    }
}
```

### Usage in Service (with search + extension method)

```csharp
public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
    string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
{
    var tenantId = _currentTenant.TenantId
        ?? throw new TenantContextRequiredException();

    var query = _db.{Name}s
        .Where(x => x.TenantId == tenantId)
        .AsNoTracking();

    if (!string.IsNullOrWhiteSpace(search))
        query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));

    return await query
        .OrderBy(x => x.Name)
        .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
        .ToPaginatedResultAsync(page, pageSize, ct);
}
```

### Frontend Types (`@/types/pagination.ts`)

```typescript
export interface PaginatedResult<T> {
  items: T[];
  totalCount: number;
  page: number;
  pageSize: number;
  totalPages: number;
  hasPreviousPage: boolean;
  hasNextPage: boolean;
}

export interface PaginationParams {
  page?: number;
  pageSize?: number;
  search?: string;
  sortBy?: string;
  sortDirection?: 'asc' | 'desc';
}
```

### Type Names to Avoid

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

### Rules

- **Max pageSize = 100** — enforced via `Math.Clamp(pageSize, 1, 100)` or extension method
- **Default page = 1, pageSize = 20** — all GetAll endpoints
- **Search param mandatory** — enables `EntityLookup` on frontend
- **POST-CHECK C12** blocks `List<T>` returns on GetAll
- **POST-CHECK C28** blocks non-canonical pagination type names

---

## Critical Anti-Patterns (with code examples)

> **These are the most common and dangerous mistakes.** Each one has been observed in production code generation.

### Anti-Pattern 1: HasQueryFilter with `!= Guid.Empty` (Security vulnerability)

The `HasQueryFilter` in EF Core should use **runtime tenant resolution**, NOT a static comparison against `Guid.Empty`.

**INCORRECT — Does NOT isolate tenants:**
```csharp
// WRONG: This only excludes empty GUIDs — ALL tenant data is still visible to everyone!
public void Configure(EntityTypeBuilder<MyEntity> builder)
{
    builder.HasQueryFilter(e => e.TenantId != Guid.Empty);
}
```

**CORRECT — Tenant isolation via service:**
```csharp
// Correct: In SmartStack, tenant filtering is done in the SERVICE layer, not via HasQueryFilter.
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
{
    var query = _db.MyEntities
        .Where(x => x.TenantId == _currentUser.TenantId)  // Required runtime filter
        .AsNoTracking();
    // ...
}
```

**Why it's wrong:** `HasQueryFilter(e => e.TenantId != Guid.Empty)` is a **static filter** — it only removes records with empty GUIDs. It does NOT restrict data to the current tenant. This is an **OWASP A01 Broken Access Control** vulnerability.

---

### Anti-Pattern 2: `List<T>` instead of `PaginatedResult<T>` for GetAll

**INCORRECT — No pagination:**
```csharp
// WRONG: Returns all records at once — no pagination, no totalCount
public async Task<List<MyEntityDto>> GetAllAsync(CancellationToken ct)
{
    return await _db.MyEntities
        .Where(x => x.TenantId == _currentUser.TenantId)
        .Select(x => new MyEntityDto(x.Id, x.Code, x.Name))
        .ToListAsync(ct);
}
```

**CORRECT — Paginated with search:**
```csharp
// Correct: Returns PaginatedResult<T> with search, page, pageSize
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(
    string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
{
    var query = _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).AsNoTracking();
    if (!string.IsNullOrWhiteSpace(search))
        query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
    var totalCount = await query.CountAsync(ct);
    var items = await query.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize)
        .Select(x => new MyEntityDto(x.Id, x.Code, x.Name, x.CreatedAt)).ToListAsync(ct);
    return new PaginatedResult<MyEntityDto>(items, totalCount, page, pageSize);
}
```

**Why it's wrong:** `List<T>` loads ALL records into memory. It also breaks `EntityLookup` which requires `{ items, totalCount }` response format.

---

### Anti-Pattern 3: Missing `IAuditableEntity` on tenant entities

**INCORRECT — No audit trail:**
```csharp
// WRONG: Tenant entity without IAuditableEntity
public class MyEntity : BaseEntity, ITenantEntity
{
    public Guid TenantId { get; private set; }
    public string Code { get; private set; } = null!;
}
```

**CORRECT — Always pair ITenantEntity with IAuditableEntity:**
```csharp
public class MyEntity : BaseEntity, ITenantEntity, IAuditableEntity
{
    public Guid TenantId { get; private set; }
    public string? CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }
    public string Code { get; private set; } = null!;
}
```

**Why it's wrong:** Without `IAuditableEntity`, there is no record of who created or modified data. Mandatory for compliance in multi-tenant environments.

---

### Anti-Pattern 4: Code auto-generation with `Count() + 1`

**INCORRECT — Race condition:**
```csharp
// WRONG: Two concurrent requests get the same count
var count = await _db.MyEntities.Where(x => x.TenantId == tenantId).CountAsync(ct);
return $"emp-{(count + 1):D5}";
```

**CORRECT — Use `ICodeGenerator<T>.NextCodeAsync()` (atomic with retry):**
```csharp
private readonly ICodeGenerator<MyEntity> _codeGenerator;

// In CreateAsync:
var code = await _codeGenerator.NextCodeAsync(ct);
var entity = MyEntity.Create(tenantId, code, dto.Name, createdBy: null);
```

**Why it's wrong:** `Count() + 1` causes **race conditions** — concurrent requests generate duplicate codes. `ICodeGenerator<T>` uses `OrderByDescending` on existing codes + retry on unique constraint violation for safe concurrency.

**Key rules when using auto-generated codes:**
- **REMOVE** `Code` from `CreateDto` (auto-generated, not user-provided)
- **KEEP** `Code` in `ResponseDto` (returned to frontend)
- Register `ICodeGenerator<T>` in DI with `CodePatternConfig`
- Code regex in validators: `^[a-z0-9_-]+$` (supports hyphens)

**Full reference:** See `references/code-generation.md` for strategies (sequential, timestamp, yearly, UUID), volume-to-digits calculation, and complete implementation patterns.

---

### Anti-Pattern 5: Missing Update validator

**INCORRECT — Only CreateValidator:**
```csharp
public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto>
{
    public CreateMyEntityDtoValidator()
    {
        RuleFor(x => x.Code).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
    }
}
// No UpdateMyEntityDtoValidator exists!
```

**CORRECT — Always create validators in pairs:**
```csharp
public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto> { /* ... */ }
public class UpdateMyEntityDtoValidator : AbstractValidator<UpdateMyEntityDto>
{
    public UpdateMyEntityDtoValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
    }
}
```

**Why it's wrong:** Without an `UpdateValidator`, the Update endpoint accepts **any data without validation**.

---

### Anti-Pattern 6: `TenantId!.Value` null-forgiving operator

The `!` (null-forgiving) operator followed by `.Value` on a `Guid?` suppresses compiler warnings but **throws `InvalidOperationException` at runtime** when TenantId is null.

**INCORRECT — Crashes with 500 Internal Server Error:**
```csharp
// Wrong: Throws InvalidOperationException("Nullable object must have a value") → 500 error
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
{
    var tenantId = _currentTenant.TenantId!.Value;  // Crashes if no tenant context
    var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
    // ...
}
```

**CORRECT — Clean 400 via GlobalExceptionHandlerMiddleware:**
```csharp
// Correct: Throws TenantContextRequiredException → middleware converts to 400 Bad Request
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
{
    var tenantId = _currentTenant.TenantId
        ?? throw new TenantContextRequiredException();
    var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
    // ...
}
```

**Why `!.Value` is wrong:** When a user hits an API via Swagger with a valid JWT but no tenant context (missing `X-Tenant-Slug` header), `TenantId` is null. The `!.Value` pattern produces an opaque `500 Internal Server Error` instead of a clear `400 Bad Request` with an actionable message.

**Why `UnauthorizedAccessException` is wrong:** A missing tenant is NOT an auth failure — the JWT is valid, `[Authorize]` passed. Using `UnauthorizedAccessException` returns 401, which triggers the frontend interceptor to clear the token and redirect to login. Use `TenantContextRequiredException` instead (returns 400, does not clear the token).
