mirror of
https://github.com/MAKS-IT-COM/maksit-results.git
synced 2026-11-13 21:06:10 +01:00
171 lines
5.9 KiB
Markdown
171 lines
5.9 KiB
Markdown
# MaksIT.Results
|
|
|
|
[](https://www.nuget.org/packages/MaksIT.Results)
|
|
[](LICENSE.md)
|
|
|
|

|
|

|
|

|
|
|
|
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
|
|
|
|
```bash
|
|
dotnet add package MaksIT.Results
|
|
```
|
|
|
|
```powershell
|
|
Install-Package MaksIT.Results
|
|
```
|
|
|
|
## Quick start
|
|
|
|
### Create results
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
Result<int> source = Result<int>.Ok(42, "Value loaded");
|
|
|
|
Result<string?> asString = source.ToResultOfType(v => v?.ToString());
|
|
Result nonGeneric = source.ToResult();
|
|
```
|
|
|
|
### Controllers
|
|
|
|
```csharp
|
|
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`):
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
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](CONTRIBUTING.md). Changelog: [CHANGELOG.md](CHANGELOG.md).
|
|
|
|
## License
|
|
|
|
[MIT](LICENSE.md) - Maksym Sadovnychyy (MAKS-IT)
|
|
|
|
**Contact:** [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com)
|