# CLAUDE.md — GoodBooks GB5 Backend (.NET 9 Microservices)



Act as a senior .NET developer. Follow clean architecture and avoid over-engineering.
Identify edge cases and include proper validation.
Refactor this without changing behavior.
Optimize this for performance and explain trade-offs.


## Project Overview

Enterprise-grade .NET 9 microservices ERP backend.
**Architecture:** 3-tier per module (SL → BLL → DAL) with shared framework libraries.
**No Entity Framework.** All data access uses **Dapper** with raw, parameterized SQL.
**No traditional MVC controllers.** All HTTP endpoints use **FastEndpoints**.

---

## Architecture Rules

### Layer Responsibilities

| Layer | Folder suffix | Responsibility |
|-------|--------------|----------------|
| Service Layer | `*SL` | HTTP endpoint definitions (FastEndpoints), request/response mapping, cache-key generation |
| Business Logic Layer | `*BLL` | Validation, orchestration, business rules, calling DAL |
| Data Access Layer | `*DAL` | Dapper queries, stored-procedure calls, transaction management |

**Strict rules:**
- SL must never call DAL directly — always go through BLL.
- BLL must never reference ASP.NET types (`HttpContext`, `IEndpointRouteBuilder`, etc.).
- DAL must never contain business logic; only data retrieval/mutation.
- DTOs cross all layers; domain objects stay inside BLL.

### Module Structure Template

```
ModuleSL/
  Endpoints/
    GetFoo.cs          ← FastEndpoint
    SaveFoo.cs
  Parameters/
    GetFooParameters.cs
  Hubs/                ← only present for real-time modules
    FooHub.cs          ← SignalR Hub (inherits Hub<IFooClient>)
    IFooClient.cs      ← strongly-typed client contract interface
ModuleBLL/
  Interfaces/
    IFooBLL.cs
  Implementations/
    FooBLL.cs
ModuleDAL/
  Interfaces/
    IFooDAL.cs
  Implementations/
    FooDAL.cs
  QueryBuilders/
    FooQB.cs           ← SQL constants only
  DTOs/
    FooDTO.cs
```

---

## Coding Standards

### Naming Conventions

```csharp
// Interfaces — prefix with I
public interface IAccountBLL { }
public interface IAccountDAL { }

// Implementations — no prefix
public class AccountBLL : IAccountBLL { }
public class AccountDAL : IAccountDAL { }

// Endpoints — verb + entity
public class GetAccount : BaseEndpoint<GetAccountParameters, ResponseStandardDTO<object>> { }
public class SaveAccount : BaseEndpoint<SaveAccountParameters, ResponseStandardDTO<object>> { }

// Query Builders — entity + QB
public static class AccountQB
{
    public const string GET_ACCOUNT = "SELECT ...";
    public const string SAVE_ACCOUNT = "INSERT INTO ...";
}

// Parameters (request DTOs) — PascalCase, suffix Parameters
public class GetAccountParameters { }

// DTOs — suffix DTO
public class AccountDTO { }
```

### Async/Await — Always Async All the Way

```csharp
// CORRECT — async throughout
public async Task<string> GetAccount(int id, LoginDTO login)
    => await _dal.GetAccount(id, login);

// WRONG — blocking on async
public string GetAccount(int id, LoginDTO login)
    => _dal.GetAccount(id, login).Result;  // deadlock risk
```

### Cancellation Token Propagation

Always accept and forward `CancellationToken` in every async method chain.

```csharp
// SL (endpoint)
protected override async Task<ResponseStandardDTO<object>> ExecuteAsync(
    GetAccountParameters req, LoginDTO login, CancellationToken ct)
{
    return await _bll.GetAccount(req.AccountId, login, ct);
}

// BLL
public async Task<string> GetAccount(int id, LoginDTO login, CancellationToken ct)
    => await _dal.GetAccount(id, login, ct);

// DAL
public async Task<string> GetAccount(int id, LoginDTO login, CancellationToken ct)
{
    string sql = AccountQB.GET_ACCOUNT;
    return await _queryExecutor.QuerySingleAsync<string>(login, sql,
        new { AccountId = id }, cancellationToken: ct);
}
```

---

## Layer Return Type Contract — Non-Negotiable

Every method in every layer must follow this exact contract.
Violating it is the **single most common source of double-serialization bugs** and bypassed audit pipelines.

### DAL Return Types

| Operation | Return type | Example |
|-----------|-------------|---------|
| Read single | `Task<FooDTO?>` | `Task<MtrReportDTO?>` |
| Read list (unbounded) | `Task<IEnumerable<FooDTO>>` | `Task<IEnumerable<MtrListDTO>>` |
| Read paged | `Task<PagedResult<FooDTO>>` | `Task<PagedResult<MtrListDTO>>` |
| Save / Update | `Task<Result<string>>` | `Result<string>.Success("saved")` |
| Delete | `Task<Result<string>>` | `Result<string>.Success("deleted")` |

**Rules — enforced without exception:**
- DAL read methods **never** return `Task<string>`. Never call `JsonConvert.SerializeObject()` in DAL.
- DAL write methods always accept `DbTransaction tx` as the last parameter before `CancellationToken ct`.
- DAL **never** calls `BeginTransactionAsync` / `CommitAsync` / `RollbackAsync` — the transaction is owned by BLL.
- Audit fields (`CreatedById`, `CreatedOn`, `ModifiedById`, `ModifiedOn`) are set inside DAL, not BLL.
- All `await` calls in DAL use `.ConfigureAwait(false)`.

### BLL Return Types

| Operation | Return type | Notes |
|-----------|-------------|-------|
| Read single | `Task<FooDTO?>` | Typed pass-through from DAL |
| Read paged | `Task<PagedResult<FooDTO>>` | Typed pass-through from DAL |
| Save / Delete | `Task<string>` | Localised success message via `SuccessResponse.*` |

BLL read methods are **typed pass-throughs** — never serialize to JSON.
All `await` calls in BLL use `.ConfigureAwait(false)`.

### SL (Endpoint) Rules

- All endpoints return `ResponseStandardDTO<object>` — call `Response.CreateSuccessResponse(typedDto, level, login)`.
- `CreateSuccessResponse` accepts any typed object; do **not** pre-serialize to JSON before passing it.
- `ExecuteAsync` must **NOT** contain try/catch — `BaseEndpoint.HandleAsync` owns error logging and response formatting. Adding try/catch here hides errors from the OTel span and causes double-handling.

```csharp
// WRONG — redundant, hides OTel traces
protected override async Task<ResponseStandardDTO<object>> ExecuteAsync(...)
{
    try { var r = await _bll.Get(...); return await Response.CreateSuccessResponse(r, ...); }
    catch (Exception ex) { return await Response.CreateExceptionError<string>(ex, ...); }
}

// CORRECT
protected override async Task<ResponseStandardDTO<object>> ExecuteAsync(...)
{
    var result = await _bll.Get(req.FooId, loginDTO, ct);
    return await Response.CreateSuccessResponse(result, CacheKeyLevel.CLIENT_LEVEL, loginDTO);
}
```

### Canonical Reference Files

| Layer | Canonical file |
|-------|---------------|
| DAL | `GB5Solution/MM/MMDAL/CustomCode/SupplyGroup/SupplyGroupDAL.cs` |
| BLL | `GB5Solution/MM/MMBLL/SupplyGroup/SupplyGroupBLL.cs` |
| SL | `GB5Framework/FrameworkSL/Endpoints/GOP/TargetOperation/GetGopTargetOperation.cs` |

---

## Data Access — Dapper Rules (No EF)

### Parameterized Queries — Non-Negotiable

```csharp
// CORRECT
string sql = "SELECT * FROM Account WHERE AccountId = @AccountId AND ClientId = @ClientId";
var result = await _queryExecutor.QuerySingleAsync<AccountDTO>(
    loginDTO, sql, new { AccountId = id, ClientId = loginDTO.ClientId });

// WRONG — SQL injection vulnerability
string sql = $"SELECT * FROM Account WHERE AccountId = {id}";
```

