Metadata
AspNetConventions automatically enriches your API responses with contextual metadata and pagination information, providing better observability and a smoother client experience.
Response Metadata
Every response includes a metadata block that provides essential request context and observability information. This helps with debugging, logging, and correlating requests across distributed systems.
Metadata Fields
Metadata is a Dictionary<string, object?> — each standard field is stored under a well-known key constant defined on the Metadata class. Keys follow the configured JSON naming policy (e.g., camelCase, kebab-case) automatically.
| Field (JSON) | Const key | Source | Description |
|---|---|---|---|
requestType |
Metadata.RequestTypeKey |
HttpContext.Request.Method |
HTTP method (GET, POST, PUT, DELETE, etc.) |
path |
Metadata.PathKey |
HttpContext.Request.Path |
Request URL path (e.g., /api/users/123) |
timestamp |
Metadata.TimestampKey |
DateTime.UtcNow |
ISO 8601 UTC timestamp of response generation |
traceId |
Metadata.TraceIdKey |
Activity.Current?.Id or HttpContext.TraceIdentifier |
Unique identifier for end-to-end request tracing across services |
Example Metadata Block
{
"requestType": "POST",
"timestamp": "0000-00-00T00:00:00.000000Z",
"traceId": "00-ed89d1cc507c35126d6f0e933984f774-99b8b9a3feb75652-00",
"path": "/api/transactions"
}
Extending Metadata
Because Metadata is a plain dictionary, you can add, remove, or replace any entry before the response is built using the CustomizeMetadata hook:
options.Response.Hooks.CustomizeMetadata = (metadata, request) =>
{
// Add custom entries
metadata["userId"] = request.UserId;
metadata["tenantId"] = request.HttpContext.Items["TenantId"];
// Remove a standard entry
metadata.Remove(Metadata.PathKey);
// Replace a standard entry
metadata[Metadata.TraceIdKey] = MyCorrelationIdProvider.Get(request.HttpContext);
};
The hook receives the Metadata dictionary after the standard fields (requestType, path, timestamp, traceId) have been populated. Use the Metadata.*Key constants to safely reference standard fields without hard-coding strings.
See ResponseFormattingHooks for more information.
Configuration
You can disable metadata entirely:
options.Response.IncludeMetadata = false;
See ResponseFormattingOptions for more configuration options.
When Metadata is Omitted
When IncludeMetadata = false, the response will omit the entire metadata field:
{
"status": "success",
"statusCode": 200,
"data": { ... }
// No "metadata" field
}
See DefaultApiResponseBuilder and DefaultApiErrorResponseBuilder for more information about response formats.
Observability Benefits
The traceId is particularly valuable for:
Correlating logs across multiple services in a distributed system
Tracing request flow through message queues, databases, and external APIs
Debugging production issues by following a single request from entry to exit
Pagination Metadata
When returning paginated results using ApiResults.Paginate() or CollectionResult<T>, the response includes a pagination block with navigation links and page information.
PaginationMetadata
| Field | Source | Description |
|---|---|---|
pageNumber |
Math.Max(pageNumber, 1) |
Current page number (1-indexed, automatically normalized to at least 1) |
pageSize |
pageSize parameter |
Number of items per page |
totalPages |
Math.Ceiling(totalRecords / pageSize) |
Total number of pages available |
totalRecords |
totalRecords parameter |
Total count of items across all pages |
hasNextPage |
derived | true if a next page exists. Only included when IncludeNavigationFlags = true |
hasPreviousPage |
derived | true if a previous page exists. Only included when IncludeNavigationFlags = true |
links |
PaginationLinks object |
Navigation URLs for pagination traversal |
PaginationLinks
The links object contains URIs for navigating between pages:
| Field | Description |
|---|---|
firstPageUrl |
URI to the first page of results (always available when pagination is enabled) |
lastPageUrl |
URI to the last page of results (always available when pagination is enabled) |
nextPageUrl |
URI to the next page, or null if current page is the last page |
previousPageUrl |
URI to the previous page, or null if current page is the first page |
CollectionResult
The CollectionResult<T> is a wrapper class that combines a collection of items with pagination metadata. It’s used internally by ApiResults.Paginate() to structure paginated responses.
| Property | Type | Description |
|---|---|---|
Items |
IEnumerable<T> |
The collection of items for the current page |
TotalRecords |
int |
Total number of records available across all pages |
PageNumber |
int |
Current page number (1-indexed) |
PageSize |
int |
Number of items per page |
When to use: Typically you won’t need to create CollectionResult<T> manually—ApiResults.Paginate() handles this for you. Use it directly when you need more control over the pagination structure or when integrating with existing pagination logic. See Using Pagination in Your Endpoints.
Example Pagination Block
{
"pageNumber": 1,
"pageSize": 25,
"totalPages": 20,
"totalRecords": 500,
"links": {
"firstPageUrl": "/api/user/orders?page-number=1&page-size=25",
"lastPageUrl": "/api/user/orders?page-number=3&page-size=25",
"nextPageUrl": "/api/user/orders?page-number=2&page-size=25",
"previousPageUrl": null
}
}
With IncludeNavigationFlags = true:
{
"pageNumber": 1,
"pageSize": 25,
"totalPages": 20,
"totalRecords": 500,
"hasNextPage": true,
"hasPreviousPage": false,
"links": { ... }
}
Configuration
Customize pagination behavior in your configuration:
options.Response.Pagination.IncludeMetadata = true;
options.Response.Pagination.IncludeLinks = false;
options.Response.Pagination.IncludeNavigationFlags = true;
options.Response.Pagination.PageNumberParameterName = "p";
options.Response.Pagination.PageSizeParameterName = "limit";
See PaginationOptions for more information.
When Pagination Metadata is Omitted
When Pagination.IncludeMetadata = false, the response will omit the entire pagination field:
{
"status": "success",
"statusCode": 200,
"data": { ... },
"metadata": { ... },
// No "pagination" field
}
When Pagination Links are Omitted
When Pagination.IncludeLinks = false, the response will omit the entire pagination.links fields:
{
"status": "success",
"statusCode": 200,
"data": { ... },
"metadata": { ... },
"pagination": {
"pageNumber": 1,
"pageSize": 25,
"totalPages": 20,
"totalRecords": 500,
// No "links" field
}
}
Using Pagination in Your Endpoints
Return paginated results from your controllers or minimal APIs.
When your endpoint returns a ApiResults.Paginate() or CollectionResult<T>, AspNetConventions automatically adds pagination metadata:
Example:
// using CollectionResult<T> (Manually)
var result = new CollectionResult<Product>(
items: items,
totalRecords: 100,
pageNumber: 1,
pageSize: 10);
return Ok(result);
// or using ApiResults.Paginate() (Recommended)
// Basic pagination
return ApiResults.Paginate(items, totalRecords, pageNumber, pageSize);
// With custom status code
return ApiResults.Paginate(items, totalRecords, pageNumber, pageSize, HttpStatusCode.OK);
// With custom message
return ApiResults.Paginate(items, totalRecords, pageNumber, pageSize, "Records retrieved successfully");
How Link Generation Works
The library automatically builds navigation links by:
Preserving all existing query parameters (filters, sorting, search terms, etc.)
Updating only the
pageNumberandpageSizeparametersGenerating complete, absolute URLs based on the current request
Example with preserved filters:
Request URL:
GET /api/transactions?category=electronics&sort=desc&page-number=2&page-size=20
Generated nextPageUrl:
GET /api/transactions?category=electronics&sort=desc&page-number=3&page-size=20
Summary
| Feature | Purpose | Key Fields |
|---|---|---|
| Response Metadata | Request observability and tracing | traceId, timestamp, requestType, path |
| Pagination Metadata | Navigation through large result sets | pageNumber, totalPages, links |
Both features work together to create APIs that are:
Observable — Every response carries tracing information
Discoverable — Clients can navigate pages without URL construction
Consistent — Same structure across all endpoints