Responses
Responses⌗
ResponseResolver is a static class that converts ArturRios.Output envelopes — DataOutput<T>,
ProcessOutput, and PaginatedOutput<T> — into ASP.NET Core ActionResults, so a controller action
never has to hand-pick a status code for the happy and unhappy paths itself.
Resolve overloads⌗
ResponseResolver.Resolve has three overloads, one per envelope type:
| Overload | Returns |
|---|---|
Resolve<T>(DataOutput<T?> dataOutput, int? statusCode = null) | ActionResult<DataOutput<T?>> |
Resolve(ProcessOutput processOutput, int? statusCode = null) | ActionResult<ProcessOutput> |
Resolve<T>(PaginatedOutput<T> paginatedOutput, int? statusCode = null) | ActionResult<PaginatedOutput<T>> |
Each wraps the envelope in an ObjectResult whose StatusCode is set from the resolved status code —
none of the three re-shape or otherwise touch the envelope itself; the body returned to the client is the
same object you passed in.
Status resolution order⌗
Every overload also accepts an optional statusMap — an
IReadOnlyDictionary<string, int> from an envelope message to an HTTP status code — and
resolves its status the same way:
statusCodesupplied — used as-is, regardless of the envelope’sSuccessvalue or the map.statusMapsupplied — the lookup key is the firstErrorsentry whenSuccessisfalse, or the firstMessagesentry whenSuccessistrue. If that key is present in the map, its value is used.- Otherwise — no map, an empty list, or a key not found — defaults to 200 when
Successistrueand 400 otherwise.
The caller owns the dictionary, so its key comparer controls matching — build it with
StringComparer.OrdinalIgnoreCase for case-insensitive keys.
flowchart LR
Output["DataOutput / ProcessOutput / PaginatedOutput"] --> Resolve["ResponseResolver.Resolve(output, statusCode?, statusMap?)"]
Resolve --> HasCode{"statusCode supplied?"}
HasCode -- "yes" --> UseCode["Use statusCode as-is"]
HasCode -- "no" --> HasMap{"statusMap supplied?"}
HasMap -- "yes" --> Lookup{"first error/message in map?"}
Lookup -- "yes" --> UseMapped["Use mapped status"]
Lookup -- "no" --> Success{"output.Success?"}
HasMap -- "no" --> Success
Success -- "true" --> Ok["200"]
Success -- "false" --> Bad["400"]
UseCode --> Result["ObjectResult"]
UseMapped --> Result
Ok --> Result
Bad --> Result
To map specific errors to specific statuses without branching in the action, pass a
statusMap keyed on the envelope’s first error:
var statusMap = new Dictionary<string, int>
{
["User not found"] = 404,
["Email already registered"] = 409,
};
return ResponseResolver.Resolve(output, statusMap: statusMap);
This means a failed operation that should still return, say, a 404 or 409 rather than a generic 400 just
needs an explicit statusCode:
return ResponseResolver.Resolve(output, statusCode: 404);
Pairing with the envelopes⌗
ResponseResolver is the last stop for the “envelopes, not exceptions” pattern the rest of the library
follows (see Architecture): application code builds a ProcessOutput or
DataOutput<T> (WithData, WithError, etc.) to describe what happened, and ResponseResolver is the
single place that decides how that maps onto the HTTP response — so success and failure both flow through
the same, predictable shape instead of being scattered across Ok(...)/BadRequest(...)/NotFound(...)
calls in every action. It pairs naturally with AddCustomInvalidModelStateResponse() (see
Configuration), which shapes ASP.NET Core’s own model-validation 400 as a
DataOutput<string> so it looks identical to a Resolved failure.
Controller-action example⌗
[HttpGet("{id:int}")]
public ActionResult<DataOutput<UserDto?>> GetById(int id)
{
DataOutput<UserDto?> output = _userService.GetById(id);
return ResponseResolver.Resolve(output);
}
_userService.GetById returns a DataOutput<UserDto?> whose Success reflects whether the user was
found; ResponseResolver.Resolve turns that straight into a 200 with the user payload or a 400 with the
service’s error messages, with no branching in the action itself.
Where to next⌗
- Architecture — where
ResponseResolversits at the end of the request pipeline, and the envelope class hierarchy (ProcessOutput→DataOutput<T>→PaginatedOutput<T>). - Configuration —
AddCustomInvalidModelStateResponse(), which shapes validation failures the same way. - Middleware & Diagnostics —
ExceptionMiddleware, which returns the sameDataOutput<string>shape for unhandled exceptions that never reach aResolvecall.