# 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.{Context}.{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
        };
    }
}
```

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

> **CRITICAL:** ALL services MUST inject `ICurrentUserService` + `ICurrentTenantService` and filter by `TenantId`. Missing TenantId = 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.{Context}.{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)
    {
        // MANDATORY guard — 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)  // MANDATORY 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)  // MANDATORY
            .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,  // MANDATORY — 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)

**MANDATORY 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.

**FORBIDDEN 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.{Context}.{App};

[ApiController]
[NavRoute("{context}.{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();
    }
}
```

**CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).

**CRITICAL — Permission paths use IDENTICAL segments to NavRoute codes (kebab-case):**
- NavRoute: `business.human-resources.employees` → Permission: `business.human-resources.employees.read`
- NavRoute: `business.human-resources.employees.leaves` → Permission: `business.human-resources.employees.leaves.read`
- FORBIDDEN: `business.humanresources.employees.read` (no kebab-case — mismatches NavRoute)
- SmartStack.app convention: `business.support-client.my-tickets.read` (always kebab-case)

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

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

```csharp
// Section-level controller: navRoute has 4 segments
[ApiController]
[NavRoute("{context}.{app}.{module}.{section}")]
[Authorize]
public class {Section}Controller : ControllerBase
{
    // Example: business.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:**
| Level | NavRoute format | Example |
|-------|----------------|---------|
| Module | `{context}.{app}.{module}` (3 segments) | `business.human-resources.employees` |
| Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.human-resources.employees.departments` |

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

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

### Sub-Resource Pattern (NavRoute Suffix)

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

```csharp
// Sub-resource controller: types are nested under leaves
[ApiController]
[NavRoute("business.human-resources.employees.leaves", Suffix = "types")]
[Authorize]
public class LeaveTypesController : ControllerBase
{
    [HttpGet]
    [RequirePermission(Permissions.Leaves.Read)]  // inherits parent section permission
    public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> 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(...)
```

> **CRITICAL — 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 DON'T 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 (CRITICAL — 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 | `/{context}/{app-kebab}` | `/business/human-resources` |
| Module | `/{context}/{app-kebab}/{module-kebab}` | `/business/human-resources/employees` |
| Section | `/{context}/{app-kebab}/{module-kebab}/{section-kebab}` | `/business/human-resources/employees/departments` |
| Resource | `/{context}/{app-kebab}/{module-kebab}/{section-kebab}/{resource-kebab}` | `/business/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., `/business/human-resources/employees`)
> - `detail` section route = module route + `/:id` (e.g., `/business/human-resources/employees/:id`)
> - FORBIDDEN: `/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 context 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();
```

### SeedConstants Pattern

> **CRITICAL — NavigationContext IDs are NEVER generated.**
> Contexts (`business`, `platform`, `personal`) are pre-seeded by SmartStack core with hardcoded GUIDs.
> You MUST query the context by code at runtime in the SeedDataProvider:
> `var ctx = await db.NavigationContexts.FirstOrDefaultAsync(c => c.Code == "business", ct);`
> **FORBIDDEN:** `DeterministicGuid("nav:business")` or any ContextId constant in SeedConstants.

```csharp
public static class SeedConstants
{
    // Deterministic GUIDs (SHA256-based, reproducible across environments)
    // NOTE: Application/Module/Section/Resource IDs are deterministic.
    //       Context IDs are NOT — they are pre-seeded by SmartStack core.
    public static readonly Guid ApplicationId = DeterministicGuid("nav:business.human-resources");
    public static readonly Guid ModuleId = DeterministicGuid("nav:business.human-resources.employees");
    public static readonly Guid SectionId = DeterministicGuid("nav:business.human-resources.employees.departments");

    // FORBIDDEN — Context IDs are NOT deterministic, they come from SmartStack core:
    // public static readonly Guid BusinessContextId = DeterministicGuid("nav:business"); // WRONG!

    private static Guid DeterministicGuid(string input)
    {
        var hash = System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes(input));
        var bytes = new byte[16];
        Array.Copy(hash, bytes, 16);
        bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50); // version 5
        bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // variant
        return new Guid(bytes);
    }
}
```

### Navigation Seed Data Example

```csharp
// CRITICAL: Look up the context by code — NEVER use a hardcoded/deterministic GUID
var businessCtx = await db.NavigationContexts
    .FirstOrDefaultAsync(c => c.Code == "business", ct);
if (businessCtx == null) return; // Context not yet seeded by SmartStack core

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

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

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

### FORBIDDEN in Seed Data

| Mistake | Reality |
|---------|---------|
| `"humanresources"` as route | Must be `"/business/human-resources"` (full path, kebab-case) |
| `"employees"` as route | Must be `"/business/human-resources/employees"` (includes parent) |
| `Guid.NewGuid()` in seed data | Must use deterministic GUIDs (SHA256) |
| `DeterministicGuid("nav:business")` for ContextId | Context IDs are pre-seeded by SmartStack core — look up by code |
| 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>();
```

---

## 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] + [NavRoute]` | Only `[NavRoute]` needed (resolves route from DB) |
| `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 `"/business/human-resources"` |
| Route without leading `/` | All routes must start with `/` |
| `business.humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `business.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 |
| FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
| `PagedResult<T>` / `PaginatedResultDto<T>` | FORBIDDEN — use `PaginatedResult<T>` only |

---

## 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';
}
```

### FORBIDDEN Type Names

| Forbidden | Canonical Replacement |
|-----------|----------------------|
| `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 16** blocks `List<T>` returns on GetAll
- **POST-CHECK 31** 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 — OWASP A01)

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)  // MANDATORY 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 (RUNTIME CRASH)

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;  // CRASH 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).