### Query Builder Pattern

All SQL lives in `*QB.cs` files as `public const string`. Never embed SQL inline in BLL/DAL methods.

```csharp
public static class AccountQB
{
    public const string GET_ACCOUNT = @"
        SELECT a.AccountId, a.AccountCode, a.AccountName, a.IsActive
        FROM   Account a
        WHERE  a.AccountId   = @AccountId
        AND    a.DatabaseName = @DatabaseName";

    public const string SAVE_ACCOUNT = @"
        INSERT INTO Account (AccountCode, AccountName, ClientId, CreatedBy, CreatedDate)
        VALUES (@AccountCode, @AccountName, @ClientId, @CreatedBy, GETUTCDATE())";
}
```

### IQueryExecutor Usage Reference

```csharp
// Single record
var dto = await _qe.QuerySingleAsync<AccountDTO>(login, sql, param, ct);

// List
var list = await _qe.QueryAsync<AccountDTO>(login, sql, param, ct);

// Execute (INSERT/UPDATE/DELETE) — returns rows affected
int rows = await _qe.ExecuteAsync(login, sql, param, ct);

// Insert returning new identity
int newId = await _qe.ExecuteInsertReturnIdentityAsync(login, sql, param, ct);

// Scalar (COUNT, SUM, etc.)
int count = await _qe.ExecuteScalarAsync<int>(login, sql, param, ct);

// Paged result
var paged = await _qe.QueryPagedAsync<AccountDTO>(login, sql, countSql, param, page, size, ct);

// Streaming large datasets — never load >10k rows into memory at once
await foreach (var row in _qe.StreamAsync<AccountDTO>(login, sql, param, ct))
{
    // process each row
}

// Multi-table join
var result = await _qe.QueryMultiMapAsync<AccountDTO, BranchDTO, AccountDTO>(
    login, sql, (a, b) => { a.Branch = b; return a; }, "BranchId", param, ct);
```



### Transaction Management

```csharp
// Always use try/catch/finally; never leave transactions open
await using var tx = await _queryExecutor.BeginTransactionAsync(login);
try
{
    await _queryExecutor.ExecuteAsync(login, AccountQB.SAVE_ACCOUNT, dto, tx, ct);
    await _queryExecutor.ExecuteAsync(login, JournalQB.SAVE_JOURNAL, journalDto, tx, ct);
    await tx.CommitAsync(ct);
}
catch
{
    await tx.RollbackAsync(ct);
    throw;
}
```

### Large Dataset Rules

- Use `StreamAsync<T>()` for any query potentially returning >5,000 rows.
- Use `BulkInsertAsync<T>()` for batch inserts of >100 rows — never loop `ExecuteAsync`.
- Paginate all list endpoints via `QueryPagedAsync<T>()` — no unbounded SELECT *.

---

## FastEndpoints — SL Layer

### Endpoint Template

```csharp
public class GetAccount : BaseEndpoint<GetAccountParameters, ResponseStandardDTO<object>>
{
    private readonly IAccountBLL _bll;

    public GetAccount(IAccountBLL bll) => _bll = bll;

    public override void Configure()
    {
        Get("/Account/GetAccount");
        AllowAnonymous();   // replace with Roles("Admin") for secured endpoints
    }

    protected override string? GetCacheKey(GetAccountParameters req, LoginDTO login)
        => $"Account:{login.ClientId}:{req.AccountId}";  // return null to skip cache

    protected override async Task<ResponseStandardDTO<object>> ExecuteAsync(
        GetAccountParameters req, LoginDTO login, CancellationToken ct)
        => await _bll.GetAccount(req.AccountId, login, ct);
}
```

### Route Convention

```
GET    /Entity/GetEntity           ← single record or list
POST   /Entity/SaveEntity          ← create
PUT    /Entity/UpdateEntity        ← update
DELETE /Entity/DeleteEntity        ← delete
GET    /Entity/GetSelectListEntity ← dropdown data
```

### Response Convention

Always return `ResponseStandardDTO<object>` from endpoints. BLL returns serialized JSON string; SL wraps it.

---

## Real-Time / SignalR Modules

Use SignalR only for modules that genuinely require server-push or bidirectional communication
(e.g., CollabSpace, live notifications, presence tracking).
REST endpoints (FastEndpoints) remain the standard for all request/response interactions.

### Folder Structure

Hub files live exclusively in `ModuleSL/Hubs/`. Two files per Hub:

```
ModuleSL/Hubs/
  CollabSpaceHub.cs      ← Hub implementation
  ICollabSpaceClient.cs  ← strongly-typed client contract
```

### Strongly-Typed Client Contract

Always use `Hub<TClient>` — never the untyped `Hub`. This gives compile-time verification
of every client-side method name and signature.

```csharp
// ICollabSpaceClient.cs
public interface ICollabSpaceClient
{
    Task ReceiveMessage(CollabMessageDTO message);
    Task ReceivePresenceUpdate(PresenceDTO presence);
    Task ReceiveTypingIndicator(string senderName, bool isTyping);
    Task ReceiveError(string errorMessage);
}
```

### Hub Template

```csharp
// CollabSpaceHub.cs
public class CollabSpaceHub : Hub<ICollabSpaceClient>
{
    private readonly ICollabSpaceBLL _bll;
    private readonly ILogger<CollabSpaceHub> _logger;

    public CollabSpaceHub(ICollabSpaceBLL bll, ILogger<CollabSpaceHub> logger)
    {
        _bll   = bll;
        _logger = logger;
    }

    // ── Connection lifecycle ──────────────────────────────────────────

    public override async Task OnConnectedAsync()
    {
        var login = GetLoginDTO();
        await Groups.AddToGroupAsync(Context.ConnectionId, HubGroup(login));
        _logger.LogInformation("SignalR connected: {ConnectionId} user {UserId}",
            Context.ConnectionId, login.UserId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var login = GetLoginDTO();
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, HubGroup(login));
        if (exception is not null)
            _logger.LogWarning(exception, "SignalR disconnected with error: {ConnectionId}",
                Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }

    // ── Hub methods (all async Task, never async void) ───────────────

    public async Task SendMessage(CollabMessageParameters param, CancellationToken ct)
    {
        var login = GetLoginDTO();
        try
        {
            var result = await _bll.SendMessage(param, login, ct);
            // Broadcast to the group — not back to caller only
            await Clients.Group(HubGroup(login)).ReceiveMessage(result);
        }
        catch (ValidationException vex)
        {
            await Clients.Caller.ReceiveError(vex.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "SendMessage failed for user {UserId}", login.UserId);
            await Clients.Caller.ReceiveError("An error occurred. Please retry.");
        }
    }

    public async Task SetTypingIndicator(bool isTyping, CancellationToken ct)
    {
        var login = GetLoginDTO();
        // Broadcast to group excluding the sender
        await Clients.GroupExcept(HubGroup(login), Context.ConnectionId)
                     .ReceiveTypingIndicator(login.Username, isTyping);
    }

    // ── Helpers ──────────────────────────────────────────────────────

    // ALWAYS reconstruct LoginDTO from HTTP context — never accept it from the client payload
    private LoginDTO GetLoginDTO()
    {
        var httpContext = Context.GetHttpContext()
            ?? throw new InvalidOperationException("Hub invoked outside HTTP context.");
        return LoginDTO.FromHttpContext(httpContext);
    }

    // Group name convention: {module}:{sessionId}
    // Use ClientId for tenant-wide, SessionId for session-scoped, WorkOUId for OU-scoped
    private static string HubGroup(LoginDTO login) => $"collabspace:{login.SessionId}";
}
```

### LoginDTO — Always from Hub Context, Never from Client

