A lightweight, zero-dependency library for ASP.NET Core (Minimal APIs & MVC) that wraps every response
in a consistent ApiResponse<T> envelope — success, error, pagination, correlation ID, and timestamp included.
| Feature | Description |
|---|---|
| Consistent envelope shape | Every response carries success, data, error, meta, requestId, timestamp |
| Minimal API helpers | Envelope.Ok / Created / Accepted / NotFound / BadRequest / ... — full HTTP vocabulary |
| MVC action filter | AddEnvelopeFilter() wraps controller ObjectResult responses automatically |
| Global exception middleware | UseApiEnvelope() catches unhandled exceptions and writes a safe envelope |
| Exception → status mapping | Map NotFoundException → 404, ValidationException → 422 in options — no try/catch in handlers |
[SkipEnvelope] opt-out |
Exclude file-download or health-check endpoints from envelope handling |
| Correlation ID propagation | Reads X-Correlation-ID from request, echoes it in every response |
OnBeforeWriteError hook |
Attach OpenTelemetry traceId, tenant ID, or any custom field before an error is written |
ToResult() extensions |
Bridge ApiResponse<T> from your domain layer to IResult without an HTTP dependency |
| Configurable JSON options | Plug in your own JsonSerializerOptions; defaults to camelCase, compact, nulls omitted |
| Stable JSON field order | [JsonPropertyOrder] guarantees success → data → error → meta → requestId → timestamp |
| Pagination support | PaginationMeta with Page, PageSize, Total, HasMore |
| Multi-target | Ships net9.0 and net10.0 assemblies in one package |
dotnet add package Slck.EnvelopeThis diagram shows how Slck.Envelope keeps both success and failure responses on the same contract.
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#ffe082',
'primaryTextColor': '#1f2937',
'primaryBorderColor': '#f59e0b',
'lineColor': '#f8fafc',
'secondaryColor': '#bfdbfe',
'tertiaryColor': '#fecdd3',
'background': '#fffdf7'
}}}%%
flowchart LR
Client["Client App"] --> Endpoint["Minimal API or MVC Endpoint"]
Endpoint --> Helpers["Envelope Helpers<br/>Ok / Created / Accepted"]
Endpoint --> Skip["[SkipEnvelope]"]
Endpoint --> Exceptions["Unhandled Exception"]
Helpers --> Response["ApiResponse<T> JSON"]
Exceptions --> Middleware["UseApiEnvelope Middleware"]
Middleware --> Response
Response --> Meta["requestId + timestamp + meta"]
classDef warm fill:#ffe082,stroke:#f59e0b,color:#1f2937,stroke-width:2px;
classDef cool fill:#bfdbfe,stroke:#2563eb,color:#0f172a,stroke-width:2px;
classDef rose fill:#fecdd3,stroke:#e11d48,color:#111827,stroke-width:2px;
classDef mint fill:#bbf7d0,stroke:#16a34a,color:#14532d,stroke-width:2px;
linkStyle default stroke:#f8fafc,stroke-width:3px;
class Client,Endpoint,Response warm;
class Helpers,Meta cool;
class Middleware,Exceptions rose;
class Skip mint;
builder.Services.AddApiEnvelope(opts =>
{
opts.IncludeExceptionDetails = builder.Environment.IsDevelopment();
opts.CorrelationIdHeader = "X-Correlation-ID"; // default
// Map domain exceptions to HTTP status codes — no try/catch in handlers
opts.MapException<KeyNotFoundException>(404, "not_found")
.MapException<UnauthorizedAccessException>(401, "unauthorized");
// Attach OpenTelemetry trace ID to every error envelope
opts.OnBeforeWriteError = (ex, response) =>
response with { RequestId = Activity.Current?.TraceId.ToString() ?? response.RequestId };
});app.UseApiEnvelope(); // global unhandled-exception handler
// For MVC controllers:
builder.Services.AddControllers().AddEnvelopeFilter();app.MapGet("/ticket/{id}", (string id) =>
{
var ticket = db.Find(id);
return ticket is null
? Envelope.NotFound($"Ticket '{id}' not found")
: Envelope.Ok(ticket);
});
app.MapGet("/tickets", (int page = 1, int pageSize = 10) =>
{
var meta = new PaginationMeta { Page = page, PageSize = pageSize, Total = total };
return Envelope.Ok(paged, meta);
});
app.MapPost("/ticket", (Ticket t) => Envelope.Created($"/ticket/{t.Id}", t));
// 202 Accepted — async job pattern
app.MapPost("/jobs", (Job j) => Envelope.Accepted(new { j.Id, Status = "queued" }, $"/jobs/{j.Id}/status"));{
"success": true,
"data": { "id": "123", "title": "Sample Ticket" },
"meta": { "page": 1, "pageSize": 10, "total": 3, "hasMore": false },
"requestId": "0HNK...",
"timestamp": "2026-04-16T08:00:00+00:00"
}{
"success": false,
"error": { "code": "not_found", "message": "Ticket '999' not found" },
"requestId": "0HNK...",
"timestamp": "2026-04-16T08:00:01+00:00"
}{
"success": false,
"error": {
"code": "validation_error",
"message": "Validation failed",
"details": {
"id": ["Id is required."],
"title": ["Title is required."]
}
},
"requestId": "0HNK...",
"timestamp": "2026-04-16T08:00:02+00:00"
}| Method | Status |
|---|---|
Envelope.Ok(data, meta?) |
200 |
Envelope.Created(location, data) |
201 + Location header |
Envelope.Accepted(data, statusUrl?) |
202 + Location header |
Envelope.NoContent() |
204 |
Envelope.BadRequest(message, details?) |
400 |
Envelope.Unauthorized(message) |
401 |
Envelope.Forbidden(message) |
403 |
Envelope.NotFound(message) |
404 |
Envelope.Conflict(message) |
409 |
Envelope.UnprocessableEntity(errors, message) |
422 |
Envelope.TooManyRequests(message) |
429 |
Envelope.Error(message, code) |
500 |
Envelope.ServiceUnavailable(message) |
503 |
Envelope.From(response, statusCode) |
any (escape hatch) |
// Minimal API
app.MapGet("/report/download", ...).WithMetadata(new SkipEnvelopeAttribute());
// MVC
[SkipEnvelope]
public class HealthController : ControllerBase { ... }EnvelopeFactory.Ok(data).ToOkResult();
EnvelopeFactory.Fail<T>("not_found", "...").ToNotFoundResult();
EnvelopeFactory.Ok(job).ToAcceptedResult("/jobs/1/status");
response.ToResult(207); // any status code| Property | Default | Description |
|---|---|---|
IncludeExceptionDetails |
false |
Include ex.Message in 500 responses (dev only) |
CorrelationIdHeader |
"X-Correlation-ID" |
Request/response correlation header |
JsonSerializerOptions |
null |
Override JSON serialization globally |
ExceptionMap |
{} |
Dictionary of Type → ExceptionMapping |
OnBeforeWriteError |
null |
Hook to enrich error envelopes before write |
src/Slck.Envelope/ — library source
samples/sample.api/ — runnable sample demonstrating all features
CHANGELOG.md — full version history
MIT — see LICENSE.
Slck.Envelope is a lightweight, opinionated library for ASP.NET Core minimal APIs that standardizes API responses into a consistent envelope format. It provides a structured way to handle success responses, errors, and metadata across your entire API.
src/Slck.Envelope- package sourcesamples/sample.api- active sample API in the solutiontests- place future test projects here
-
Consistent Response Shape: Every endpoint returns
ApiResponse<T>with standardized properties:success: Boolean indicating request statusdata: The response payload (null on error)error: Error details (null on success)meta: Optional metadata (pagination, timestamps, etc.)
-
Minimal API Integration: Seamless integration with ASP.NET Core minimal APIs using
IResultimplementations:Envelope.Ok(data, message): 200 OK responsesEnvelope.Created(data, uri): 201 Created responsesEnvelope.NotFound(message): 404 Not Found responsesEnvelope.BadRequest(message, errors): 400 Bad Request with validation errorsEnvelope.Unauthorized(message): 401 Unauthorized responsesEnvelope.InternalServerError(message): 500 Internal Server Error
-
Exception Handling Middleware: Automatic wrapping of unhandled exceptions into standardized error envelopes.
-
Pagination Support: Built-in
PaginationMetafor paginated list endpoints with:- Page number
- Page size
- Total items
- Total pages
- Navigation links
-
Developer Experience:
- CamelCase JSON serialization
- Ignore null properties
- Extension methods for common scenarios
- Type-safe response factories
Install via NuGet Package Manager:
dotnet add package Slck.Envelope