Moes.Common.ApiResponse 1.3.1

dotnet add package Moes.Common.ApiResponse --version 1.3.1
                    
NuGet\Install-Package Moes.Common.ApiResponse -Version 1.3.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Moes.Common.ApiResponse" Version="1.3.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Moes.Common.ApiResponse" Version="1.3.1" />
                    
Directory.Packages.props
<PackageReference Include="Moes.Common.ApiResponse" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Moes.Common.ApiResponse --version 1.3.1
                    
#r "nuget: Moes.Common.ApiResponse, 1.3.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Moes.Common.ApiResponse@1.3.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Moes.Common.ApiResponse&version=1.3.1
                    
Install as a Cake Addin
#tool nuget:?package=Moes.Common.ApiResponse&version=1.3.1
                    
Install as a Cake Tool

Moes.Common.ApiResponse

NuGet Version NuGet Downloads

Unified API response envelope for microservices.

Moes.Common.ApiResponse provides a consistent JSON response envelope for ASP.NET Core microservices, so consumers of your APIs never have to guess whether a given service wraps errors differently than another. Unhandled exceptions are converted into that envelope automatically; for success responses, you opt in by returning ApiResponse<T> from your actions.

Features

  • A single response envelope (ApiResponse<T>) for success, failure, and paginated results.
  • Unhandled exceptions — domain exceptions, FluentValidation failures, ModelState failures, SQL Server exceptions, and cancelled requests — are converted into the envelope automatically, with stable, machine-readable error codes.
  • Automatic validation error responses in the unified envelope, replacing ASP.NET Core's built-in automatic 400 response.
  • Pagination metadata helpers.
  • Setup via two extension methods: one for DI, one for the middleware pipeline.

Installation

dotnet add package Moes.Common.ApiResponse

Requirements

  • net8.0 or net10.0
  • An ASP.NET Core host

This package brings in FluentValidation and Microsoft.Data.SqlClient as dependencies, used for automatic validation and SQL exception mapping respectively.

Getting Started

Register the services and add the middleware in Program.cs:

using Moes.Common.ApiResponse.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddUnifiedApiResponse(o =>
{
    o.ServiceName = "orders-service";
    o.IncludeExceptionDetailsInResponse = builder.Environment.IsDevelopment();
    o.UseCamelCase = true;
});

builder.Services.AddControllers();
// register your FluentValidation validators — see Validation below

var app = builder.Build();

app.UseUnifiedApiResponse(); // register as early as possible in the pipeline

app.MapControllers();
app.Run();

Then, in a controller, return the envelope instead of raw payloads and throw domain exceptions instead of building error responses by hand:

using Moes.Common.ApiResponse.Exceptions;
using Moes.Common.ApiResponse.Models;