```csharp
// CORRECT — reconstruct from verified HTTP context (token/claims)
private LoginDTO GetLoginDTO()
    => LoginDTO.FromHttpContext(Context.GetHttpContext()!);

// WRONG — trusting client-supplied identity allows impersonation
public async Task SendMessage(CollabMessageParameters param, LoginDTO login) { }
```

### Server-Initiated Push via IHubContext

When BLL needs to push to connected clients outside of a Hub method (e.g., after a background job,
after a REST endpoint write, after a Dapr event), inject `IHubContext<THub, TClient>`.

```csharp
// In BLL or a background service
public class NotificationBLL(
    IHubContext<CollabSpaceHub, ICollabSpaceClient> hubContext,
    ILogger<NotificationBLL> logger) : INotificationBLL
{
    public async Task BroadcastApproval(ApprovalDTO dto, LoginDTO login, CancellationToken ct)
    {
        var group = $"collabspace:{login.SessionId}";
        await hubContext.Clients.Group(group)
                        .ReceiveMessage(dto.ToCollabMessageDTO());
        logger.LogInformation("Approval broadcast to group {Group}", group);
    }
}
```

### Hub Group Naming Convention

| Scope | Pattern | Example |
|-------|---------|---------|
| Per session | `{module}:{sessionId}` | `collabspace:abc123` |
| Per tenant/client | `{module}:client:{clientId}` | `collabspace:client:42` |
| Per organizational unit | `{module}:ou:{workOUId}` | `collabspace:ou:7` |
| Per user | `{module}:user:{userId}` | `collabspace:user:99` |

Never use connection ID as a group — SignalR already supports `Clients.Client(connectionId)` directly.

### DI Lifetime — Hub is Transient

Hubs are **transient** by design — a new instance is created per Hub method invocation.
Consequences:

```csharp
// WRONG — Hub instance fields do NOT persist across calls; don't store state here
public class FooHub : Hub<IFooClient>
{
    private List<string> _messages = new(); // lost after each method call
}

// CORRECT — Hub has no mutable state; inject Scoped services normally
// Scoped services work correctly because Hub lifetime is within a single invocation scope
public class FooHub : Hub<IFooClient>
{
    private readonly ICollabSpaceBLL _bll;  // injected fresh each invocation — fine
    public FooHub(ICollabSpaceBLL bll) => _bll = bll;
}

// For cross-invocation state (e.g., presence tracking) — use IDistributedCache or Redis
// via a Singleton service, NOT the Hub instance
```

### Registration in Program.cs

```csharp
// Add once per SL project that contains Hubs
builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaximumReceiveMessageSize = 32 * 1024;  // 32 KB — raise only if justified
    options.ClientTimeoutInterval     = TimeSpan.FromSeconds(60);
    options.KeepAliveInterval         = TimeSpan.FromSeconds(15);
});

// Map after app.UseAuthorization()
app.MapHub<CollabSpaceHub>("/hubs/collabspace");
```

### High-Frequency Push — Backpressure with Channel<T>

For real-time feeds (e.g., live price ticks, log streaming) that can overwhelm slow clients:

```csharp
public async Task StreamLiveFeed(CancellationToken ct)
{
    var login = GetLoginDTO();
    var channel = Channel.CreateBounded<FeedItemDTO>(
        new BoundedChannelOptions(100)
        {
            FullMode = BoundedChannelFullMode.DropOldest  // drop stale items under backpressure
        });

    _ = ProduceFeedAsync(channel.Writer, login, ct);  // fire-and-forget producer

    await foreach (var item in channel.Reader.ReadAllAsync(ct))
        await Clients.Caller.ReceiveMessage(item.ToCollabMessageDTO());
}

private async Task ProduceFeedAsync(
    ChannelWriter<FeedItemDTO> writer, LoginDTO login, CancellationToken ct)
{
    try
    {
        await foreach (var item in _bll.StreamFeedAsync(login, ct))
            await writer.WriteAsync(item, ct);
    }
    finally
    {
        writer.TryComplete();
    }
}
```

### Memory Leak Prevention — SignalR Specific

```csharp
// WRONG — holding a reference to Clients/Groups outside of Hub scope
private IHubClients<IFooClient> _savedClients;  // dangling reference after disconnect

// WRONG — not removing from group on disconnect
public override Task OnConnectedAsync()
{
    Groups.AddToGroupAsync(Context.ConnectionId, "group");  // never removed
    return base.OnConnectedAsync();
}

// CORRECT — always mirror AddToGroup with RemoveFromGroup in OnDisconnectedAsync

// WRONG — connection ID dictionary that never evicts
private static readonly Dictionary<string, LoginDTO> _connections = new();
// (grows unbounded as connections accumulate)

// CORRECT — use IDistributedCache with sliding TTL, or ConcurrentDictionary + OnDisconnected cleanup
private static readonly ConcurrentDictionary<string, LoginDTO> _connections = new();
public override Task OnDisconnectedAsync(Exception? ex)
{
    _connections.TryRemove(Context.ConnectionId, out _);  // always clean up
    return base.OnDisconnectedAsync(ex);
}
```

### SignalR Rules Summary

- Hub classes live in `ModuleSL/Hubs/` — nowhere else.
- Always use `Hub<IClientContract>` (strongly typed) — never untyped `Hub`.
- All Hub methods are `async Task` — never `async void`, never synchronous.
- `LoginDTO` is always reconstructed from `Context.GetHttpContext()` — never accepted from client payload.
- No direct DAL access from Hubs — always through BLL, same as endpoints.
- No mutable state stored in Hub instance fields — Hub is transient.
- Always clean up group membership in `OnDisconnectedAsync`.
- Use `IHubContext<THub, TClient>` for server-push from BLL or background services.
- Use `Channel<T>` with bounded capacity for high-frequency streaming to prevent client overload.

---

## Caching

### Cache Level Selection

| Use Case | Level |
|----------|-------|
| Language / global config | `CacheKeyLevel.OVERALL` |
| Per database server | `CacheKeyLevel.DB_SERVER` |
| Per client/tenant | `CacheKeyLevel.CLIENT_LEVEL` |
| Per role | `CacheKeyLevel.ROLE_LEVEL` |
| Per user | `CacheKeyLevel.USER_LEVEL` |
| Session-specific data | `CacheKeyLevel.SESSION_LEVEL` |
| Highly volatile / transactional data | `CacheKeyLevel.NOT_REQUIRED` |

### Cache Key Generation

`KeyGenerator.KeyGeneration()` produces a SHA256 Base64 hash used as the Dapr statestore key.
Returns `"Null"` for `NOT_REQUIRED` level — `BaseEndpoint` skips cache when key is null/`"Null"`.

```csharp
// Single-record GET endpoint — objectId overload
protected override string? GetCacheKey(GetAccountParameters req, LoginDTO login)
    => KeyGenerator.KeyGeneration(
           req.AccountId,                  // int objectId
           EntityConstant.OBJECTACCOUNT,   // int objectTypeId
           CacheKeyLevel.CLIENT_LEVEL,
           login);

// List endpoint — criteria overload (includes paging in hash)
protected override string? GetCacheKey(GetAccountListParams req, LoginDTO login)
    => KeyGenerator.KeyGeneration(
           req.CriteriaDTO,               // CriteriaDTO criteria
           req.PageOffset,                // int pageOffset
           CacheKeyLevel.CLIENT_LEVEL,
           login);

// Write endpoints — return null → BaseEndpoint skips cache entirely
protected override string? GetCacheKey(SaveAccountParameters req, LoginDTO login) => null;
```

**Rules:**
- All read endpoints (`Get*`, `GetSelectList*`) MUST override `GetCacheKey` and return a non-null key.
- All write endpoints (`Save*`, `Update*`, `Delete*`) do NOT override — default returns null.
- Cache level for module master data: `CLIENT_LEVEL` (shared across tenant users).

### Cache Invalidation

