A set of utilities for returning standardized API results, supporting both controllers and minimal APIs.
$ dotnet add package BonyadCode.Resulter.AspNetCoreIn the name of God
A utility library to standardize API responses in ASP.NET Core. It supports both Controllers and Minimal APIs with consistent handling of success, failure, validation errors, and exceptions, using rich ProblemDetails following RFC7807.
ResultBuilder<T> and ResultBuilder for all HTTP outcomes.IActionResult or IResult.dotnet add package BonyadCode.Resulter.AspNetCore
[HttpGet("hello")]
public IActionResult GetHello()
{
var result = ResultBuilder<string>.Success("Hello, world!");
return result.ToHttpResultController();
}
Response JSON:
{
"succeeded": true,
"statusCode": 200,
"data": "Hello, world!",
"problemDetails": null
}
app.MapGet("/hello", () =>
{
var result = ResultBuilder<string>.Success("Hello, world!");
return result.ToHttpResultMinimal();
});
Response JSON:
{
"succeeded": true,
"statusCode": 200,
"data": "Hello, world!",
"problemDetails": null
}
[HttpGet("hello")]
public IActionResult GetHello()
{
var result = ResultBuilder.Success("User was Created", HttpStatusCode.Created);
return result.ToHttpResultController();
}
Response JSON:
{
"succeeded": true,
"statusCode": 201,
"data": "User was Created",
"problemDetails": null
}
app.MapGet("/hello", () =>
{
var result = ResultBuilder.Success("User was Created", HttpStatusCode.Created);
return result.ToHttpResultMinimal();
});
Response JSON:
{
"succeeded": true,
"statusCode": 201,
"data": "User was Created",
"problemDetails": null
}
[HttpPost("register")]
public IActionResult RegisterUser(UserDto dto)
{
var result = ResultBuilder<string>.Failure()
.AddProblemDetailsFromKeyValuePairs("Email", "Email is required.")
.AddProblemDetailsFromKeyValuePairs("Password", "Password must be at least 8 characters." );
return result.ToHttpResultController();
}
or
[HttpPost("register")]
public IActionResult RegisterUser(UserDto dto)
{
var result = ResultBuilder<string>.Failure()
.AddProblemDetailsFromKeyValuePairs(["Email","Password"], ["Email is required.","Password must be at least 8 characters." ]);
return result.ToHttpResultController();
}
Response JSON:
{
"succeeded": false,
"statusCode": 400,
"data": null,
"problemDetails": {
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "A problem occurred.",
"detail": "A problem occurred (400 BadRequest).",
"status": 400,
"extensions": {
"Email": [
"Email is required."
],
"Password": [
"Password must be at least 8 characters."
]
}
}
}
app.MapGet("/status", () =>
{
var result = ResultBuilder<string>.Success("All systems operational.", HttpStatusCode.NoContent);
return result.ToHttpResultMinimal();
});
or
app.MapGet("/status", () =>
{
var result = ResultBuilder<string>.Success("All systems operational.");
return result.ToHttpResultMinimal(HttpStatusCode.NoContent);
});
Response JSON:
{
"succeeded": true,
"statusCode": 204,
"data": "All systems operational.",
"problemDetails": null
}
app.MapPost("/login", (LoginRequest request, HttpContext? httpContext, IValidator<LoginRequest> validator) =>
{
var validationResult = validator.Validate(request);
if(!validationResult.Succeeded)
{
var result = ResultBuilder<string>.Failure()
.AddProblemDetailsFromFluentValidationResult(validationResult);
return result.ToHttpResultMinimal(HttpStatusCode.Conflict, httpContext);
}
return ResultBuilder<string>.Success("Logged in").ToHttpResultMinimal();
});
Response JSON:
{
"succeeded": false,
"statusCode": 409,
"data": null,
"problemDetails": {
"type": "https://example.com/problems/validation",
"title": "A validation problem occurred.",
"detail": "One or more validation failures occured.",
"status": 409,
"instance": "/api/register",
"extensions": {
"Username": [
"Username already exists."
]
}
}
}
var result = ResultBuilder<string>.Failure()
.AddCustomProblemDetails(
type: "https://example.com/problems/validation",
title: "A custom problem occurred.",
details: "One or more custom failures occured.",
statusCode: HttpStatusCode.Conflict,
instance: "/api/register",
extensions: new Dictionary<string, object?>
{
{ "Username", new[] { "Username already exists." } }
}).ToHttpResultMinimal(HttpStatusCode.Conflict, httpContext);
Note: You can pass the httpContext to the above method so that "instance" is extracted automatically from it.
Response JSON:
{
"succeeded": false,
"statusCode": 409,
"data": null,
"problemDetails": {
"type": "https://example.com/problems/validation",
"title": "A custom problem occurred.",
"detail": "One or more custom failures occured.",
"status": 409,
"instance": "/api/register",
"extensions": {
"Username": [
"Username already exists."
]
}
}
}
var validationResult = new ValidationResult("Email", new[] { "Invalid email" });
var result = ResultBuilder.Failure()
.AddProblemDetailsFromValidationResult(validationResult);
var result = ResultBuilder.Failure()
.AddProblemDetailsFromFluentValidationResult(validationResult);
var result = ResultBuilder.Failure()
.AddProblemDetailsFromIdentityError(identityResult);
try
{
throw new InvalidOperationException("Something broke.");
}
catch (Exception ex)
{
var result = ResultBuilder.Failure()
.AddProblemDetailsFromException(ex, httpContext);
return result.ToHttpResultController();
}
Response JSON (truncated):
{
"succeeded": false,
"statusCode": 500,
"data": null,
"problemDetails": {
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title": "A Server problem occurred.",
"detail": {
"message": "Something broke."
},
"status": 500,
"instance": "/api/failing-endpoint",
"extensions": {
"Message": [
"Something broke."
],
"Source": [
"MyApp"
]
}
}
}
Endpoint Result Methods:
| Method | Description |
|---|---|
ToHttpResultController() | returns an IActionResult result for use in Controllers |
ToHttpResultController(...) | returns an IActionResult result for use in Controllers (accepting parameters) |
ToHttpResultMinimal() | returns an IResult result for use in Minimal |
ToHttpResultMinimal(...) | returns an IResult result for use in Minimal APIs (accepting parameters) |
ProblemDetails Helper Methods:
| Method | Description |
|---|---|
AddSimpleProblemDetails() | Attaches default problem info |
AddCustomProblemDetails(...) | Fully customized metadata |
AddProblemDetailsFromException(...) | Adds public exception properties |
AddProblemDetailsFromIdentityError(...) | Maps IdentityResult errors |
AddProblemDetailsFromFluentValidationResult(...) | Maps FluentValidation errors |
AddProblemDetailsFromValidationResult(...) | Maps DataAnnotation errors |
AddProblemDetailsFromKeyValuePairs(...) | Adds custom errors by key/value(s) |
| Scenario | Method | Status Code | Description |
|---|---|---|---|
| Success | ResultBuilder.Success() | Any (Default: 200 OK) | Standard success with optional data |
| Failure | ResultBuilder.Failure() | Any (Default: 400 Bad Request) | Standard failure with optional data (exception, validation, etc.) |
| Create | ResultBuilder.Create() | Any | User-defined structured error response |
PRs and feedback welcome! GitHub →
Apache 2.0 — see the LICENSE file.
Usage Conditions: