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:

OverloadReturns
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:

  1. statusCode supplied — used as-is, regardless of the envelope’s Success value or the map.
  2. statusMap supplied — the lookup key is the first Errors entry when Success is false, or the first Messages entry when Success is true. If that key is present in the map, its value is used.
  3. Otherwise — no map, an empty list, or a key not found — defaults to 200 when Success is true and 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 ResponseResolver sits at the end of the request pipeline, and the envelope class hierarchy (ProcessOutputDataOutput<T>PaginatedOutput<T>).
  • ConfigurationAddCustomInvalidModelStateResponse(), which shapes validation failures the same way.
  • Middleware & DiagnosticsExceptionMiddleware, which returns the same DataOutput<string> shape for unhandled exceptions that never reach a Resolve call.