| .github/workflows | ||
| src | ||
| utils | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE.md | ||
| README.md | ||
MaksIT.Results
HTTP-aware Result / Result<T> for .NET - model success and failure once, then map to ASP.NET Core IActionResult or parse the same wire shape from an HttpResponseMessage.
Target: .NET 10 (net10.0)
Why
Most Result libraries give you success/failure (and sometimes a typed error), then leave HTTP as a second mapping in the controller: domain NotFound -> 404, Conflict -> 409, and so on. That remapping is where APIs drift.
MaksIT.Results uses HTTP status codes as the outcome vocabulary. One type carries success/failure, messages, and a shared classification (NotFound, Conflict, TooManyRequests, ...). The service already decided the status; the controller calls ToActionResult().
That is not "leaking HTTP into the domain." Status codes are a mature taxonomy of outcomes that apply beyond the wire - including desktop, workers, and CLI:
| Status idea | Same idea outside the browser |
|---|---|
| 404 | entity / file / setting not found |
| 409 | concurrent edit / state conflict |
| 401 / 403 | not signed in / not allowed |
| 422 | validation failed |
| 429 | rate limit / throttle |
| 503 | dependency down / retry later |
| 202 | accepted, still processing |
Compared to typical Result libs
| Typical Result lib | MaksIT.Results | |
|---|---|---|
| Outcome model | Success / Failure (+ custom errors) | Success / Failure + HttpStatusCode |
| Controller mapping | Invent codes, map again to HTTP | Status already set; ToActionResult() |
| Failure body | Often ad hoc | RFC 7807 ProblemDetails |
| Client parse | Rare / out of scope | FromHttpResponseAsync / FromActionResult |
| Status factories | Usually Ok / Fail (+ a few) |
Named 1xx-5xx factories |
Bottom line: most Result packages stop at "return from a method." This one is a service <-> API <-> client result model - one contract from domain to wire and back.
Install
dotnet add package MaksIT.Results
Install-Package MaksIT.Results
Quick start
Create results
using MaksIT.Results;
Result ok = Result.Ok("Operation completed");
Result failed = Result.BadRequest("Validation failed");
Result<int> answer = Result<int>.Ok(42, "Answer generated");
Result<UserDto?> missing = Result<UserDto?>.NotFound(null, "Entity not found");
Map between result shapes
Result<int> source = Result<int>.Ok(42, "Value loaded");
Result<string?> asString = source.ToResultOfType(v => v?.ToString());
Result nonGeneric = source.ToResult();
Controllers
using MaksIT.Results;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("users")]
public sealed class UsersController : ControllerBase {
[HttpGet("{id:guid}")]
public IActionResult Get(Guid id) {
Result<UserDto?> result = id == Guid.Empty
? Result<UserDto?>.BadRequest(null, "Invalid id")
: Result<UserDto?>.Ok(new UserDto(id, "maks"), "User loaded");
return result.ToActionResult();
}
}
public sealed record UserDto(Guid Id, string Name);
HTTP clients
Parse responses produced in the same contract (JSON body, redirect Location, or problem+json):
using HttpResponseMessage response = await http.GetAsync(url);
Result bare = await Result.FromHttpResponseAsync(response);
Result<UserDto> typed = await Result<UserDto>.FromHttpResponseAsync(response);
Or reverse an IActionResult you already built:
IActionResult action = result.ToActionResult();
Result parsed = Result.FromActionResult(action);
Result<UserDto> typed = Result<UserDto>.FromActionResult(action);
ToActionResult() contract
| Outcome | Response |
|---|---|
Result success |
Status code only |
Result<T> success, non-null Value |
JSON body + status |
Result<T> success, redirect status (300-303, 307-308) and non-empty string Value |
Redirect; status preserved on RedirectResult |
| Any failure | ProblemDetails: status, title = "An error occurred", detail = joined Messages, content type application/problem+json |
FromActionResult / FromHttpResponseAsync
| Wire shape | Result |
|---|---|
| Success, no body | Success Result / empty Result<T> |
| Success JSON body | Deserialized into Result<T>.Value |
Redirect + Location |
Result<string> with that URL and redirect status |
application/problem+json (or JSON with detail / title) |
Failure; messages from problem details |
JSON options
ObjectResult uses the app's JsonSerializerOptions when configured via AddJsonOptions:
builder.Services.AddControllers()
.AddJsonOptions(options => {
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
If none are registered, responses fall back to camel case.
Status code factories
| Family | Examples |
|---|---|
| Informational | Continue, SwitchingProtocols, Processing, ... |
| Success | Ok, Created, NoContent, ... |
| Redirection | Found, MovedPermanently, PermanentRedirect, ... |
| Client error | BadRequest, NotFound, Conflict, TooManyRequests, ... |
| Server error | InternalServerError, ServiceUnavailable, ... |
Use Result.* or Result<T>.* (e.g. Result<MyDto>.Ok(value, "message")).
Contributing
See CONTRIBUTING.md. Changelog: CHANGELOG.md.
License
MIT - Maksym Sadovnychyy (MAKS-IT)
Contact: maksym.sadovnychyy@gmail.com