After every write, BLL calls `KeyInvalidate.AllInvalidateCache(key)` to clear stale reads.
The key passed to invalidation must be built the same way the read endpoint builds it.

```csharp
// In BLL after CommitAsync — inject KeyInvalidate
var cacheKey = KeyGenerator.KeyGeneration(
    dto.AccountId, EntityConstant.OBJECTACCOUNT, CacheKeyLevel.CLIENT_LEVEL, login);
await _keyInvalidate.AllInvalidateCache(cacheKey);
```

### Caching Anti-Patterns to Avoid

```csharp
// WRONG — caching transactional/financial data without expiry
// WRONG — using USER_LEVEL cache for data shared across users
// WRONG — caching mutable reference data at OVERALL level
// WRONG — no cache invalidation after writes
// CORRECT — financial ledger/journal entries: NOT_REQUIRED
// CORRECT — dropdown master data: CLIENT_LEVEL with 5-min TTL
// CORRECT — language resources: OVERALL level
```

---

## Performance

### Database

1. **Always filter by tenant** — every query must include `ClientId`/`DatabaseName`.
2. **Use covering indexes** — QB comments must note required indexes.
3. **Avoid N+1** — use `QueryMultiMapAsync` or JOIN instead of loops.
4. **Use `ValueTask`** for hot paths that usually return synchronously.
5. **Prefer `IAsyncEnumerable`** (StreamAsync) over `List<T>` for large result sets.

### Memory

1. **Never store `IDbConnection` as a field** — connections are managed by `IQueryExecutor`.
2. **Dispose `IAsyncEnumerable` enumerators** — use `await foreach` with `ConfigureAwait(false)`.
3. **Do not cache `LoginDTO`** — it contains sensitive session data; reconstruct per-request.
4. **Large string building** — use `StringBuilder` or `Span<T>`, not string concatenation in loops.
5. **PDF/Excel generation** — stream to `MemoryStream` and dispose immediately after send; never hold in-process.

### Async Pitfalls

```csharp
// WRONG — sync-over-async causes threadpool starvation
var result = _bll.GetAccount(id, login).GetAwaiter().GetResult();

// WRONG — ConfigureAwait missing in library code
var result = await _dal.GetAccount(id, login);  // in BLL/DAL (non-ASP.NET context)

// CORRECT — library/non-UI code
var result = await _dal.GetAccount(id, login).ConfigureAwait(false);

// WRONG — Task.Run wrapping async
var result = await Task.Run(() => _dal.GetAccount(id, login));
```

---

## Memory Leak Prevention

### Disposable Resources

```csharp
// IDbConnection — NEVER hold as instance field
// CORRECT — let QueryExecutor manage connection lifetime

// MemoryStream for documents
using var ms = new MemoryStream();
// ... generate PDF ...
await Response.SendStreamAsync(ms, "application/pdf", cancellation: ct);
// ms disposed automatically

// HttpClient — NEVER instantiate directly
// CORRECT — inject IHttpClientFactory
public class FooService(IHttpClientFactory factory)
{
    private readonly HttpClient _http = factory.CreateClient("named-client");
}
```

### DI Lifetime Rules

| Service Type | Lifetime |
|-------------|----------|
| BLL, DAL, Validators | `Scoped` (per-request) |
| QueryExecutor, DbConnection | `Scoped` |
| Cache utilities, KeyGenerators | `Singleton` |
| HttpClient (via factory) | Managed by factory |
| LoginDTO / session state | **Never register** — pass as parameter |

```csharp
// WRONG — capturing scoped service in singleton
public class MySingleton(IScopedService scoped) { } // runtime error

// CORRECT — use IServiceScopeFactory if singleton needs scoped service
public class MySingleton(IServiceScopeFactory factory)
{
    public async Task DoWork()
    {
        using var scope = factory.CreateScope();
        var svc = scope.ServiceProvider.GetRequiredService<IScopedService>();
        await svc.DoSomething();
    }
}
```

### Event Handler & Timer Leaks

```csharp
// Always unsubscribe events in Dispose
public class FooService : IDisposable
{
    private readonly Timer _timer;
    public FooService() => _timer = new Timer(Callback, null, 0, 5000);
    public void Dispose() => _timer?.Dispose();
}
```

### Static Collections

```csharp
// WRONG — unbounded static Dictionary grows forever
private static readonly Dictionary<string, object> _cache = new();

// CORRECT — use IMemoryCache / IHybridCache with expiry, or ConcurrentDictionary with eviction
```

---

## Security

### Multi-Tenancy — TENANTID is Mandatory

Every GET query (single record or list) **must** include `TENANTID` in both the WHERE clause and the SELECT list.
Omitting it is a security violation that leaks cross-tenant data.

```csharp
// CORRECT — TENANTID in WHERE + SELECT
public const string GET_ACCOUNT = @"
    SELECT a.ACCOUNTID     AS AccountId,
           a.ACCOUNTNAME   AS AccountName,
           a.TENANTID      AS TenantId       -- always select TENANTID for DTO mapping
    FROM   MACCOUNT a
    WHERE  a.ACCOUNTID  = @AccountId
    AND    a.TENANTID   = @TenantId";       -- mandatory tenant filter

// WRONG — no tenant filter → cross-tenant data leak
public const string GET_ACCOUNT_BAD = @"
    SELECT a.ACCOUNTID, a.ACCOUNTNAME
    FROM   MACCOUNT a
    WHERE  a.ACCOUNTID = @AccountId";       -- security violation!
```

`@TenantId` is automatically populated by `IQueryExecutor` from `loginDTO.TenantId` — no manual assignment needed in DAL.

### Input Validation

```csharp
// BLL validates before calling DAL
public async Task<string> SaveAccount(AccountDTO dto, LoginDTO login, CancellationToken ct)
{
    _validation.NotEmpty(dto.AccountCode, nameof(dto.AccountCode));
    _validation.NotEmpty(dto.AccountName, nameof(dto.AccountName));
    // ... more rules
    return await _dal.SaveAccount(dto, login, ct);
}
```

### Sensitive Data

- Connection strings stored in **HashiCorp Vault** (`VaultSharp`) — never in appsettings.json.
- `LoginDTO` credentials encrypted before transport via `LoginEncryption`.
- Never log `LoginDTO` fields, connection strings, or financial amounts.
- Never return stack traces to the client — log server-side, return generic message.

---

## Dependency Injection Registration

Modules use **assembly scanning** — follow this pattern exactly so auto-registration picks up new classes:

```csharp
// Program.cs — already configured; just ensure classes implement the interface
services.AddScopedFromAssembly(typeof(IAccountBLL).Assembly);  // BLL assembly
services.AddScopedFromAssembly(typeof(IAccountDAL).Assembly);  // DAL assembly
```

New BLL/DAL classes are auto-registered if they implement their interface. No manual registration needed.

---

## Observability

### Logging

```csharp
// Use structured logging — never string interpolation in log calls
_logger.LogInformation("Account {AccountId} saved by user {UserId}", dto.AccountId, login.UserId);

// WRONG
_logger.LogInformation($"Account {dto.AccountId} saved by user {login.UserId}");

// Log levels:
// Debug    — high-frequency dev diagnostics (disabled in prod)
// Info     — significant business events (saves, approvals, logins)
// Warning  — recoverable issues (cache miss, retry)
// Error    — caught exceptions that affect a request
// Critical — process-level failures
```

### Event Log Publishing (Dapr)

Publish business events for audit trail:

```csharp
await _eventLogPublish.PublishEventLogAsync(new EventLogDTO
{
    EventText    = "Account Created",
    UserId       = login.UserId,
    Data         = JsonSerializer.Serialize(dto),
    EventTypeId  = EventTypes.CREATE,
    MachineIP    = login.MachineIP
}, login);
```

### OpenTelemetry Tracing

```csharp
// Add spans for significant operations
using var activity = ActivitySource.StartActivity("SaveAccount");
activity?.SetTag("account.id", dto.AccountId);
activity?.SetTag("client.id", login.ClientId);
```

