API Conventions Guide
Overview
The BookStore API follows strict standards for time handling and JSON serialization to ensure consistency, interoperability, and maintainability.
Time Standards
Always Use UTC
Rule: All timestamps MUST use UTC timezone.
✅ Correct:
var timestamp = DateTimeOffset.UtcNow;
var eventTime = DateTimeOffset.UtcNow;
❌ Incorrect:
var timestamp = DateTime.Now; // Local timezone - NEVER use
var eventTime = DateTimeOffset.Now; // Local timezone - NEVER use
Always Use DateTimeOffset
Rule: Use DateTimeOffset instead of DateTime for all timestamps.
✅ Correct:
public DateTimeOffset Timestamp { get; set; }
public DateTimeOffset LastModified { get; set; }
❌ Incorrect:
public DateTime Timestamp { get; set; } // No timezone info - NEVER use
ISO 8601 Format
All date/time values are automatically serialized in ISO 8601 format:
{
"timestamp": "2025-12-26T17:16:09.123Z",
"lastModified": "2025-12-26T17:16:09Z",
"publicationDate": "2008-08-01"
}
Format Details:
DateTimeOffset:YYYY-MM-DDTHH:mm:ss.fffZ(with milliseconds)DateOnly:YYYY-MM-DD- Timezone: Always
Z(UTC)
JSON Serialization Standards
camelCase
Rule: All JSON properties use camelCase.
{
"bookId": "018d5e4a-7b2c-7000-8000-123456789abc",
"title": "Clean Code",
"publicationDate": "2008-08-01",
"lastModified": "2025-12-26T17:16:09Z"
}
Enums: String Serialization
Rule: Enums are serialized as strings, not integers.
✅ Correct (String):
{
"status": "Active",
"role": "Administrator",
"orderStatus": "Shipped"
}
❌ Incorrect (Integer):
{
"status": 0,
"role": 1,
"orderStatus": 2
}
Benefits:
- Readable:
"Active"is clearer than0 - Evolvable: Can reorder enum values without breaking API
- Self-documenting: No need to look up enum definitions
- Debuggable: Easier to understand logs and database queries
Configuration
ASP.NET Core (API Responses)
Configured in Program.cs:
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.Converters.Add(
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
});
Marten (Database Storage)
Configured in Program.cs:
options.UseDefaultSerialization(
enumStorage: EnumStorage.AsString,
casing: Casing.CamelCase);
Identifier Standards
UUIDv7 for All IDs
Rule: Use Guid.CreateVersion7() for all entity identifiers.
✅ Correct:
public record CreateBook(...)
{
public Guid Id { get; init; } = Guid.CreateVersion7();
}
var bookId = Guid.CreateVersion7();
❌ Incorrect:
var bookId = Guid.NewGuid(); // WRONG - creates random UUIDv4
Benefits of UUIDv7:
- ✅ Time-ordered: Naturally sortable by creation time
- ✅ Database performance: Better index locality and reduced fragmentation
- ✅ Distributed systems: Safe to generate across multiple servers
- ✅ No collisions: Globally unique without coordination
Format:
018d5e4a-7b2c-7000-8000-123456789abc
└─────┬─────┘ └┬┘ └┬┘ └──────┬──────┘
Timestamp Ver Var Random
Note
The BookStore project includes a Roslyn analyzer (BS1006) that enforces this convention by flagging any use of Guid.NewGuid().
Type Conventions
Use Record Types
Rule: Use record types for immutable data structures (DTOs, commands, events).
✅ Correct:
// DTOs
public record BookDto(
Guid Id,
string Title,
string? Isbn,
PublisherDto? Publisher);
// Commands
public record CreateBook(
string Title,
string? Isbn,
List<Guid> AuthorIds);
// Events
public record BookAdded(
Guid Id,
string Title,
DateTimeOffset Timestamp);
❌ Incorrect:
// WRONG - using class for immutable data
public class BookDto
{
public Guid Id { get; set; }
public string Title { get; set; }
}
Benefits:
- ✅ Immutability: Value-based equality by default
- ✅ Concise: Less boilerplate code
- ✅ Thread-safe: Immutable objects are inherently thread-safe
- ✅ Event sourcing: Perfect for immutable events
Nullable Reference Types
Rule: Enable nullable reference types and use ? for optional values.
✅ Correct:
public record BookDto(
Guid Id,
string Title, // Required
string? Isbn, // Optional
string? Description, // Optional
PublisherDto? Publisher // Optional
);
❌ Incorrect:
public record BookDto(
Guid Id,
string Title,
string Isbn, // WRONG - should be string? if optional
string Description // WRONG - should be string? if optional
);
Configuration:
<!-- In .csproj -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Pagination Standards
PagedListDto Structure
Rule: Use consistent pagination response format.
public record PagedListDto<T>(
IReadOnlyList<T> Items,
long PageNumber,
long PageSize,
long TotalItemCount)
{
public long PageCount { get; init; } =
(long)double.Ceiling(TotalItemCount / (double)PageSize);
public bool HasPreviousPage { get; init; } = PageNumber > 1;
public bool HasNextPage { get; init; } =
PageNumber < (long)double.Ceiling(TotalItemCount / (double)PageSize);
}
JSON Response:
{
"items": [
{ "id": "...", "title": "Clean Code" },
{ "id": "...", "title": "Design Patterns" }
],
"pageNumber": 1,
"pageSize": 20,
"totalItemCount": 42,
"pageCount": 3,
"hasPreviousPage": false,
"hasNextPage": true
}
Pagination Query Parameters
Rule: Use page and pageSize query parameters.
GET /api/books?page=2&pageSize=20&search=architecture
Default Values:
page: 1pageSize: 20 (configurable viaPaginationOptions)
HTTP Header Standards
Standard Headers
The API uses the following standard headers:
Request Headers
| Header | Required | Description | Example |
|---|---|---|---|
Accept-Language |
No | Preferred language for localized content | pt-PT, en-US |
If-None-Match |
No | ETag for conditional requests (caching) | "5" |
If-Match |
No | ETag for optimistic concurrency control | "5" |
Response Headers
| Header | Description | Example |
|---|---|---|
ETag |
Entity version for caching and concurrency | "5" |
Cache-Control |
Caching directives | private, max-age=60 |
Custom Headers
Correlation and Causation IDs
Rule: Use X-Correlation-ID and X-Causation-ID for distributed tracing.
POST /api/admin/books
X-Correlation-ID: workflow-123
X-Causation-ID: user-action-456
Content-Type: application/json
{
"title": "Clean Code",
...
}
Behavior:
- If
X-Correlation-IDis not provided, the API generates a new UUIDv7 - The API echoes back the correlation ID in the response
- All events in the event store are tagged with correlation and causation IDs
Benefits:
- ✅ Traceability: Track requests across services
- ✅ Debugging: Correlate logs and events
- ✅ Auditing: Understand cause-and-effect relationships
API Versioning
Header-Based Versioning
Rule: Use api-version header for API versioning.
GET /api/books
api-version: 1.0
Current Version: 1.0
Benefits:
- ✅ Clean URLs: No version in the path
- ✅ Flexible: Can version individual endpoints
- ✅ Backward compatible: Defaults to latest version
Localization Standards
Supported Languages
The API supports the following languages:
| Code | Language |
|---|---|
en |
English (default) |
pt |
Portuguese |
es |
Spanish |
fr |
French |
de |
German |
Accept-Language Header
Rule: Use Accept-Language header for localized content.
GET /api/categories
Accept-Language: pt-PT
Response:
{
"items": [
{ "id": "...", "name": "Ficção" },
{ "id": "...", "name": "Mistério" }
]
}
Fallback Strategy
- Try requested language (e.g.,
pt) - Fall back to default language (
en) - Fall back to first available translation
Error Response Standards
Problem Details (RFC 7807)
Rule: All error responses use the Problem Details format (RFC 7807).
The API is configured with AddProblemDetails() which automatically converts error responses to the standard format.
Standard Error Response Format
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
}
Validation Error Response
For validation errors, the API returns detailed error information:
{
"error": "Invalid language code",
"languageCode": "xx",
"message": "The language code 'xx' is not valid. Must be a valid ISO 639-1 (e.g., 'en'), ISO 639-3 (e.g., 'fil'), or culture code (e.g., 'en-US')"
}
Common Error Responses
| Status Code | Description | Example |
|---|---|---|
400 Bad Request |
Invalid input or validation failure | Invalid language code, description too long |
404 Not Found |
Resource not found | Book ID doesn't exist |
412 Precondition Failed |
ETag mismatch (optimistic concurrency) | Concurrent update detected |
422 Unprocessable Entity |
Business rule violation | Cannot delete book with active orders |
Error Response Examples
Validation Error:
POST /api/admin/books
Content-Type: application/json
{
"title": "Clean Code",
"language": "invalid",
...
}
Response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "Invalid language code",
"languageCode": "invalid",
"message": "The language code 'invalid' is not valid. Must be a valid ISO 639-1 (e.g., 'en'), ISO 639-3 (e.g., 'fil'), or culture code (e.g., 'en-US')"
}
Length Validation Error:
POST /api/admin/books
Content-Type: application/json
{
"title": "Clean Code",
"translations": {
"en": {
"description": "A very long description that exceeds the maximum allowed length..."
}
},
...
}
Response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "Description too long",
"languageCode": "en",
"maxLength": 5000,
"actualLength": 6543,
"message": "Description for language 'en' cannot exceed 5000 characters"
}
Not Found Error:
GET /api/books/00000000-0000-0000-0000-000000000000
Response:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
}
Optimistic Concurrency Error:
PUT /api/admin/books/018d5e4a-7b2c-7000-8000-123456789abc
If-Match: "5"
Content-Type: application/json
{
"title": "Updated Title",
...
}
Response (if ETag doesn't match):
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
{
"type": "https://tools.ietf.org/html/rfc7232#section-4.2",
"title": "Precondition Failed",
"status": 412,
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
}
Error Response Best Practices
- Always include descriptive messages - Help clients understand what went wrong
- Include relevant context - Add fields like
languageCode,maxLength, etc. - Use appropriate status codes - Follow HTTP semantics
- Include trace IDs - For debugging and correlation
- Be consistent - Use the same error format across all endpoints
Common Patterns
Creating Events
Always use DateTimeOffset.UtcNow for event timestamps:
public static BookAdded Create(Guid id, string title, ...)
{
return new BookAdded(
id,
title,
...,
DateTimeOffset.UtcNow // ✅ Always UTC
);
}
Storing Timestamps
Use DateTimeOffset for all timestamp properties:
public record BookAdded(
Guid Id,
string Title,
DateTimeOffset Timestamp // ✅ DateTimeOffset with UTC
);
Querying by Time
Use DateTimeOffset.UtcNow for time-based queries:
var recentBooks = await session.Query<BookSearchProjection>()
.Where(b => b.LastModified > DateTimeOffset.UtcNow.AddDays(-7))
.ToListAsync();
Benefits
UTC Timezone
- ✅ No timezone conversion errors
- ✅ Consistent across all servers
- ✅ Works globally without confusion
- ✅ Simplifies distributed systems
ISO 8601 Format
- ✅ Universal standard (RFC 3339)
- ✅ Sortable as strings
- ✅ Human-readable
- ✅ Supported by all platforms
UUIDv7
- ✅ Time-ordered for better performance
- ✅ Database index optimization
- ✅ Natural sorting by creation time
- ✅ Distributed generation without conflicts
Record Types
- ✅ Immutability by default
- ✅ Value-based equality
- ✅ Less boilerplate code
- ✅ Perfect for DTOs and events
Enum Strings
- ✅ Self-documenting APIs
- ✅ Safe enum reordering
- ✅ Easier debugging
- ✅ Better database queries
camelCase
- ✅ JavaScript/TypeScript convention
- ✅ Consistent with web standards
- ✅ Better readability in JSON
Common Pitfalls
❌ Using DateTime.Now
var timestamp = DateTime.Now; // WRONG - uses local timezone
❌ Using DateTime instead of DateTimeOffset
public DateTime Timestamp { get; set; } // WRONG - no timezone info
❌ Using Guid.NewGuid()
var id = Guid.NewGuid(); // WRONG - creates random UUIDv4
❌ Using classes for DTOs
public class BookDto // WRONG - should use record
{
public Guid Id { get; set; }
}
❌ Manual date formatting
var dateStr = date.ToString("yyyy-MM-dd"); // WRONG - use serialization
❌ Integer enum serialization
// WRONG - will serialize as integer without configuration
public enum Status { Active, Inactive }
❌ Not using nullable reference types
public record BookDto(
string Isbn // WRONG - should be string? if optional
);
Validation
Unit Tests
Test JSON serialization format:
[Fact]
public void DateTimeOffset_Should_Serialize_As_ISO8601()
{
var obj = new { timestamp = DateTimeOffset.UtcNow };
var json = JsonSerializer.Serialize(obj);
// Should match ISO 8601 format: "2025-12-26T17:16:09.123Z"
Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z", json);
}
[Fact]
public void Enum_Should_Serialize_As_String()
{
var obj = new { status = Status.Active };
var json = JsonSerializer.Serialize(obj);
Assert.Contains("\"Active\"", json);
Assert.DoesNotContain("0", json);
}
[Fact]
public void Guid_Should_Be_Version7()
{
var id = Guid.CreateVersion7();
var bytes = id.ToByteArray();
// Version 7 has version bits set to 0111
Assert.Equal(7, (bytes[7] & 0xF0) >> 4);
}
Summary
Golden Rules:
- ✅ Always use
DateTimeOffset.UtcNow(neverDateTime.Now) - ✅ Always use
DateTimeOffsettype (neverDateTime) - ✅ Always use
Guid.CreateVersion7()(neverGuid.NewGuid()) - ✅ Always use
recordtypes for DTOs, commands, and events - ✅ ISO 8601 format is automatic (don't format manually)
- ✅ Enums serialize as strings (configured globally)
- ✅ JSON properties use camelCase (configured globally)
- ✅ Use nullable reference types (
string?for optional values) - ✅ Use
X-Correlation-IDandX-Causation-IDfor tracing - ✅ Use
Accept-Languageheader for localization - ✅ Use Problem Details (RFC 7807) for error responses
These standards ensure the API is:
- Consistent: Same format everywhere
- Interoperable: Works with all clients
- Maintainable: Easy to understand and debug
- Scalable: Works across timezones and regions
- Performant: Optimized for databases and distributed systems