# SmartStack Controller Templates

> **⚠️ OBSOLETE - DO NOT USE THESE TEMPLATES MANUALLY**
>
> **The `/controller` skill now uses the MCP `scaffold_extension` tool to generate controllers.**
> These templates are kept for reference only. All controller generation MUST go through the MCP to ensure:
> - ✅ `[NavRoute]` attribute is included for frontend/backend sync
> - ✅ Permissions are correctly generated
> - ✅ Consistency with SmartStack conventions
>
> **To generate a controller, use:** `/controller` skill which calls MCP `scaffold_extension` automatically.
>
> **If you modify these templates, they will NOT be used.** The MCP templates in `templates/mcp-scaffolding/controller.cs.hbs` are the source of truth.

---

## Template CRUD Controller (Standard)

```csharp
// src/SmartStack.Api/Controllers/{Area}/{Module}Controller.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Authorization;
using SmartStack.Application.Common.Interfaces;
using SmartStack.Api.Authorization;
using SmartStack.Domain.{DomainNamespace};

namespace SmartStack.Api.Controllers.{Area};

[ApiController]
[NavRoute("{area}.{module}")]
[Microsoft.AspNetCore.Authorization.Authorize]
[Produces("application/json")]
[Tags("{Module}")]
public class {Module}Controller : ControllerBase
{
    private readonly ISender _mediator;  // MediatR — NOT IApplicationDbContext
    private readonly ICurrentUserService _currentUser;
    private readonly ILogger<{Module}Controller> _logger;

    public {Module}Controller(
        IApplicationDbContext context,
        ICurrentUserService currentUser,
        ILogger<{Module}Controller> logger)
    {
        _context = context;
        _currentUser = currentUser;
        _logger = logger;
    }

    #region GET - List with Pagination

    [HttpGet]
    [RequirePermission(Permissions.{PermissionClass}.View)]
    [ProducesResponseType(typeof(PaginatedResult<{Entity}ListDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PaginatedResult<{Entity}ListDto>>> Get{Module}(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string? search = null,
        CancellationToken cancellationToken = default)
    {
        var query = _context.{DbSet}.AsQueryable();

        // Search filter
        if (!string.IsNullOrWhiteSpace(search))
        {
            var searchLower = search.ToLower();
            query = query.Where(x =>
                x.Name.ToLower().Contains(searchLower) ||
                x.Description != null && x.Description.ToLower().Contains(searchLower));
        }

        var totalCount = await query.CountAsync(cancellationToken);

        var items = await query
            .OrderBy(x => x.Name)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new {Entity}ListDto(
                x.Id,
                x.Name,
                x.Description,
                x.IsActive,
                x.CreatedAt
            ))
            .ToListAsync(cancellationToken);

        _logger.LogInformation("User {User} retrieved {Count} {Module}",
            _currentUser.Email, items.Count, "{Module}");

        return Ok(new PaginatedResult<{Entity}ListDto>(items, totalCount, page, pageSize));
    }

    #endregion

    #region GET - Single by ID

    [HttpGet("{id:guid}")]
    [RequirePermission(Permissions.{PermissionClass}.View)]
    [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        return Ok(new {Entity}DetailDto(
            entity.Id,
            entity.Name,
            entity.Description,
            entity.IsActive,
            entity.CreatedAt,
            entity.UpdatedAt
        ));
    }

    #endregion

    #region POST - Create

    [HttpPost]
    [RequirePermission(Permissions.{PermissionClass}.Create)]
    [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<ActionResult<{Entity}DetailDto>> Create{Entity}(
        [FromBody] Create{Entity}Request request,
        CancellationToken cancellationToken)
    {
        // Check for duplicates
        var exists = await _context.{DbSet}
            .AnyAsync(x => x.Name == request.Name, cancellationToken);

        if (exists)
            return Conflict(new { message = "{Entity} with this name already exists" });

        var entity = {Entity}.Create(
            request.Name,
            request.Description
        );

        _context.{DbSet}.Add(entity);
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("User {User} created {Entity} {EntityId} ({Name})",
            _currentUser.Email, entity.Id, entity.Name);

        return CreatedAtAction(
            nameof(Get{Entity}),
            new { id = entity.Id },
            new {Entity}DetailDto(
                entity.Id,
                entity.Name,
                entity.Description,
                entity.IsActive,
                entity.CreatedAt,
                entity.UpdatedAt
            ));
    }

    #endregion

    #region PUT - Update

    [HttpPut("{id:guid}")]
    [RequirePermission(Permissions.{PermissionClass}.Update)]
    [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<ActionResult<{Entity}DetailDto>> Update{Entity}(
        Guid id,
        [FromBody] Update{Entity}Request request,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        // Check for duplicate name (excluding current)
        if (!string.IsNullOrEmpty(request.Name))
        {
            var duplicate = await _context.{DbSet}
                .AnyAsync(x => x.Name == request.Name && x.Id != id, cancellationToken);

            if (duplicate)
                return Conflict(new { message = "{Entity} with this name already exists" });
        }

        entity.Update(
            request.Name ?? entity.Name,
            request.Description ?? entity.Description
        );

        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("User {User} updated {Entity} {EntityId}",
            _currentUser.Email, entity.Id);

        return Ok(new {Entity}DetailDto(
            entity.Id,
            entity.Name,
            entity.Description,
            entity.IsActive,
            entity.CreatedAt,
            entity.UpdatedAt
        ));
    }

    #endregion

    #region PATCH - Activate/Deactivate

    [HttpPatch("{id:guid}/activate")]
    [RequirePermission(Permissions.{PermissionClass}.Update)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Activate{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        entity.Activate();
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("User {User} activated {Entity} {EntityId}",
            _currentUser.Email, entity.Id);

        return NoContent();
    }

    [HttpPatch("{id:guid}/deactivate")]
    [RequirePermission(Permissions.{PermissionClass}.Update)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Deactivate{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        entity.Deactivate();
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogWarning("User {User} deactivated {Entity} {EntityId}",
            _currentUser.Email, entity.Id);

        return NoContent();
    }

    #endregion

    #region DELETE

    [HttpDelete("{id:guid}")]
    [RequirePermission(Permissions.{PermissionClass}.Delete)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Delete{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        // Check for dependencies before deletion
        // var hasReferences = await _context.ChildEntities.AnyAsync(x => x.{Entity}Id == id, ct);
        // if (hasReferences)
        //     return BadRequest(new { message = "Cannot delete: has dependent records" });

        _context.{DbSet}.Remove(entity);
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogWarning("User {User} deleted {Entity} {EntityId} ({Name})",
            _currentUser.Email, id, entity.Name);

        return NoContent();
    }

    #endregion
}

#region DTOs

public record {Entity}ListDto(
    Guid Id,
    string Name,
    string? Description,
    bool IsActive,
    DateTime CreatedAt
);

public record {Entity}DetailDto(
    Guid Id,
    string Name,
    string? Description,
    bool IsActive,
    DateTime CreatedAt,
    DateTime? UpdatedAt
);

public record Create{Entity}Request(
    string Name,
    string? Description
);

public record Update{Entity}Request(
    string? Name,
    string? Description
);

public record PaginatedResult<T>(
    List<T> Items,
    int TotalCount,
    int Page,
    int PageSize
)
{
    public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
    public bool HasPreviousPage => Page > 1;
    public bool HasNextPage => Page < TotalPages;
}

#endregion
```