---

## BLL Save Pattern — ExecuteSaveAsync

All entity save operations in the BLL follow this exact structure. Use this pattern for every module that creates or updates a domain entity.

### Structure

```
BeginTransaction
  → (if new) GetAutoNumber → assign to DTO
  → ExecuteSaveAsync
      → (inside callback) DAL.SaveEntity or DAL.UpdateEntity
  → CommitTransaction
catch
  → RollbackTransaction + rethrow
```

### Canonical Template

```csharp
public async Task<string> Save{Entity}({Entity}DTO {entity}DTO, LoginDTO loginDTO)
{
    var Trans = await _QueryExecutor.BeginTransactionAsync(loginDTO);
    try
    {
        bool isNew = {entity}DTO.{Entity}Id == 0;
        if (isNew)
        {
            var auto = await _AutoNumber.GetNumberAsync(
                1,
                AUTONUMBERCONSTANT.{ENTITY},
                loginDTO);

            {entity}DTO.{Entity}Id = auto.StartNumber;
        }

        await _baseEntityAppService.ExecuteSaveAsync(
            EntityConstant.{ENTITY},
            EventTypeConstant.SAVE{ENTITY}EVENTTYPEID,
            {entity}DTO,
            loginDTO,
            async tx =>
            {
                var result = isNew
                    ? await _{Entity}DAL.Save{Entity}({entity}DTO, loginDTO, tx)
                    : await _{Entity}DAL.Update{Entity}({entity}DTO, loginDTO, tx);

                return {entity}DTO.{Entity}Id;
            },
            new Dictionary<string, object?> { { "{ParentKeyName}", {entity}DTO.{ParentKeyProperty} } },
            BIZTRANSACTIONCLASSCONSTANT.{ENTITY},
            BizTransactionConstant.{ENTITY},
            Trans);

        await _QueryExecutor.CommitAsync(Trans);
        return isNew
            ? "Details saved successfully."
            : "Details updated successfully.";
    }
    catch (Exception)
    {
        await _QueryExecutor.RollbackAsync(Trans);
        throw;
    }
}
```

### ExecuteSaveAsync Parameter Order

| # | Parameter | What to supply |
|---|-----------|----------------|
| 1 | `entityConstant` | `EntityConstant.{ENTITY}` |
| 2 | `eventTypeId` | `EventTypeConstant.SAVE{ENTITY}EVENTTYPEID` |
| 3 | `dto` | The DTO being saved/updated |
| 4 | `loginDTO` | Pass through from method parameter |
| 5 | `dalCallback` | `async tx => { ... return entityId; }` |
| 6 | `extraMeta` | `Dictionary<string, object?>` for parent keys; `null` if none |
| 7 | `bizTransactionClass` | `BIZTRANSACTIONCLASSCONSTANT.{ENTITY}` |
| 8 | `bizTransaction` | `BizTransactionConstant.{ENTITY}` |
| 9 | `transaction` | `Trans` from `BeginTransactionAsync` |

### Constants Naming

| Class | Pattern | Example |
|-------|---------|---------|
| `EntityConstant` | `{ENTITY}` | `EntityConstant.TASK` |
| `EventTypeConstant` | `SAVE{ENTITY}EVENTTYPEID` | `EventTypeConstant.SAVETASKEVENTTYPEID` |
| `AUTONUMBERCONSTANT` | `{ENTITY}` | `AUTONUMBERCONSTANT.TASK` |
| `BIZTRANSACTIONCLASSCONSTANT` | `{ENTITY}` | `BIZTRANSACTIONCLASSCONSTANT.TASK` |
| `BizTransactionConstant` | `{ENTITY}` | `BizTransactionConstant.TASK` |

### Key Rules

- `BeginTransactionAsync` is called first — before the `isNew` check.
- Auto-number is assigned to the DTO **before** `ExecuteSaveAsync`, not inside the callback.
- The `dalCallback` receives `tx` and must pass it to every DAL call. It returns the entity ID.
- `CommitAsync` is on the happy path only. `RollbackAsync` is always in `catch`, always followed by `throw`.
- Do **not** use `await using` on `Trans` — explicit Commit/Rollback is required.
- Return strings are fixed: `"Details saved successfully."` / `"Details updated successfully."` — no variation.

### Required Injected Dependencies — Every Save BLL Must Have All Five

```csharp
private readonly I{Entity}DAL                         _{Entity}DAL;
private readonly AutoNumber                            _AutoNumber;
private readonly IQueryExecutor                        _QueryExecutor;    // BeginTransaction / Commit / Rollback
private readonly KeyInvalidate                         _KeyInvalidate;    // cache invalidation after write
private readonly BaseEntityAppService<{Entity}DTO>     _BaseEntityAppService; // ExecuteSaveAsync pipeline
```

If any of these five are absent, the save pipeline is **incomplete** — the module will bypass workflow, events, or leave cache stale.

### KeyInvalidate — Mandatory After Every CommitAsync

```csharp
await _QueryExecutor.CommitAsync(Trans);

// MANDATORY — immediately after commit, before returning
var cacheKey = KeyGenerator.KeyGeneration(
    dto.{Entity}Id, EntityConstant.OBJECT{ENTITY}, CacheKeyLevel.CLIENT_LEVEL, loginDTO);
await _KeyInvalidate.AllInvalidateCache(cacheKey);
```

Omitting this causes stale reads from Dapr cache until TTL expiry — every write must invalidate.

### Save Pattern Anti-Patterns

| Anti-Pattern | Why Wrong |
|-------------|-----------|
| DAL called outside the `ExecuteSaveAsync` callback | Bypasses workflow engine, Qualifier validation, OutBox audit |
| DAL managing its own `BeginTransaction/Commit/Rollback` | Transaction lifecycle belongs to BLL; DAL only receives `tx` |
| DAL write method returns `Task` or `Task<string>` (not `Task<Result<string>>`) | Caller cannot distinguish success/failure without exceptions |
| Auto-number assigned inside the DAL callback | DTO must be fully consistent before callback runs |
| Missing `RollbackAsync` in catch | Transaction left open — connection leak |
| Missing `KeyInvalidate.AllInvalidateCache` after commit | Stale reads after writes — cache out of sync |
| `catch { }` without rethrow | Silent failure hides bugs |
| Validation inside the `dalCallback` | Validation belongs before `BeginTransactionAsync` |

> **Canonical reference:** `GB5Solution/MM/MMBLL/SupplyGroup/SupplyGroupBLL.cs` — correct pattern with all five injections, `ExecuteSaveAsync`, and `KeyInvalidate`.
> **Skill file:** See `SKILL-bll-execute-save.md` for extended reference including anti-patterns and worked examples.

---

## What BaseEndpoint Provides Automatically

`BaseEndpoint.HandleAsync` handles all cross-cutting concerns before calling `ExecuteAsync`.
**BLL and DAL must never duplicate any of the following.**

| Concern | What BaseEndpoint does |
|---------|----------------------|
| OpenTelemetry tracing | `Tracer.StartActiveSpan(GetType().Name)` + child spans with `db.name`, `enduser.id`, `session.id` |
| Structured logging | `Logger.LogInformation/LogError` on request received, cache hit, and errors |
| Dapr caching | Calls `GetCacheKey()` → checks Dapr statestore → if miss, calls `ExecuteAsync` → saves result with TTL |
| WIP approval detection | Reads `X-Wip-Approval` header → sets `loginDto.WipApprovalId` |
| LoginDTO extraction | Deserializes from `[FromHeader] string Login` before `ExecuteAsync` is called |
| Error handling (prod) | Records exception on OTel span + calls `SendErrorsAsync` — never leaks stack traces |