[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(Guid id)
{
    var order = await _orders.FindAsync(id)
        ?? throw new NotFoundException(nameof(Order), id);

    return Ok(ApiResponse<OrderDto>.OK(order));
}

If order is null, throwing NotFoundException produces a 404 response in the unified shape automatically — no try/catch needed in the action. Note that this automatic handling only applies to errors: a plain Ok(someObject) is never wrapped for you, so use ApiResponse<T> explicitly whenever you want the envelope on a success response.

Configuration

AddUnifiedApiResponse accepts an optional Action<UnifiedApiResponseOptions> delegate:

Option Type Default Description
ServiceName string "unknown" Name of the current service, included in logs. Recommended to use snake_case.
IncludeExceptionDetailsInResponse bool false Whether stack traces / inner exception details / SQL diagnostics are included in the response body. Only enable in development.
UseCamelCase bool true JSON property naming policy used when error responses are serialized.

AddUnifiedApiResponse also disables ASP.NET Core's built-in automatic 400 response application-wide (ApiBehaviorOptions.SuppressModelStateInvalidFilter = true) so validation failures come back in the unified envelope instead — see Validation below.

UseUnifiedApiResponse wires up the automatic exception-to-response conversion described throughout this document. Call it as early as possible in the pipeline so it can catch exceptions from everything downstream.

The Response Envelope

Every response carries exactly one of a success payload or a list of errors — never both — plus optional metadata. Null members are omitted from the JSON output.

Success

{
  "data": { "id": "1a2b3c", "status": "Pending" }
}

Build it with ApiResponse<T>.OK(data), or ApiResponse<T>.OK(data, meta) to attach metadata. Use the non-generic ApiResponse.OK() for endpoints that return no payload.

Paginated success

{
  "data": [ { "id": "1a2b3c" }, { "id": "4d5e6f" } ],
  "meta": {
    "pagination": { "pageNumber": 1, "pageSize": 20, "totalItems": 42, "totalPages": 3, "hasPrevious": false, "hasNext": true }
  }
}

Build it with ApiResponse<T>.Paged(pagedResult) — see Pagination. Note this returns ApiResponse<IReadOnlyList<T>> (a page of T, not a single T).

Failure

{
  "errors": [
    { "message": "Order with id '1a2b3c' not found.", "code": "NOT_FOUND", "details": { "resourceType": "Order", "id": "1a2b3c" } }
  ]
}

Build it with ApiResponse<T>.Fail(errors), or simply throw one of the exceptions below and let it happen automatically. Each error is an ApiError:

Field Description
message Human-readable, developer-facing message.
code Stable, machine-readable code — see Error codes reference.
details Optional context (e.g. the offending field name for validation errors).

An error response also carries an X-Trace-Id header for correlating it with your logs. Successful responses do not include this header.

Validation

Register FluentValidation validators as usual. For each action argument, if a matching IValidator<T> is registered, it runs and any failures are converted automatically into the unified error envelope. ASP.NET Core's own ModelState errors (e.g. from data-annotation attributes) are checked too and converted the same way, so both validation styles end up in the same response shape. Either way, you never need to check ModelState.IsValid yourself — the built-in automatic 400 response is suppressed in favor of this unified shape, and you get one error per field/message pair.

using FluentValidation;

public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderRequestValidator()
    {
        RuleFor(x => x.CustomerId).NotEmpty();
        RuleFor(x => x.Quantity).GreaterThan(0);
    }
}

builder.Services.AddScoped<IValidator<CreateOrderRequest>, CreateOrderRequestValidator>();

Example resulting 400 response:

{
  "errors": [
    { "message": "'Customer Id' must not be empty.", "code": "VALIDATION_ERROR", "details": { "field": "customerId" } },
    { "message": "'Quantity' must be greater than '0'.", "code": "VALIDATION_ERROR", "details": { "field": "quantity" } }
  ]
}

Field names in details.field are camelCased, preserving dot notation for nested properties (e.g. address.postalCode).

Exceptions

Throw these from anywhere in the request pipeline — they are caught automatically and turned into the matching response, no try/catch required. All of them derive from the public abstract AppException, so you can also define your own domain exceptions by extending it directly with a custom status code and error code — they'll be handled the same way.

Exception HTTP status Error code Notes
ValidationException(field, message) 400 VALIDATION_ERROR Also accepts a dictionary of field → messages; produces one error per (field, message) pair.
UnauthorizedException(message?) 401 UNAUTHORIZED Request lacks valid authentication credentials.
ForbiddenException(message?) 403 FORBIDDEN Caller is authenticated but not permitted.
NotFoundException(resourceType, id?) 404 NOT_FOUND Message is "{resourceType} not found.", or "{resourceType} with id '{id}' not found." when id is supplied.
BusinessRuleException(message, errorCode?, details?) 409 BUSINESS_RULE_VIOLATION (or your own errorCode) A domain rule prevented the operation, e.g. "INSUFFICIENT_STOCK".
using Moes.Common.ApiResponse.Exceptions;

throw new NotFoundException(nameof(Order), id);
throw new ForbiddenException();
throw new BusinessRuleException("Cannot cancel a shipped order.", "ORDER_ALREADY_SHIPPED");
throw new Moes.Common.ApiResponse.Exceptions.ValidationException("email", "must be a valid email address.");

Note: ValidationException shares its short name with FluentValidation.ValidationException and System.ComponentModel.DataAnnotations.ValidationException. If your file also uses either of those — likely, since this library uses FluentValidation — qualify the type as shown above, or alias it (using ValidationException = Moes.Common.ApiResponse.Exceptions.ValidationException;) to avoid ambiguity.