---

## Template Auth Controller (Login/Logout)

```csharp
// src/SmartStack.Api/Controllers/AuthController.cs
// NOTE: This controller already exists - use as reference for auth patterns

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using SmartStack.Application.Common.Interfaces;
using SmartStack.Application.Common.Settings;
using SmartStack.Domain.Platform.Administration.Users;

namespace SmartStack.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IApplicationDbContext _context;
    private readonly IPasswordService _passwordService;
    private readonly IJwtService _jwtService;
    private readonly IUserSessionService _sessionService;
    private readonly ISessionValidationService _sessionValidationService;
    private readonly SessionSettings _sessionSettings;
    private readonly ILogger<AuthController> _logger;

    // ... Constructor avec tous les services auth

    #region Login - LOGS CRITIQUES OBLIGATOIRES

    [HttpPost("login")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status401Unauthorized)]
    public async Task<ActionResult<LoginResponse>> Login(
        [FromBody] LoginRequest request,
        CancellationToken cancellationToken)
    {
        var ipAddress = GetClientIpAddress();
        var userAgent = Request.Headers.UserAgent.ToString();

        var user = await _context.Users
            .Include(u => u.UserRoles)
                .ThenInclude(ur => ur.Role)
                    .ThenInclude(r => r!.RolePermissions)
                        .ThenInclude(rp => rp.Permission)
            .FirstOrDefaultAsync(u => u.Email == request.Email, cancellationToken);

        // ============================================
        // LOGS CRITIQUES - NE JAMAIS OMETTRE
        // ============================================

        if (user == null)
        {
            // WARNING: User not found (potential enumeration)
            _logger.LogWarning(
                "Login failed: User not found - {Email} from {IpAddress}",
                request.Email, ipAddress);
            return Unauthorized(new ErrorResponse("Identifiants invalides", "INVALID_CREDENTIALS"));
        }

        if (!user.IsActive)
        {
            // WARNING: Disabled account
            _logger.LogWarning(
                "Login failed: Account disabled - {Email} from {IpAddress}",
                request.Email, ipAddress);
            return Unauthorized(new ErrorResponse("Account disabled", "ACCOUNT_DISABLED"));
        }

        if (user.IsLocked)
        {
            // CRITICAL: Locked account attempt
            _logger.LogCritical(
                "SECURITY: Login attempt on locked account - User: {Email} (ID: {UserId}) from {IpAddress}",
                request.Email, user.Id, ipAddress);
            return Unauthorized(new ErrorResponse("Account locked", "ACCOUNT_LOCKED_BY_ADMIN"));
        }

        // Check brute force protection
        var recentFailedAttempts = await _context.UserSessions
            .Where(s => s.UserId == user.Id && !s.IsSuccessful && s.LoginAt > DateTime.UtcNow.AddMinutes(-15))
            .CountAsync(cancellationToken);

        if (recentFailedAttempts >= 5)
        {
            // CRITICAL: Too many failed attempts
            _logger.LogCritical(
                "SECURITY: Account temporarily locked due to brute force - User: {Email} (ID: {UserId}) from {IpAddress}",
                request.Email, user.Id, ipAddress);
            return Unauthorized(new ErrorResponse("Account temporarily locked", "ACCOUNT_LOCKED"));
        }

        if (!_passwordService.VerifyPassword(request.Password, user.PasswordHash))
        {
            // WARNING: Invalid password with remaining attempts
            var remainingAttempts = 5 - recentFailedAttempts - 1;
            _logger.LogWarning(
                "Login failed: Invalid password - {Email} from {IpAddress}, remaining attempts: {Remaining}",
                request.Email, ipAddress, remainingAttempts);

            // Log failed attempt to session
            await _sessionService.LogFailedLoginAsync(user.Id, ipAddress, userAgent, cancellationToken);

            return Unauthorized(new ErrorResponse("Mot de passe incorrect", "INVALID_PASSWORD"));
        }

        // ============================================
        // SUCCESS - Generate tokens
        // ============================================

        var roles = user.UserRoles.Select(ur => ur.Role!.Name).ToList();
        var permissions = user.UserRoles
            .SelectMany(ur => ur.Role!.RolePermissions)
            .Select(rp => rp.Permission!.Path)
            .Distinct()
            .ToList();

        var accessToken = _jwtService.GenerateAccessToken(user, roles, permissions);
        var refreshToken = _jwtService.GenerateRefreshToken();

        await _sessionService.LogLoginAsync(user.Id, accessToken, ipAddress, userAgent, cancellationToken: cancellationToken);

        // INFO: Successful login
        _logger.LogInformation(
            "User logged in successfully: {Email} from {IpAddress}",
            user.Email, ipAddress);

        return Ok(new LoginResponse(accessToken, refreshToken, /* UserInfo */));
    }

    #endregion

    #region Logout

    [HttpPost("logout")]
    [Authorize]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> Logout(CancellationToken cancellationToken)
    {
        var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
        await _sessionService.LogLogoutAsync(token, cancellationToken);

        _logger.LogInformation("User logged out: {UserId}", User.FindFirst("sub")?.Value);

        return NoContent();
    }

    #endregion

    #region Change Password - LOG WARNING

    [HttpPost("change-password")]
    [Authorize]
    [ProducesResponseType(typeof(ChangePasswordResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<ChangePasswordResponse>> ChangePassword(
        [FromBody] ChangePasswordRequest request,
        CancellationToken cancellationToken)
    {
        // ... validation logic

        user.UpdatePassword(newPasswordHash);
        await _context.SaveChangesAsync(cancellationToken);

        // Invalidate ALL sessions after password change
        await _sessionService.InvalidateAllUserSessionsAsync(userId, "Password changed", cancellationToken);

        // WARNING: Sensitive operation
        _logger.LogWarning(
            "Password changed for user {Email} - All sessions invalidated",
            user.Email);

        return Ok(new ChangePasswordResponse("Password changed", true));
    }

    #endregion

    private string GetClientIpAddress()
    {
        var forwardedFor = Request.Headers["X-Forwarded-For"].FirstOrDefault();
        if (!string.IsNullOrEmpty(forwardedFor))
            return forwardedFor.Split(',')[0].Trim();
        return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
    }
}
```