**Prohibited in BLL/DAL:**
- `ActivitySource.StartActivity(...)` spans — BaseEndpoint already wraps the request
- `Logger.LogInformation(...)` per-request logs — BaseEndpoint logs request entry and errors
- Direct Dapr state-store reads — only BaseEndpoint reads/writes cache via `GetCacheKey`

---

## OutBox — Event Publishing

`IOutBox.PublishEventAsync` writes an event row to the `TOUTBOX` table **inside the same DB transaction** as the business data. This guarantees at-least-once delivery even if the process crashes.

A background service (`PublishPendingEventsAsync`) picks up PENDING rows and publishes to Dapr pub/sub, then marks them PUBLISHED or FAILED.

**OutBox DOES:**
- Write to `TOUTBOX` inside the business transaction (guaranteed delivery)
- Provide correlation key for cross-service tracing
- Trigger downstream subscribers via Dapr pub/sub

**OutBox does NOT:**
- Invalidate the Dapr read-cache — BLL must call `KeyInvalidate.AllInvalidateCache(key)` explicitly after commit
- Fire-and-forget real-time SignalR notifications — use `IHubContext` from BLL for that

```csharp
// OutBox is called INSIDE ExecuteSaveAsync automatically — no manual call in BLL needed.
// KeyInvalidate must be called AFTER CommitAsync:
await _queryExecutor.CommitAsync(Trans);
await _keyInvalidate.AllInvalidateCache(cacheKey);   // ← BLL responsibility, after commit
```

**Never call `DaprClient.PublishEventAsync` directly from BLL for audit events.** Use `IOutBox` via `ExecuteSaveAsync`. Direct Dapr publish is fire-and-forget and not transactional.

---

## Result<T> — DAL Return Pattern

DAL `Save`, `Update`, and `Delete` methods return `Result<string>`, not `Task<int>` or `Task<string>`.
`Result<T>` wraps success/failure without throwing exceptions for expected (non-exceptional) outcomes.

```csharp
// DAL — Save returns Result<string>
public async Task<Result<string>> SaveFoo(FooDTO dto, LoginDTO login, DbTransaction tx)
{
    // Set audit fields inside DAL (not BLL)
    dto.CreatedById = login.UserId;
    dto.CreatedOn   = DateTime.UtcNow;
    dto.ModifiedById = login.UserId;
    dto.ModifiedOn   = DateTime.UtcNow;

    int rows = await _qe.ExecuteAsync(login, FooQB.SAVE_FOO, dto, tx);
    return rows > 0
        ? Result<string>.Success("Foo saved successfully.")
        : Result<string>.Failure("Failed to save Foo.");
}

// DAL — Delete returns Result<string>
public async Task<Result<string>> DeleteFoo(int fooId, LoginDTO login, DbTransaction tx)
{
    int rows = await _qe.ExecuteAsync(login, FooQB.DELETE_FOO, new { FooId = fooId }, tx);
    return rows > 0
        ? Result<string>.Success("Foo deleted successfully.")
        : Result<string>.Failure("Foo not found or already deleted.");
}
```

**Rules:**
- DAL Save/Update/Delete always accept `DbTransaction tx` as the last parameter — passed from the `ExecuteSaveAsync` callback.
- Audit fields (`CreatedById/On`, `ModifiedById/On`) are set inside DAL, not BLL.
- Read methods (`GetFoo`, `GetFooList`) return plain `Task<T>` — no `Result<T>` needed.

---

## Error Handling

### BLL Pattern

```csharp
public async Task<string> SaveAccount(AccountDTO dto, LoginDTO login, CancellationToken ct)
{
    try
    {
        _validation.Validate(dto);
        var result = await _dal.SaveAccount(dto, login, ct);
        await _eventLog.PublishAsync(...);
        return ResponseHelper.Success(result);
    }
    catch (ValidationException vex)
    {
        return ResponseHelper.ValidationError(vex.Message);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "SaveAccount failed for AccountCode {Code}", dto.AccountCode);
        return ResponseHelper.SystemError();
    }
}
```

### Never Swallow Exceptions

```csharp
// WRONG — silent failure hides bugs
catch (Exception) { return string.Empty; }

// CORRECT — log and return structured error
catch (Exception ex)
{
    _logger.LogError(ex, "...");
    throw;  // or return error DTO
}
```

---

## Internationalization (i18n)

### Resource Classes — Non-Negotiable

All user-visible messages returned from BLL methods MUST use the strongly-typed resource classes
in `GB5Shared.Resource.Response`, NOT hardcoded English strings.

```csharp
// CORRECT — resource string, translatable
using GB5Shared.Resource.Response;

return isNew ? SuccessResponse.SaveSuccess : SuccessResponse.UpdateSuccess;
return SuccessResponse.DeleteSuccessMessage;

// WRONG — hardcoded, breaks i18n
return isNew ? "Details saved successfully." : "Details updated successfully.";
return "Details deleted successfully.";
```

### Available Resource Properties

| Use Case | Property | Current value |
|----------|----------|--------------|
| Create success | `SuccessResponse.SaveSuccess` | "Details Saved Successfully" |
| Update success | `SuccessResponse.UpdateSuccess` | "Details Updated Successfully" |
| Delete success | `SuccessResponse.DeleteSuccessMessage` | "Details have been deleted successfully" |
| Create success with ID | `$"{SuccessResponse.SaveSuccessMessage} {id}"` | "Details have been saved successfully with id = {id}" |
| Update success with ID | `$"{SuccessResponse.UpdateSuccessMessage} {id}"` | "Details have been updated successfully with id = {id}" |

### Adding New Messages

When an operation requires a message not covered above, add a new entry to:
1. `GB5Shared/Resource/Response/SuccessResponse.resx` (or `ErrorResponse.resx`) — add the XML `<data>` element
2. `GB5Shared/Resource/Response/SuccessResponse.Designer.cs` — add the corresponding `public static string` property following the existing auto-generated pattern

Do NOT return raw English strings as a shortcut — ever.

### Reference Implementation

`GB5Solution/Costing/CostingBLL/CostAnalysis/CostAnalysisBLL.cs` — correct pattern using `SuccessResponse.SaveSuccessMessage`.

---

## New Module Checklist

When adding a new module, follow this order:

- [ ] Create `ModuleDAL/DTOs/FooDTO.cs`
- [ ] Create `ModuleDAL/QueryBuilders/FooQB.cs` — SQL constants with index notes
- [ ] Create `ModuleDAL/Interfaces/IFooDAL.cs`
- [ ] Create `ModuleDAL/Implementations/FooDAL.cs` — inject `IQueryExecutor`
- [ ] Create `ModuleBLL/Interfaces/IFooBLL.cs`
- [ ] Create `ModuleBLL/Implementations/FooBLL.cs` — inject `IFooDAL`, `IValidation`, `ILogger<FooBLL>`
- [ ] Create `ModuleSL/Parameters/GetFooParameters.cs`
- [ ] Create `ModuleSL/Endpoints/GetFoo.cs`, `SaveFoo.cs`, `DeleteFoo.cs`
- [ ] Add cache key logic in each read endpoint's `GetCacheKey()`
- [ ] Add cache invalidation in BLL after every write
- [ ] Register new `.csproj` references if new project added
- [ ] Add required DB indexes to migration script

**If module requires real-time (SignalR):**

- [ ] Create `ModuleSL/Hubs/IFooClient.cs` — strongly-typed client contract
- [ ] Create `ModuleSL/Hubs/FooHub.cs` — inherits `Hub<IFooClient>`, no DAL access
- [ ] Implement `OnConnectedAsync` / `OnDisconnectedAsync` with group add/remove
- [ ] Implement `GetLoginDTO()` extracting from `Context.GetHttpContext()` — not from params
- [ ] Register `app.MapHub<FooHub>("/hubs/foo")` in `Program.cs`
- [ ] Add `builder.Services.AddSignalR()` if not already present in this SL project
- [ ] Inject `IHubContext<FooHub, IFooClient>` into BLL methods that need server-push

