API Layer (ServiceDeskLite.Api)
Middleware Pipeline Order (Program.cs)
1. Serilog configuration (before WebApplication builder)
2. Services:
AddApiDocumentation → OpenAPI
AddApiErrorHandling → ProblemDetails + ExceptionHandler + Mapper
AddApplication → use-case handlers (Scoped)
AddApiInfrastructure → persistence provider switch
3. EF Core auto-migration (Postgres only)
4. app.UseSerilogRequestLogging()
5. app.UseApiDocumentation()
6. app.UseApiErrorHandling() → UseExceptionHandler()
7. app.UseHttpsRedirection()
8. app.UseCors("WebDev") → https://localhost:7023 only
9. Endpoint mapping
Endpoints
Tickets (TicketsEndpoints.cs) — base route group: /api/v1/tickets
| HTTP | Route | Handler | Returns |
|---|---|---|---|
| GET | /api/v1/tickets | SearchTicketsAsync | 200 OK + PagedResponse<TicketListItemResponse> |
| POST | /api/v1/tickets | CreateTicketAsync | 201 Created + CreateTicketResponse |
| GET | /api/v1/tickets/{id:guid} | GetTicketByIdAsync | 200 OK + TicketResponse |
| POST | /api/v1/tickets/{id:guid}/status | ChangeTicketStatusAsync | 200 OK + TicketResponse |
| POST | /api/v1/tickets/{id:guid}/comments | AddCommentAsync | 201 Created + CommentResponse |
| POST | /api/v1/tickets/{id:guid}/assign | AssignTicketAsync | 200 OK + TicketResponse |
| GET | /api/v1/tickets/{id:guid}/audit-events | GetAuditEventsAsync | 200 OK + AuditEventResponse[] |
Dashboard (DashboardEndpoints.cs) — base route group: /api/v1/dashboard
| HTTP | Route | Handler | Returns |
|---|---|---|---|
| GET | /api/v1/dashboard/summary | GetDashboardSummaryAsync | 200 OK + DashboardSummaryResponse |
All errors return RFC 9457 ProblemDetails via ResultToProblemDetailsMapper.
Request Lifecycle
The following sequence covers the POST /api/v1/tickets happy path. All other endpoints follow the same structure.
Correlation
public static class Correlation
{
public static string GetTraceId(HttpContext ctx)
=> Activity.Current?.Id
?? ctx.TraceIdentifier
?? "unknown";
}
TraceId is attached to every ProblemDetails response as the traceId extension field.
ResultToProblemDetailsMapper
public sealed class ResultToProblemDetailsMapper
{
public IResult ToHttpResult<T>(
HttpContext ctx, Result<T> result, Func<T, IResult> onSuccess)
public IResult ToProblem(HttpContext ctx, ApplicationError error)
}
Uses ApiProblemDetailsFactory to produce RFC 9457 responses with extensions:
code, errorType, traceId, meta.
ResultMappingExtensions
Fluent bridge: result.ToHttpResult(ctx, mapper, value => Results.Ok(value.ToResponse())).
Exception Handling Pipeline
ApiExceptionHandler : IExceptionHandlercatches all unhandled exceptionsExceptionClassification.IsClientBadRequest(ex)→BadHttpRequestException, FormatException, InvalidOperationException→ 400ExceptionClassification.IsCancellation(ex, ctx)→ request cancelled → no response- All other exceptions → 500 Unexpected
Logging: 5xx → ERROR, 409 → WARNING, 400 → WARNING, others → INFO.
Enum Mapping (Api/Mapping/Tickets/TicketEnumMapping.cs)
// Contracts → Domain
public static DomainTicketPriority ToDomain(this TicketPriority value)
public static DomainTicketStatus ToDomain(this TicketStatus value)
// Contracts → Application
public static AppTicketSortField ToApplication(this TicketSortField value)
public static AppSortDirection ToApplication(this SortDirection value)
// Application → Contracts
public static TicketResponse ToResponse(this TicketDetailsDto dto)
public static TicketListItemResponse ToListItemResponse(this TicketListItemDto dto)
public static Paging ToPaging(this SearchTicketsRequest request)
public static SortSpec? ToSort(this SearchTicketsRequest request)
public static PagedResponse<TicketListItemResponse>
ToPagedResponse(this PagedResult<TicketListItemDto> page)
appsettings.json (Production)
{
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } },
"Persistence": { "Provider": "Postgres" },
"ConnectionStrings": { "ServiceDeskLite": "Host=localhost;Port=5432;Database=servicedesklite;Username=postgres;Password=postgres" },
"AllowedHosts": "*"
}
appsettings.Development.json
{
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } },
"Persistence": { "Provider": "InMemory" }
}
OpenAPI Contract Snapshot
The API exposes an OpenAPI v1 specification which is:
- Generated from the running Minimal API
- Snapshotted into
docs/api/openapi.v1.json - Rendered via Swagger UI on GitHub Pages
- Verified in CI to detect contract drift
Purpose
This ensures:
- Contract-driven development
- Transparent API surface for consumers
- Deterministic documentation builds
- Early detection of breaking changes
The OpenAPI snapshot acts as a versioned contract artifact between API and Web layer.