---

## Template Permissions Constants

```csharp
// src/SmartStack.Application/Common/Authorization/Permissions.cs
// ADD to existing class

public static class Permissions
{
    // ... existing permissions ...

    public static class {PermissionClass}
    {
        public const string Access = "{permission.path}";
        public const string View = "{permission.path}.read";
        public const string Create = "{permission.path}.create";
        public const string Update = "{permission.path}.update";
        public const string Delete = "{permission.path}.delete";
        // Optional depending on module
        public const string Assign = "{permission.path}.assign";
        public const string Execute = "{permission.path}.execute";
        public const string Export = "{permission.path}.export";
    }
}
```

---

## Template PermissionConfiguration Seed

> **CRITICAL:** This template is MANDATORY. Without these entries, all API calls will return 403 Forbidden.

```csharp
// src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
// ADD in Configure() method, HasData section

// ============================================
// STEP 1: Declare ModuleId
// ============================================
// Check in ModuleConfiguration.cs if module already exists
// Otherwise, create the module first via /application skill

var {module}ModuleId = Guid.Parse("{MODULE-GUID}");  // Get from ModuleConfiguration.cs

// ============================================
// STEP 2: Add permissions (HasData)
// ============================================

// Pattern: {application}.{module}.{action}
// Example: administration.users.read

var seedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);

builder.HasData(
    // Wildcard permission (full module access)
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-1}"),
        Path = "{application}.{module}.*",
        Level = PermissionLevel.Module,
        Action = (PermissionAction?)null,
        IsWildcard = true,
        ModuleId = {module}ModuleId,
        Description = "Full {module} management",
        CreatedAt = seedDate
    },

    // Read permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-2}"),
        Path = "{application}.{module}.read",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Read,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "View {module}",
        CreatedAt = seedDate
    },

    // Create permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-3}"),
        Path = "{application}.{module}.create",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Create,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "Create {module}",
        CreatedAt = seedDate
    },

    // Update permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-4}"),
        Path = "{application}.{module}.update",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Update,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "Update {module}",
        CreatedAt = seedDate
    },

    // Delete permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-5}"),
        Path = "{application}.{module}.delete",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Delete,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "Delete {module}",
        CreatedAt = seedDate
    }

    // Optional actions depending on module:
    // - PermissionAction.Assign  → To assign resources/roles
    // - PermissionAction.Execute → To execute actions (export, etc.)
);
```

