Imported from klinkby/booqr (
src/Klinkby.Booqr.Application/AGENTS.md). Install upstream withnpx skills add klinkby/booqr --skill Klinkby.Booqr.Application. Copyright stays with the author.
Application Layer (Klinkby.Booqr.Application)
Purpose
The Application layer contains the business logic and orchestrates operations between the API and Infrastructure layers. It implements use cases through Commands and Services while remaining independent of I/O concerns.
Architectural Rules
Dependencies
- Internal References: Only references
Coreinternally - MUST NOT reference
Infrastructure: Application layer is I/O agnostic - Third-party libraries allowed: Limited to business logic concerns (BCrypt, JWT, DI abstractions)
I/O Restrictions
No direct I/O or data-access dependencies. Must NOT depend on types in namespaces matching:
- ❌
Dapper - ❌
System.Console - ❌
System.IO - ❌
System.Net - ❌
System.Data - ❌
Npgsql
Immutability
- Classes whose names end with
Requestare immutable - Use
recordtypes for request DTOs
Contents
- Commands: ICommand implementations (use case orchestration)
- Services: Business logic services (e.g., token generation, password hashing)
- Background services: Hosted services for async processing (email, reminders)
- Request/Response DTOs: Data transfer objects
Purpose
- Use-case orchestration
- Business rules enforcement
- Transaction coordination
- Authorization logic
Key Patterns
Commands (ICommand)
Commands encapsulate use cases and orchestrate:
- Authorization checks (validate user access)
- Transaction management (Begin → Commit/Rollback)
- Repository interactions (via interfaces from Core)
- Business rule validation
Example structure:
public sealed class DeleteBookingCommand(
IBookingRepository bookings,
ICalendarRepository calendar,
ITransaction transaction,
ILogger<DeleteBookingCommand> logger) : ICommand<DeleteBookingRequest, Task<Result<bool>>>
{
public async Task<Result<bool>> Execute(DeleteBookingRequest request, CancellationToken ct)
{
// 1. Authorize
if (!IsAuthorized(request.User, booking.CustomerId))
return Problem.Forbidden with { Detail = "You do not have access to delete this booking" };
// 2. Begin transaction
await transaction.Begin(IsolationLevel.ReadCommitted, ct);
try
{
// 3. Business logic
var deleted = await bookings.Delete(id, ct);
// 4. Side effects
await calendar.Add(vacancy, ct);
// 5. Commit
await transaction.Commit(ct);
return deleted;
}
catch
{
await transaction.Rollback(ct);
throw;
}
}
}
Result/Problem Pattern
Commands report expected failures (not found, forbidden, unauthorized, validation, conflict) as data, not exceptions. Execute returns Task<Result<T>> (or Task<Result<bool>> for delete/update) instead of throwing.
Result<T>(Result.cs) is a closed discriminated union:Result<T>.Success(T Value)orResult<T>.Fault(Problem Problem).Result(non-generic) is thebool-less equivalent used where there's no payload.Problem(Problem.cs) is an RFC 7807-shaped record (Type,Title,HttpStatusCode,Detail). Reuse the static instances (Problem.NotFound,.ValidationFailed,.Unauthorized,.Forbidden,.Conflict,.MidAirCollision) and customize withwith { Detail = "..." }.- Implicit conversions remove boilerplate:
return someValue;becomesSuccess, andreturn Problem.NotFound with { ... };becomesFault— no need to constructnew Result<T>.Success(...)explicitly. - API layer mapping:
CommandExtensions(Klinkby.Booqr.Api/Util/CommandExtensions.cs) pattern-matchesResult<T>and callsProblem.ToProblemHttpResult()onFault, turning it into aProblemHttpResultviaTypedResults.Problem. Endpoints never see raw exceptions for these cases. - Prefer returning a
Problemover throwing whenever the failure is an expected outcome of the use case (not found, access denied, validation failed, optimistic-concurrency conflict, business-rule violation the caller can act on).
Authorization Patterns
- Customers: Only access their own resources (
user.Id == targetUserId) - Employees/Admins: Access any resources
- Check authorization BEFORE calling repositories
- Return
Problem.Forbidden(already-authenticated user acting on a resource they don't own) orProblem.Unauthorized(failed/missing authentication) instead of throwing — seeResult/Problem Patternabove
When to Still Throw
Exceptions are reserved for truly exceptional, non-recoverable conditions the caller isn't expected to handle as a business outcome:
- Programming/contract violations:
ArgumentNullException.ThrowIfNull(query), missing/invalid auth claims (InvalidClaimException) - Invariant violations that indicate a bug:
UnreachableExceptionfor exhaustive switches,InvalidOperationExceptionwhen a just-created entity can't be re-read - Anything genuinely unrecoverable at the use-case level (let it propagate to
StatusCode.FromExceptionin the API layer for a 5xx/502/504 mapping)
Do not throw for conditions a caller can reasonably branch on (not found, forbidden, validation, conflict) — model those as a Problem instead.
Background Services
- EmailWorker: Processes email queue via channels
- ReminderService: CRON-scheduled reminder delivery
Activity Recording
- ActivityRecorder: Records audit events synchronously on the request's own tenant connection (via
IActivityRepository.Record), not through a background channel. Writes are best-effort — a failed insert is logged and swallowed in the Infrastructure repository, so audit recording never faults the surrounding use case. On a tenant connectiontenant_idis stamped by the RLS column DEFAULT, so no cross-tenant (booqr_batch/BYPASSRLS) access is used. For commands with an explicit transaction, record aftertransaction.Commit.
Testing Guidelines
See tests/Klinkby.Booqr.Application.Tests/Commands/AGENTS.md for detailed testing practices.
Key principles:
- Mock repositories/services with Moq
- Use
NullLogger<T>.Instance(never mock ILogger) - Accept
DateTime t0from[ApplicationAutoData]for deterministic time - Verify transaction lifecycle:
Begin→Commiton success,Rollbackon exception - Assert repository calls are skipped (
Times.Never) when unauthorized - Assert on the
Result<T>shape (.IsSuccess,Result<T>.Fault { Problem: ... }) rather than expecting exceptions for expected-failure paths
Key Dependencies
From .csproj:
- BCrypt.Net-Next: Secure password hashing
- System.IdentityModel.Tokens.Jwt: JWT token generation
- ServiceScan.SourceGenerator: Automatic DI registration
- Microsoft.Extensions.* : Logging, DI, Configuration, Options, Hosting abstractions
Enforcement
These rules are enforced through:
- Automated tests:
Klinkby.Booqr.TestsusesTngTech.ArchUnitNETto validate:- No
Infrastructurereferences - No forbidden I/O namespace dependencies
*Requesttypes are immutable
- No
- Code review: Manual verification during PR review
Related Documentation
- ARCHITECTURE.md - Complete architectural policies
- tests/AGENTS.md - General testing guidelines
- tests/Klinkby.Booqr.Application.Tests/Commands/AGENTS.md - ICommand testing guidelines
- src/Klinkby.Booqr.Core/AGENTS.md - Core layer guidelines