---

## Anti-Patterns — Prohibited

| Anti-Pattern | Why Prohibited |
|-------------|----------------|
| EF Core DbContext (for CRUD) | Not used — Dapper + raw SQL is the standard |
| MVC Controllers | Not used — FastEndpoints only |
| `async void` methods | Exceptions are unobservable; use `async Task` |
| `.Result` / `.Wait()` on Tasks | Deadlocks in ASP.NET context |
| `Thread.Sleep` | Blocks threadpool; use `Task.Delay` |
| Global mutable static state | Race conditions, memory leaks |
| Storing `IDbConnection` as a field | Connection leaks |
| `new HttpClient()` | Socket exhaustion; use `IHttpClientFactory` |
| Unbounded `List<T>` from DB | OOM on large tables; use paging or streaming |
| SQL string concatenation | SQL injection |
| Cross-tenant data access | Security violation — always filter by `ClientId`/`DatabaseName` |
| Logging sensitive fields (passwords, tokens) | Security violation |
| Caching financial/ledger data | Stale data in accounting; always query live |
| `async void` Hub methods | Exceptions are silently swallowed; use `async Task` |
| Untyped `Hub` base class | No compile-time check on client method names; use `Hub<TClient>` |
| Accepting `LoginDTO` as a Hub method parameter | Client-supplied identity enables impersonation |
| Mutable state in Hub instance fields | Hub is transient — state is lost between invocations |
| Missing `OnDisconnectedAsync` group cleanup | Groups accumulate stale connection IDs — memory leak |
| Calling DAL directly from a Hub | Bypasses BLL validation and event publishing |
| Unbounded connection tracking dictionary | Grows forever without disconnect cleanup |

---

## Technology Stack Reference

| Concern | Technology |
|---------|-----------|
| Runtime | .NET 9, C# 13 |
| HTTP Framework | FastEndpoints 6.x |
| Data Access | Dapper 2.x (no EF) |
| SQL Server driver | Microsoft.Data.SqlClient 6.x |
| PostgreSQL driver | Npgsql 8.x |
| Distributed messaging | Dapr (pub/sub, state store) |
| Caching | HybridCache + Redis (StackExchange.Redis) |
| Secrets | HashiCorp Vault (VaultSharp) |
| Serialization | System.Text.Json (primary) + Newtonsoft.Json (legacy) |
| PDF generation | iText7 / QuestPDF / PuppeteerSharp |
| Excel | ClosedXML |
| Real-time / bidirectional | ASP.NET Core SignalR (`Hub<TClient>`, `IHubContext`) |
| Observability | OpenTelemetry → Zipkin/Jaeger |
| Multi-database | SQL Server + PostgreSQL (runtime switchable per `LoginDTO.DatabaseType`) |

---

## List Query Infrastructure (GB5Shared/ListQuery/)

### The Pattern — Zero Handler Files

Module developers write **one class per query variant** (the QB class implementing `IQueryBuilder<TQuery, TResult>`).
`GenericListHandler<TQ,TR>` is auto-registered by `AddListInfrastructure(assembly)` at startup.
`SqlListHandler<TQ,TCriteria,TR>` remains as an escape hatch for handlers that need custom logic beyond what GenericListHandler provides.

```
New query variant checklist:
  1. Add criteria class implementing IListCriteria (in *DTO.cs)
  2. Add query record implementing IListQuery<TResult, TCriteria> (in *DTO.cs)
  3. Add QB class implementing IQueryBuilder<TQuery, TResult> (in *QB.cs)
     — declare private static _sort allowlist
     — call SqlClauses.OrderBy() to build safe ORDER BY
     — delegate to static Build* method with orderBy string
  4. Nothing else. DI wires the handler at startup.
```

### IQueryBuilder — the only file you write

```csharp
public sealed class PendingMMDocumentsQB
    : IQueryBuilder<PendingMMDocumentsQuery, PendingAllocationListDTO>
{
    private static readonly IReadOnlyDictionary<string, string> _sort =
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        { ["headerdate"] = "h.MMHEADERDATE", ["partyname"] = "pt.PARTYNAME" };

    public (string Sql, DynamicParameters Params) Build(
        PendingMMDocumentsQuery query, ISqlDialect dialect)
    {
        var orderBy = SqlClauses.OrderBy(
            query.Criteria.SortBy, query.Criteria.SortDesc,
            _sort, defaultCol: "h.MMHEADERID DESC");
        return PendingAllocationQB.BuildPendingMMDocuments(query.Criteria, dialect, orderBy);
    }
}
```

### GenericListHandler — owned by GB5Shared

Handles guard validation → dialect → QB.Build() → execute → timing log. Never subclass it.
Slow query threshold: >2000ms → `LogWarning`; ≤2000ms → `LogDebug`.

### IQueryGuard — mandatory filter enforcement

```csharp
public sealed class RequiresOUFilter : IQueryGuard<PendingMMDocumentsQuery>
{
    public void Validate(PendingMMDocumentsQuery query, LoginDTO login)
    {
        if (query.Criteria.OUId is null)
            throw new ArgumentException("OUId is required for pending MM documents.");
    }
}
```

Register automatically by placing the class in MMDAL — `AddListInfrastructure()` finds it.
Guards are sync, fast, and must not call the DB.

### QueryIntent + [QuerySource]

```csharp
[QuerySource(QueryIntent.Reporting)]
public sealed record PendingMMDocumentsQuery(PendingMMDocumentCriteria Criteria)
    : IListQuery<PendingAllocationListDTO, PendingMMDocumentCriteria>;
```

Defaults to `Transactional` if absent. Phase 2 will use intent for DB routing.

### Sort Safety — non-negotiable

```csharp
// CORRECT — allowlist validates before building ORDER BY
var orderBy = SqlClauses.OrderBy(criteria.SortBy, criteria.SortDesc, _sort, "h.MMHEADERID DESC");

// WRONG — SQL injection via client-supplied sort column
string sql = $"ORDER BY {criteria.SortBy}";   // NEVER
```

`SqlClauses.MultiOrderBy(sortFields, _sort, defaultCol)` supports compound sort tokens like `["headerdate:desc","partyname:asc"]`.

### DI Registration (Program.cs)

```csharp
// Scans MMDAL for IQueryBuilder implementations → auto-registers GenericListHandler for each
builder.Services.AddListInfrastructure(mmDALAssembly);
```

---

## Critical Analysis Culture

Engineers must push back on inefficient or incorrect requirements. This applies to both code review and requirements analysis.

### When to push back

- A suggestion introduces a known anti-pattern (memory leak, SQL injection risk, deadlock vector).
- A requirement duplicates existing infrastructure — "we already have X" is the right answer.
- A design adds complexity without payoff for the actual scale of the problem.
- A pattern was copied from a sample but is incompatible with GB5's existing abstractions.

### How to push back

- State the problem specifically: "this causes X because Y" — not vague discomfort.
- Propose the correct alternative — don't just reject, redirect.
- If uncertain, say so and investigate before coding. Never implement a design you don't understand.

### Rules

- Truth over comfort. A polite wrong answer costs more than a blunt correct one.
- No cowpath paving — migrating a bad pattern from GB4 to GB5 is not progress.
- No speculative abstractions — build for today's requirements, not imagined future ones.
- Pushback is a sign of engagement, not obstruction. Silence on a bad design is the real failure.

---

## Code Generation Quality Rules

These rules exist because each item below has caused post-generation defects in real modules.
Violating any of them produces code that does not compile or does not behave correctly.

---

### 1 — Using Directives: Verify Before You Write

Every `using` statement emitted in generated code must correspond to a namespace that actually
exists in the project or its referenced assemblies.

**Rules:**
- Never assume a namespace — derive it from the actual project structure (`*BLL`, `*DAL`, `*SL`, `GB5Shared.*`, `GB5Framework.*`).
- Do not copy `using` lines from unrelated files unless the same assembly is referenced by the target `.csproj`.
- If a type is from a shared library, confirm it lives in the assembly that the target project already references.
- Missing or phantom `using` lines are a build error — generate none rather than generate wrong ones.