### GUID Generation

```bash
# PowerShell (Windows)
[guid]::NewGuid().ToString()

# Bash (Linux/Mac)
uuidgen | tr '[:upper:]' '[:lower:]'
```

### Validation Consistency Permissions.cs ↔ PermissionConfiguration.cs

> **RULE:** Each constant in `Permissions.cs` MUST have a corresponding entry in `PermissionConfiguration.cs`

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                         CONSISTENCY VALIDATION                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Permissions.cs                    PermissionConfiguration.cs               │
│  ──────────────────────────────    ──────────────────────────────────────   │
│  Permissions.Support.Tickets.View  →  Path = "support.tickets.read"│
│  Permissions.Support.Tickets.Create→  Path = "support.tickets.create"│
│  Permissions.Support.Tickets.Update→  Path = "support.tickets.update"│
│  Permissions.Support.Tickets.Delete→  Path = "support.tickets.delete"│
│                                                                             │
│  WARNING: COMMON ERROR:                                                    │
│  - Permissions.cs: "support.tickets.read"                          │
│  - PermissionConfiguration.cs: MISSING                                     │
│  → Result: 403 Forbidden for ALL users                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Post-generation commands

After adding entries in both files:

```bash
# 1. Create migration
/efcore migration Add{Module}Permissions

# 2. Apply migration
/efcore db-deploy

# 3. Verify (optional)
/efcore db-status
```

---

## Template Controller avec Relations

```csharp
// For controllers with related entities (ex: Tickets with Comments)

#region GET with Includes

[HttpGet("{id:guid}")]
[RequirePermission(Permissions.{PermissionClass}.View)]
[ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
    Guid id,
    CancellationToken cancellationToken)
{
    var entity = await _context.{DbSet}
        .Include(x => x.CreatedByUser)
        .Include(x => x.AssignedToUser)
        .Include(x => x.Comments)
            .ThenInclude(c => c.Author)
        .Include(x => x.Attachments)
        .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

    if (entity == null)
        return NotFound(new { message = "{Entity} not found" });

    return Ok(MapToDetailDto(entity));
}

#endregion

#region Nested Resources

[HttpGet("{parentId:guid}/children")]
[RequirePermission(Permissions.{PermissionClass}.View)]
[ProducesResponseType(typeof(List<ChildDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<ChildDto>>> GetChildren(
    Guid parentId,
    CancellationToken cancellationToken)
{
    var children = await _context.Children
        .Where(x => x.ParentId == parentId)
        .OrderByDescending(x => x.CreatedAt)
        .Select(x => new ChildDto(x.Id, x.Name, x.CreatedAt))
        .ToListAsync(cancellationToken);

    return Ok(children);
}

[HttpPost("{parentId:guid}/children")]
[RequirePermission(Permissions.{PermissionClass}.Create)]
[ProducesResponseType(typeof(ChildDto), StatusCodes.Status201Created)]
public async Task<ActionResult<ChildDto>> AddChild(
    Guid parentId,
    [FromBody] CreateChildRequest request,
    CancellationToken cancellationToken)
{
    var parent = await _context.{DbSet}
        .FirstOrDefaultAsync(x => x.Id == parentId, cancellationToken);

    if (parent == null)
        return NotFound(new { message = "Parent not found" });

    var child = Child.Create(parentId, request.Name, _currentUser.UserId!.Value);

    _context.Children.Add(child);
    await _context.SaveChangesAsync(cancellationToken);

    _logger.LogInformation("User {User} added child to {Entity} {ParentId}",
        _currentUser.Email, parentId);

    return CreatedAtAction(
        nameof(GetChildren),
        new { parentId },
        new ChildDto(child.Id, child.Name, child.CreatedAt));
}

#endregion
```

---

## Reusable Patterns

### Error Response Standard

```csharp
public record ErrorResponse(string Message, string? Code = null);

// Usage:
return BadRequest(new ErrorResponse("Validation failed", "VALIDATION_ERROR"));
return Conflict(new ErrorResponse("Already exists", "DUPLICATE"));
return NotFound(new { message = "Resource not found" });
```

### Pagination Query Extension

```csharp
public static class QueryableExtensions
{
    public static async Task<PaginatedResult<T>> ToPaginatedResultAsync<T>(
        this IQueryable<T> query,
        int page,
        int pageSize,
        CancellationToken ct = default)
    {
        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);
    }
}
```