Note: per AIP-193, if an unauthorized caller could otherwise infer that a resource exists from a 404 vs. 403 response, prefer throwing ForbiddenException regardless of whether the resource exists, rather than leaking existence via NotFoundException.

Also handled automatically

Beyond the exceptions above, the following are converted into the envelope without you throwing anything from this library:

Situation HTTP status Error code
A FluentValidation or ModelState failure (see Validation) 400 VALIDATION_ERROR
FluentValidation.ValidationException thrown directly (e.g. via ValidateAndThrow()) 400 VALIDATION_ERROR
A Microsoft.Data.SqlClient.SqlException (see SQL Server Exception Mapping) varies varies
A cancelled request (OperationCanceledException) 499 REQUEST_CANCELLED
Any other unhandled exception 500 INTERNAL_SERVER_ERROR

Error Codes Reference

ErrorCodes is a fixed set of stable, machine-readable codes shared across all microservices. Once a code is published it is a contract — never rename, remove, or repurpose an existing one.

Code Thrown by
VALIDATION_ERROR ValidationException, FluentValidation failures, ModelState failures
UNAUTHORIZED UnauthorizedException
FORBIDDEN ForbiddenException
NOT_FOUND NotFoundException
CONFLICT SQL unique/PK/FK/check constraint violations
BUSINESS_RULE_VIOLATION BusinessRuleException, SQL RAISERROR/THROW (error number ≥ 50000)
RATE_LIMITED Reserved — not thrown by this library; use it in your own rate-limiting code
EXTERNAL_SERVICE_ERROR Reserved — not thrown by this library; use it when a downstream service call fails
INTERNAL_SERVER_ERROR Any unhandled exception without a more specific mapping
DATABASE_DEADLOCK SQL deadlock victim
DATABASE_TIMEOUT SQL command timeout
DATABASE_UNAVAILABLE SQL login failure / cannot open database

The automatic 499 cancelled-request response (see Exceptions) uses the code REQUEST_CANCELLED, which is not part of ErrorCodes.

SQL Server Exception Mapping

SQL Server exceptions are mapped automatically to the following statuses and codes, so a raw database failure never leaks past your API as an unhandled 500 with schema details:

SQL error number HTTP status Error code
≥ 50000 (custom RAISERROR/THROW) 400 BUSINESS_RULE_VIOLATION
2627, 2601 (unique / primary key violation) 409 CONFLICT
547 (foreign key / check constraint) 409 CONFLICT
1205 (deadlock victim) 409 DATABASE_DEADLOCK
-2 (command timeout) 504 DATABASE_TIMEOUT
18456, 4060 (login failed / cannot open database) 503 DATABASE_UNAVAILABLE
anything else 500 INTERNAL_SERVER_ERROR

Detailed SQL diagnostics (error number, state, procedure, line number, message) are only included in the response details when IncludeExceptionDetailsInResponse is true.

Pagination

Build a page in your service/repository layer, then convert it to an envelope:

using Moes.Common.ApiResponse.Models;

var items = await _orders.GetPageAsync(page, pageSize);
var totalItems = await _orders.CountAsync();

var result = new PagedResult<OrderDto>(items, page, pageSize, totalItems);
return Ok(ApiResponse<OrderDto>.Paged(result));

The resulting pagination metadata:

Field Description
pageNumber Current page number (1-based).
pageSize Number of items requested per page.
totalItems Total number of items across all pages.
totalPages Computed total number of pages.
hasPrevious true when pageNumber > 1.
hasNext true when there are more pages after the current one.

Limitations

  • UseCamelCase scope: this option only controls the JSON serialization of the automatic error envelope described above. It does not configure your application's general MVC / System.Text.Json output settings — configure those separately if you need consistent casing for successful responses too.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.1 106 8/26/2026
1.3.0 114 7/29/2026
1.2.1 113 6/10/2026
1.2.0 115 6/10/2026
1.0.8 116 6/1/2026
1.0.7 114 5/23/2026
1.0.6 118 5/21/2026