---

### 2 — CancellationToken: Mandatory End-to-End Propagation

`CancellationToken ct` must appear in **every** async method signature across all three layers and must be forwarded at every call site. No layer may drop it silently.

```csharp
// SL endpoint
protected override async Task<ResponseStandardDTO<object>> ExecuteAsync(
    GetFooParameters req, LoginDTO login, CancellationToken ct)
    => await _bll.GetFoo(req.FooId, login, ct);        // ← ct forwarded

// BLL
public async Task<FooDTO?> GetFoo(int fooId, LoginDTO login, CancellationToken ct)
    => await _dal.GetFoo(fooId, login, ct);            // ← ct forwarded

// DAL
public async Task<FooDTO?> GetFoo(int fooId, LoginDTO login, CancellationToken ct)
    => await _qe.QuerySingleAsync<FooDTO>(login, FooQB.GET_FOO, new { FooId = fooId }, ct);
```

**Rules:**
- SL `ExecuteAsync` always receives `CancellationToken ct` from the framework — always pass it down.
- BLL and DAL method signatures must include `CancellationToken ct` as the last parameter.
- `IQueryExecutor` methods accept a `cancellationToken` named parameter — always supply it.
- Never add `CancellationToken.None` as a workaround; fix the signature instead.

---

### 3 — Shared Library Functions: Verify Name and Signature

Before calling any method from `GB5Shared.*` or `GB5Framework.*`, verify:
1. The type exists in the assembly.
2. The method name is exact (case-sensitive).
3. The parameter list matches the actual overload.

**Common verified utilities and their exact signatures:**
- `KeyGenerator.KeyGeneration(int objectId, int objectTypeId, CacheKeyLevel level, LoginDTO login)`
- `KeyGenerator.KeyGeneration(CriteriaDTO criteria, int pageOffset, CacheKeyLevel level, LoginDTO login)`
- `KeyInvalidate.AllInvalidateCache(string cacheKey)`
- `SqlClauses.OrderBy(string? sortBy, bool sortDesc, IReadOnlyDictionary<string,string> allowlist, string defaultCol)`
- `SuccessResponse.SaveSuccess`, `SuccessResponse.UpdateSuccess`, `SuccessResponse.DeleteSuccessMessage`

**Rules:**
- Do not invent method names or overloads — if uncertain, state the uncertainty and ask rather than guess.
- Do not call `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` on any of these utilities.
- Do not substitute `ResponseHelper.Success(...)` or similar names unless you can confirm they exist.

---

### 4 — DI Registration and Project References

Every new class must be wired into the runtime. Generating the class file without the registration is incomplete.

**Rules:**
- New BLL class → implements `IFooBLL` → auto-registered by `AddScopedFromAssembly` — no manual step needed, but the `.csproj` of the SL project must reference the BLL assembly.
- New DAL class → implements `IFooDAL` → same auto-registration rule, same `.csproj` check.
- New SignalR Hub → `app.MapHub<FooHub>("/hubs/foo")` must be added to `Program.cs`.
- New background/hosted service → `builder.Services.AddHostedService<FooService>()` must be added to `Program.cs`.
- If a new project (`.csproj`) is added → list every required `<ProjectReference>` and instruct the user to add it; do not silently skip.
- Every `Program.cs` change must be listed explicitly — do not assume "the developer will wire it up."

---

### 5 — Parameters Class: One File, Correct Folder, One Entity

Parameters classes (request DTOs for endpoints) follow strict placement and naming rules.

**Rules:**
- Location: always `ModuleSL/Parameters/GetFooParameters.cs` — never inside `ModuleBLL`, `ModuleDAL`, or any other folder.
- File name: `{Verb}{Entity}Parameters.cs` — one parameters class per file, one file per endpoint.
- Never combine two or more parameter classes in a single file.
- Never reuse a parameters class across two different endpoints.

```
// CORRECT file layout
MMSL/Parameters/
  GetWorkCenterParameters.cs        ← one class, one file
  SaveWorkCenterParameters.cs       ← one class, one file
  GetWorkCenterListParameters.cs    ← one class, one file

// WRONG
MMSL/Parameters/
  WorkCenterParameters.cs           ← multiple classes in one file — prohibited
```

---

### 6 — Parameter/Endpoint Contract: Properties Must Match Exactly

Every property referenced inside `ExecuteAsync` or `GetCacheKey` must exist on the corresponding Parameters class with the same name and type.

**Rules:**
- Generate the Parameters class and the endpoint in the same pass; verify every property access.
- If the endpoint reads `req.WorkCenterId`, the Parameters class must declare `public int WorkCenterId { get; set; }`.
- Do not add properties to the Parameters class that are not used by the endpoint — dead properties create confusion.
- Do not use `req.Id` as a shorthand when the class declares `req.WorkCenterId`.

**Checklist before finalising an endpoint:**
- [ ] Every `req.*` access in `ExecuteAsync` has a matching property in the Parameters class.
- [ ] Every `req.*` access in `GetCacheKey` has a matching property in the Parameters class.
- [ ] No property declared in the Parameters class goes unreferenced.

---

### 7 — SQL Table and Column Names: Use Actual DB Names

QB files must use the **exact** database table name and column name. Assumed or camel-cased names cause runtime errors with no compile-time warning.

**Rules:**
- Table names in SQL are uppercase by convention in this project (e.g., `MWORKCENTER`, `MACCOUNT`). Never lowercase them.
- Column names are also uppercase (e.g., `WORKCENTERID`, `WORKCENTERNAME`, `TENANTID`). Never use camel-case in SQL.
- If the actual table/column name is not known, state this explicitly — do not guess or derive from the DTO property name.
- The Dapper parameter placeholder (`@WorkCenterId`) may be camel-case but the SQL column name must be the DB column name.
- All GET queries must include `TENANTID` in both the `SELECT` list and the `WHERE` clause (see Security section).

```csharp
// CORRECT — real DB names
public const string GET_WORK_CENTER = @"
    SELECT wc.WORKCENTERID   AS WorkCenterId,
           wc.WORKCENTERNAME AS WorkCenterName,
           wc.TENANTID       AS TenantId
    FROM   MWORKCENTER wc
    WHERE  wc.WORKCENTERID = @WorkCenterId
    AND    wc.TENANTID     = @TenantId";

// WRONG — assumed camel-case names
public const string GET_WORK_CENTER_BAD = @"
    SELECT wc.WorkCenterId, wc.WorkCenterName
    FROM   WorkCenter wc
    WHERE  wc.WorkCenterId = @WorkCenterId";
```

---

### 8 — One Entity, One File: No Bundled Classes

Every class — DTO, interface, implementation, query builder, endpoint, parameters — gets its own file. Never place two logically distinct entities in the same `.cs` file.

**Rules:**
- One DTO class → one file: `WorkCenterDTO.cs`, `RoutingVersionDTO.cs` — not `MMDTOs.cs`.
- One interface → one file: `IWorkCenterBLL.cs`, `IWorkCenterDAL.cs`.
- One implementation → one file: `WorkCenterBLL.cs`, `WorkCenterDAL.cs`.
- One QB → one file: `WorkCenterQB.cs`.
- One endpoint → one file: `GetWorkCenter.cs`, `SaveWorkCenter.cs`.
- One parameters class → one file: `GetWorkCenterParameters.cs`.
- Folder structure determines the module grouping — file bundling is never a substitute.

```
// CORRECT
MMDAL/DTOs/WorkCenter/
  WorkCenterDTO.cs
  WorkCenterListDTO.cs

// WRONG
MMDAL/DTOs/
  WorkCenterDTOs.cs   ← contains WorkCenterDTO and WorkCenterListDTO — prohibited
```