### Log Context Pattern

```csharp
// Always include user context in logs
_logger.LogInformation(
    "User {User} ({UserId}) performed {Action} on {Entity} {EntityId}",
    _currentUser.Email,
    _currentUser.UserId,
    "Create",
    "{Entity}",
    entity.Id);
```

---

## Template Section-Level Permissions (Level 3)

> **Usage:** When a Module has multiple sub-pages/tabs with different permissions (ex: AI → Dashboard, Settings, Prompts)

### Permissions.cs - Section

```csharp
// src/SmartStack.Application/Common/Authorization/Permissions.cs

public static class Admin
{
    public static class {Module}
    {
        // Section permissions (Level 3)
        public static class {Section}
        {
            public const string View = "{application}.{module}.{section}.read";
            public const string Create = "{application}.{module}.{section}.create";
            public const string Update = "{application}.{module}.{section}.update";
            public const string Delete = "{application}.{module}.{section}.delete";
            public const string Execute = "{application}.{module}.{section}.execute";
        }
    }
}
```

### PermissionConfiguration.cs - Section Seed

```csharp
// src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
// ADD in Configure() method, HasData section

// ============================================
// STEP 1: Declare SectionId
// ============================================
// Get from NavigationSectionConfiguration.cs

var {section}SectionId = Guid.Parse("{SECTION-GUID}");

// ============================================
// STEP 2: Add Section permissions (Level 3)
// ============================================

// Pattern: {application}.{module}.{section}.{action}
// Example: administration.ai.settings.read

builder.HasData(
    // Wildcard permission (full section access)
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-1}"),
        Path = "{application}.{module}.{section}.*",
        Level = PermissionLevel.Section,
        Action = (PermissionAction?)null,
        IsWildcard = true,
        SectionId = {section}SectionId,
        Description = "Full {section} access",
        CreatedAt = seedDate
    },

    // Read permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-2}"),
        Path = "{application}.{module}.{section}.read",
        Level = PermissionLevel.Section,
        Action = PermissionAction.Read,
        IsWildcard = false,
        SectionId = {section}SectionId,
        Description = "View {section}",
        CreatedAt = seedDate
    },

    // Create permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-3}"),
        Path = "{application}.{module}.{section}.create",
        Level = PermissionLevel.Section,
        Action = PermissionAction.Create,
        IsWildcard = false,
        SectionId = {section}SectionId,
        Description = "Create in {section}",
        CreatedAt = seedDate
    },

    // Update permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-4}"),
        Path = "{application}.{module}.{section}.update",
        Level = PermissionLevel.Section,
        Action = PermissionAction.Update,
        IsWildcard = false,
        SectionId = {section}SectionId,
        Description = "Update in {section}",
        CreatedAt = seedDate
    },

    // Delete permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-5}"),
        Path = "{application}.{module}.{section}.delete",
        Level = PermissionLevel.Section,
        Action = PermissionAction.Delete,
        IsWildcard = false,
        SectionId = {section}SectionId,
        Description = "Delete in {section}",
        CreatedAt = seedDate
    },

    // Execute permission (optional)
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-6}"),
        Path = "{application}.{module}.{section}.execute",
        Level = PermissionLevel.Section,
        Action = PermissionAction.Execute,
        IsWildcard = false,
        SectionId = {section}SectionId,
        Description = "Execute actions in {section}",
        CreatedAt = seedDate
    }
);
```

---

## Template Resource-Level Permissions (Level 4)

> **Usage:** For the finest granularity level (ex: Prompts → Blocks, Users → Profiles)
> **CRITICAL:** Used when a Section contains sub-resources with distinct permissions

### Permissions.cs - Resource

```csharp
// src/SmartStack.Application/Common/Authorization/Permissions.cs

public static class Admin
{
    public static class {Module}
    {
        public static class {Section}
        {
            // Section-level permissions...

            // Resource permissions (Level 4 - finest granularity)
            public static class {Resource}
            {
                public const string View = "{application}.{module}.{section}.{resource}.read";
                public const string Create = "{application}.{module}.{section}.{resource}.create";
                public const string Update = "{application}.{module}.{section}.{resource}.update";
                public const string Delete = "{application}.{module}.{section}.{resource}.delete";
            }
        }
    }
}
```

### PermissionConfiguration.cs - Resource Seed

