API Models & Mapping¶
API models are a separate type world from domain messages. Mapping extensions are the explicit bridge between the two.
Folder layout¶
Models/
Downloads/
AddDownloadRequest.cs
DownloadResponse.cs
DownloadStatus.cs
Feed/
Shared/
ErrorResponse.cs
OperationResult.cs
Extensions/
DownloadsMappingExtensions.cs
- Request and response records live in
Models/{Feature}/subfolders. - The namespace stays flat (
YourApp.Api.Models). Subfolders are physical organization only. - Shared types (
ErrorResponse,OperationResult, shared enums) live inModels/Shared/. - Enums live with the feature that owns them, not in a central
Enums.cs.
One role per file
A file contains either request records or response records, not both. The exception is a matched request/response pair for a single endpoint.
Enums over strings¶
Request and response records use enum types for every known finite value. Never use string for a value from a known set.
public enum DownloadStatus
{
Unknown = 0,
Queued = 1,
Running = 2,
Completed = 3
}
public sealed record DownloadResponse(Guid Id, DownloadStatus Status, DateTimeOffset CreatedAt);
- Query parameters that represent enums bind directly as the enum type.
- Never call
.ToString()on an enum in a response record. DateTimeOffsetandTimeOnlystay typed. Do not format them as strings.- Only exception: values passed through 1:1 to an external API.
Unknown = 0
Start every new enum with Unknown = 0. Unrecognized input binds to Unknown instead of failing with an automatic 400, and handlers reject it explicitly before mapping.
Mapping extensions¶
Mapping lives in Extensions/{Feature}MappingExtensions.cs.
| Method | Direction |
|---|---|
ToApi() |
domain to API model |
ToMessage() / ToCommand() |
API request to domain message |
public static class DownloadsMappingExtensions
{
public static DownloadResponse ToApi(this DownloadSnapshot s) =>
new(s.Id, s.Status.ToApi(), s.CreatedAt);
public static DownloadStatus ToApi(this MessageDownloadStatus value) => value switch
{
MessageDownloadStatus.Queued => DownloadStatus.Queued,
MessageDownloadStatus.Running => DownloadStatus.Running,
MessageDownloadStatus.Completed => DownloadStatus.Completed,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
};
}
Enum mapping rules¶
- Use exhaustive
switchexpressions with a_ => throw new ArgumentOutOfRangeException(...)arm. - Never convert between enum types with numeric casts (
(Target)(int)source). - Nullable enums:
x?.ToApi(). - A missing arm is a compile error when warnings are treated as errors.
Unknownis handled by the_ => throwarm, so handlers must reject it earlier.
Serialization¶
API enums serialize as integers. Do not add a global JsonStringEnumConverter and do not put serialization attributes on domain message or persistence enums.