```csharp
// src/SmartStack.Infrastructure/Persistence/Configurations/Navigation/PermissionConfiguration.cs
// ADD in Configure() method, HasData section

// ============================================
// STEP 1: Declare ResourceId
// ============================================
// Get from NavigationResourceConfiguration.cs

var {resource}ResourceId = Guid.Parse("{RESOURCE-GUID}");

// ============================================
// STEP 2: Add Resource permissions (Level 4)
// ============================================

// Pattern: {application}.{module}.{section}.{resource}.{action}
// Example: administration.ai.prompts.blocks.read

builder.HasData(
    // Wildcard permission (full resource access)
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-1}"),
        Path = "{application}.{module}.{section}.{resource}.*",
        Level = PermissionLevel.Resource,
        Action = (PermissionAction?)null,
        IsWildcard = true,
        ResourceId = {resource}ResourceId,
        Description = "Full {resource} access",
        CreatedAt = seedDate
    },

    // Read permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-2}"),
        Path = "{application}.{module}.{section}.{resource}.read",
        Level = PermissionLevel.Resource,
        Action = PermissionAction.Read,
        IsWildcard = false,
        ResourceId = {resource}ResourceId,
        Description = "View {resource}",
        CreatedAt = seedDate
    },

    // Create permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-3}"),
        Path = "{application}.{module}.{section}.{resource}.create",
        Level = PermissionLevel.Resource,
        Action = PermissionAction.Create,
        IsWildcard = false,
        ResourceId = {resource}ResourceId,
        Description = "Create {resource}",
        CreatedAt = seedDate
    },

    // Update permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-4}"),
        Path = "{application}.{module}.{section}.{resource}.update",
        Level = PermissionLevel.Resource,
        Action = PermissionAction.Update,
        IsWildcard = false,
        ResourceId = {resource}ResourceId,
        Description = "Update {resource}",
        CreatedAt = seedDate
    },

    // Delete permission
    new
    {
        Id = Guid.Parse("{NOUVEAU-GUID-5}"),
        Path = "{application}.{module}.{section}.{resource}.delete",
        Level = PermissionLevel.Resource,
        Action = PermissionAction.Delete,
        IsWildcard = false,
        ResourceId = {resource}ResourceId,
        Description = "Delete {resource}",
        CreatedAt = seedDate
    }
);
```

---

## Template Bulk Operations (Batch Insertion)

> **MANDATORY:** Always provide bulk endpoints when creating a CRUD controller

### Permissions.cs - Bulk Operations

```csharp
// src/SmartStack.Application/Common/Authorization/Permissions.cs

public static class {Module}
{
    // CRUD standard
    public const string View = "{path}.read";
    public const string Create = "{path}.create";
    public const string Update = "{path}.update";
    public const string Delete = "{path}.delete";

    // Bulk operations (MANDATORY for all CRUD modules)
    public const string BulkCreate = "{path}.bulk-create";
    public const string BulkUpdate = "{path}.bulk-update";
    public const string BulkDelete = "{path}.bulk-delete";
    public const string Export = "{path}.export";
    public const string Import = "{path}.import";
}
```

### PermissionConfiguration.cs - Bulk Permissions Seed

```csharp
// Add after standard CRUD permissions

// Bulk Create permission
new
{
    Id = Guid.Parse("{NOUVEAU-GUID-BULK-1}"),
    Path = "{application}.{module}.bulk-create",
    Level = PermissionLevel.Module,
    Action = PermissionAction.Create,
    IsWildcard = false,
    ModuleId = {module}ModuleId,
    Description = "Bulk create {module}",
    CreatedAt = seedDate
},

// Bulk Update permission
new
{
    Id = Guid.Parse("{NOUVEAU-GUID-BULK-2}"),
    Path = "{application}.{module}.bulk-update",
    Level = PermissionLevel.Module,
    Action = PermissionAction.Update,
    IsWildcard = false,
    ModuleId = {module}ModuleId,
    Description = "Bulk update {module}",
    CreatedAt = seedDate
},

// Bulk Delete permission
new
{
    Id = Guid.Parse("{NOUVEAU-GUID-BULK-3}"),
    Path = "{application}.{module}.bulk-delete",
    Level = PermissionLevel.Module,
    Action = PermissionAction.Delete,
    IsWildcard = false,
    ModuleId = {module}ModuleId,
    Description = "Bulk delete {module}",
    CreatedAt = seedDate
},

// Export permission
new
{
    Id = Guid.Parse("{NOUVEAU-GUID-EXPORT}"),
    Path = "{application}.{module}.export",
    Level = PermissionLevel.Module,
    Action = PermissionAction.Execute,
    IsWildcard = false,
    ModuleId = {module}ModuleId,
    Description = "Export {module} data",
    CreatedAt = seedDate
},

// Import permission
new
{
    Id = Guid.Parse("{NOUVEAU-GUID-IMPORT}"),
    Path = "{application}.{module}.import",
    Level = PermissionLevel.Module,
    Action = PermissionAction.Create,
    IsWildcard = false,
    ModuleId = {module}ModuleId,
    Description = "Import {module} data",
    CreatedAt = seedDate
}
```

### Controller Endpoints - Bulk Operations

```csharp
// src/SmartStack.Api/Controllers/{Area}/{Module}Controller.cs
// ADD after standard CRUD endpoints

#region BULK OPERATIONS

/// <summary>
/// Bulk create multiple entities
/// </summary>
[HttpPost("bulk")]
[RequirePermission(Permissions.{PermissionClass}.BulkCreate)]
[ProducesResponseType(typeof(BulkOperationResult<{Entity}Dto>), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BulkOperationResult<{Entity}Dto>>> BulkCreate{Entity}(
    [FromBody] List<Create{Entity}Request> requests,
    CancellationToken cancellationToken)
{
    if (requests == null || requests.Count == 0)
        return BadRequest(new { message = "No items provided" });

    if (requests.Count > 100)
        return BadRequest(new { message = "Maximum 100 items per bulk operation" });

    var results = new List<{Entity}Dto>();
    var errors = new List<BulkOperationError>();

    for (int i = 0; i < requests.Count; i++)
    {
        try
        {
            var entity = {Entity}.Create(
                requests[i].Name,
                requests[i].Description
            );

            _context.{DbSet}.Add(entity);
            results.Add(new {Entity}Dto(entity.Id, entity.Name));
        }
        catch (Exception ex)
        {
            errors.Add(new BulkOperationError(i, requests[i].Name, ex.Message));
        }
    }

    await _context.SaveChangesAsync(cancellationToken);

    _logger.LogInformation("User {User} bulk created {Count} {Entity}(s), {Errors} error(s)",
        _currentUser.Email, results.Count, errors.Count);

    return CreatedAtAction(
        nameof(Get{Module}),
        new BulkOperationResult<{Entity}Dto>(results, errors, results.Count, errors.Count));
}

/// <summary>
/// Bulk update multiple entities
/// </summary>
[HttpPut("bulk")]
[RequirePermission(Permissions.{PermissionClass}.BulkUpdate)]
[ProducesResponseType(typeof(BulkOperationResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BulkOperationResult>> BulkUpdate{Entity}(
    [FromBody] List<BulkUpdate{Entity}Request> requests,
    CancellationToken cancellationToken)
{
    if (requests == null || requests.Count == 0)
        return BadRequest(new { message = "No items provided" });

    if (requests.Count > 100)
        return BadRequest(new { message = "Maximum 100 items per bulk operation" });

    var ids = requests.Select(r => r.Id).ToList();
    var entities = await _context.{DbSet}
        .Where(x => ids.Contains(x.Id))
        .ToDictionaryAsync(x => x.Id, cancellationToken);

    var updated = 0;
    var errors = new List<BulkOperationError>();

    for (int i = 0; i < requests.Count; i++)
    {
        if (!entities.TryGetValue(requests[i].Id, out var entity))
        {
            errors.Add(new BulkOperationError(i, requests[i].Id.ToString(), "Entity not found"));
            continue;
        }

        try
        {
            entity.Update(
                requests[i].Name ?? entity.Name,
                requests[i].Description ?? entity.Description
            );
            updated++;
        }
        catch (Exception ex)
        {
            errors.Add(new BulkOperationError(i, requests[i].Id.ToString(), ex.Message));
        }
    }

    await _context.SaveChangesAsync(cancellationToken);

    _logger.LogInformation("User {User} bulk updated {Count} {Entity}(s), {Errors} error(s)",
        _currentUser.Email, updated, errors.Count);

    return Ok(new BulkOperationResult(updated, errors.Count, errors));
}

/// <summary>
/// Bulk delete multiple entities by IDs
/// </summary>
[HttpDelete("bulk")]
[RequirePermission(Permissions.{PermissionClass}.BulkDelete)]
[ProducesResponseType(typeof(BulkOperationResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BulkOperationResult>> BulkDelete{Entity}(
    [FromBody] List<Guid> ids,
    CancellationToken cancellationToken)
{
    if (ids == null || ids.Count == 0)
        return BadRequest(new { message = "No IDs provided" });

    if (ids.Count > 100)
        return BadRequest(new { message = "Maximum 100 items per bulk operation" });

    var entities = await _context.{DbSet}
        .Where(x => ids.Contains(x.Id))
        .ToListAsync(cancellationToken);

    var deleted = entities.Count;
    var notFound = ids.Count - deleted;

    _context.{DbSet}.RemoveRange(entities);
    await _context.SaveChangesAsync(cancellationToken);

    _logger.LogWarning("User {User} bulk deleted {Count} {Entity}(s), {NotFound} not found",
        _currentUser.Email, deleted, notFound);

    var errors = notFound > 0
        ? new List<BulkOperationError> { new(-1, "N/A", $"{notFound} entities not found") }
        : new List<BulkOperationError>();

    return Ok(new BulkOperationResult(deleted, errors.Count, errors));
}

/// <summary>
/// Export entities to CSV/Excel
/// </summary>
[HttpGet("export")]
[RequirePermission(Permissions.{PermissionClass}.Export)]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public async Task<IActionResult> Export{Module}(
    [FromQuery] string format = "csv",
    [FromQuery] string? search = null,
    CancellationToken cancellationToken = default)
{
    var query = _context.{DbSet}.AsQueryable();

    if (!string.IsNullOrWhiteSpace(search))
    {
        var searchLower = search.ToLower();
        query = query.Where(x => x.Name.ToLower().Contains(searchLower));
    }

    var entities = await query.ToListAsync(cancellationToken);

    _logger.LogInformation("User {User} exported {Count} {Entity}(s) to {Format}",
        _currentUser.Email, entities.Count, format);

    // Implement CSV/Excel export logic here
    // Using libraries like CsvHelper or ClosedXML

    var content = format.ToLower() switch
    {
        "xlsx" => GenerateExcel(entities),
        _ => GenerateCsv(entities)
    };

    var contentType = format.ToLower() == "xlsx"
        ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        : "text/csv";

    var fileName = $"{module}-export-{DateTime.UtcNow:yyyyMMdd-HHmmss}.{format}";

    return File(content, contentType, fileName);
}

#endregion

#region Bulk DTOs

public record BulkOperationResult(
    int SuccessCount,
    int ErrorCount,
    List<BulkOperationError> Errors);

public record BulkOperationResult<T>(
    List<T> Created,
    List<BulkOperationError> Errors,
    int SuccessCount,
    int ErrorCount);

public record BulkOperationError(
    int Index,
    string Identifier,
    string Message);

public record BulkUpdate{Entity}Request(
    Guid Id,
    string? Name,
    string? Description);

#endregion
```

---

## Complete Permissions Hierarchy

```
┌─────────────────────────────────────────────────────────────────────────────────┐
│                      COMPLETE PERMISSIONS HIERARCHY                             │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│  Level 1: APPLICATION                                                           │
│  └─ Path: {application}.*                                                       │
│     └─ Ex: administration.* → Full administration access                        │
│                                                                                 │
│  Level 2: MODULE                                                                │
│  └─ Path: {application}.{module}.{action}                                       │
│     └─ Ex: administration.users.read → Read users                               │
│     └─ BULK: administration.users.bulk-create → Batch create                   │
│                                                                                 │
│  Level 3: SECTION                                                               │
│  └─ Path: {application}.{module}.{section}.{action}                             │
│     └─ Ex: administration.ai.settings.update → Update AI settings              │
│                                                                                 │
│  Level 4: RESOURCE (finest granularity)                                         │
│  └─ Path: {application}.{module}.{section}.{resource}.{action}                  │
│     └─ Ex: administration.ai.prompts.blocks.delete → Delete blocks             │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘
```

---

## Controller Checklist with Complete Permissions

```
□ CRUD Standard
  □ GET /api/.../           → {path}.read
  □ GET /api/.../{id}       → {path}.read
  □ POST /api/.../          → {path}.create
  □ PUT /api/.../{id}       → {path}.update
  □ DELETE /api/.../{id}    → {path}.delete

□ Bulk Operations
  □ POST /api/.../bulk      → {path}.bulk-create
  □ PUT /api/.../bulk       → {path}.bulk-update
  □ DELETE /api/.../bulk    → {path}.bulk-delete

□ Export/Import
  □ GET /api/.../export     → {path}.export
  □ POST /api/.../import    → {path}.import

□ Permissions Configured
  □ Permissions.cs - Constants defined
  □ PermissionConfiguration.cs - Seed HasData
  □ EF Core Migration created
  □ Migration applied

□ API Versioning (if applicable)
  □ See API Versioning section below

□ Correct Permission Level
  □ Module (Level 2) - For main CRUD
  □ Section (Level 3) - If sub-pages with different permissions
  □ Resource (Level 4) - If sub-resources with distinct permissions
```

---

## API Versioning

**SmartStack convention:** Header-based versioning using `Asp.Versioning.Mvc`.

### Setup

```csharp
// Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true; // Adds api-supported-versions header
    options.ApiVersionReader = new HeaderApiVersionReader("api-version");
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});
```

### Controller Usage

```csharp
// v1 - Default (no attribute needed if only one version exists)
[ApiController]
[ApiVersion("1.0")]
[NavRoute("crm.contacts")]
public class ContactsController : ControllerBase
{
    [HttpGet]
    [RequirePermission(Permissions.Crm.Contacts.Read)]
    public async Task<ActionResult<PaginatedResult<ContactDto>>> GetAll(...) { ... }
}

// v2 - Breaking changes only
[ApiController]
[ApiVersion("2.0")]
[NavRoute("crm.contacts")]
public class ContactsV2Controller : ControllerBase
{
    [HttpGet]
    [RequirePermission(Permissions.Crm.Contacts.Read)]
    public async Task<ActionResult<PaginatedResult<ContactV2Dto>>> GetAll(...) { ... }
}
```

### Deprecation

```csharp
[ApiVersion("1.0", Deprecated = true)] // Adds api-deprecated-versions header
[ApiVersion("2.0")]
public class ContactsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public async Task<ActionResult<PaginatedResult<ContactDto>>> GetAllV1(...) { ... }

    [HttpGet]
    [MapToApiVersion("2.0")]
    public async Task<ActionResult<PaginatedResult<ContactV2Dto>>> GetAllV2(...) { ... }
}
```

### SmartStack Versioning Rules

| Rule | Detail |
|------|--------|
| Default version | `1.0` (assumed when no header sent) |
| Version header | `api-version: 2.0` |
| When to version | Breaking changes only (field removal, type change, behavior change) |
| Non-breaking changes | Add to existing version (new fields, new endpoints) |
| Naming | `V2Controller` suffix or `[MapToApiVersion]` in same controller |
| Deprecation | Mark old version deprecated, maintain for 2 releases minimum |
| Documentation | Both versions documented via `[ProducesResponseType]` |
