A lightweight, zero-dependency Result Pattern library for .NET Standard 2.0+ / C# 7.3+.
Provides a consistent, predictable way to return success/failure from services and APIs — without hidden custom exceptions from implicit operators (converting a null Result/Result<T> instance across the two types still throws the standard NullReferenceException — see the Behavior Matrix below).
- Result Pattern —
Result,Result<T>,PagedResult<T> - Smart ResultCode — class-based enum with
Name,HttpStatus,IsSuccess - Zero dependency — no JSON library required;
Statusfield auto-excluded from serialization - No hidden custom throws — implicit operators never throw a custom exception; most return null/default on null input, except converting a null instance between
ResultandResult<T>, which throws the standardNullReferenceException(see Behavior Matrix) - Paging built-in —
Paged<T>,PagedResult<T>,ToPaged(),ToPagedResult() - Serialization-friendly — clean JSON output,
Codesetter supports deserialization - .NET Standard 2.0 — compatible with .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5+
dotnet add package Lightsoft.Result
using Light.Contracts;
using Light.Extensions;
// Success with data
Result<User> result = Result<User>.Success(user);
// Error without data
Result<User> result = Result<User>.NotFound("User not found.");
// Implicit conversion
Result<string> result = "hello"; // Success
Result<string> result = (string)null; // Error (no throw)
// Extract data
if (result.IsSuccess)
{
User user = result; // implicit Result<T> -> T
}
// Non-generic result
Result result = Result.Success("Operation completed.");
// HTTP status mapping
HttpStatusCode status = result.ToHttpStatusCode(); // 200
// Paging
var paged = list.ToPagedResult(pageNumber: 1, pageSize: 10);Class-based smart enum. Identity is based on Name.
// Built-in codes
ResultCode.Success // "success", 200, IsSuccess = true
ResultCode.BadRequest // "bad_request", 400
ResultCode.Unauthorized // "unauthorized", 401
ResultCode.Forbidden // "forbidden", 403
ResultCode.NotFound // "not_found", 404
ResultCode.Conflict // "conflict", 409
ResultCode.Error // "error", 500
ResultCode.Unknown // "unknown", 500
// Custom codes
var rateLimited = new ResultCode("rate_limited", 429);
// Equality based on Name
new ResultCode("test", 200) == new ResultCode("test", 500) // true
// Implicit string conversion
string code = ResultCode.Success; // "success"
string code = (ResultCode)null; // null (no throw)
// FromName for deserialization
ResultCode.FromName("not_found") // ResultCode.NotFound (singleton)
ResultCode.FromName("custom") // new ResultCode("custom", 500)
ResultCode.FromName(null) // ResultCode.UnknownRead-only interfaces. All properties are get-only.
public interface IResult
{
string RequestId { get; }
string Code { get; }
bool IsSuccess { get; }
string Message { get; }
}
public interface IResult<out T> : IResult
{
T Data { get; }
}Abstract base class for all result types.
| Member | Type | Serialized | Description |
|---|---|---|---|
RequestId |
string |
Yes | Lazy-generated GUID |
Status |
ResultCode |
No (field) | Not serialized by any JSON library |
Code |
string |
Yes | Getter reads Status.Name, setter calls FromName() |
IsSuccess |
bool |
Yes | Computed from Status.IsSuccess |
Message |
string |
Yes | Default "" |
Non-generic result. Factory methods:
Result.Success("message")
Result.BadRequest("message")
Result.Unauthorized("message")
Result.Forbidden("message")
Result.NotFound("message")
Result.Conflict("message")
Result.Error("message")
Result.From(customCode, "message") // throws if customCode is nullGeneric result with Data. Factory methods + implicit operators:
// Factories
Result<T>.Success(data, "message") // null data -> Error result (no throw)
Result<T>.NotFound("message")
Result<T>.Error("message")
Result<T>.From(customCode, "message")
// Implicit operators - NONE throw
Result<string> r = "hello"; // T -> Result<T>: Success
Result<string> r = (string)null; // T -> Result<T>: Error (no throw)
string value = r; // Result<T> -> T: returns .Data
Result simple = r; // Result<T> -> Result: preserves RequestId
Result<string> typed = simple; // Result -> Result<T>: preserves RequestIdNote:
Datahas a public setter (needed forSystem.Text.Json/Newtonsoft.Jsonreflection-based deserialization — a get-onlyDatasilently breaks JSON round-tripping since neither library can otherwise populate it). This meansDatacan be reassigned after construction, which can desync it fromIsSuccess/Status(e.g. settingData = nullon an already-Successresult does not flip it back toError). Treat post-construction mutation as unsupported; the factories/implicit operators are the source of truth for a consistent state. Same caveat applies toPagedResult<T>.Data.
| Operator | null input | Behavior |
|---|---|---|
T -> Result<T> |
null | Error result (Code = "error") |
Result<T> -> T |
null Result<T> instance, or null .Data |
Returns default(T) |
Result<T> -> Result |
null Result<T> instance |
NullReferenceException (standard .NET) |
Result -> Result<T> |
null Result instance |
NullReferenceException (standard .NET) |
ResultCode -> string |
null | Returns null |
PagedResult<T> -> Paged<T> |
null | Returns null |
Design principle: Implicit operators never throw custom exceptions. Converting
nulldata, or a nullResult<T>instance toT, safely returnsdefault/null. The two exceptions are theResult<T> <-> Resultconversions: converting a null instance across these two types still throws the standardNullReferenceException, since there is no instance to readRequestId/Status/Messagefrom. Developer checksIsSuccessbefore accessingData.
// Interfaces
IPage -> PageNumber, PageSize (mutable — intended for binding page requests, e.g. from query params)
IPaged -> TotalPages, TotalRecords, HasNextPage, HasPreviousPage (get-only)
IPaged<T> -> Records (get-only)
// Classes
Paged -> implements IPaged
Paged<T> -> implements IPaged<T>, inherits Paged
PagedResult<T> -> ResultBase + IResult<Paged<T>>
// Usage
var paged = list.ToPaged(pageNumber: 1, pageSize: 10);
var result = list.ToPagedResult(pageNumber: 1, pageSize: 10);
// PagedResult<T> is success-oriented by design (no BadRequest/NotFound/etc. factories) —
// a paging query either returns data (possibly an empty page) or a null-data Error;
// it doesn't model arbitrary failure statuses the way Result/Result<T> do.
new PagedResult<T>(pagedData, "message") // null pagedData -> Error, message overridable
// Implicit conversion
Paged<int> data = pagedResult; // null -> null (no throw)
// TotalPages calculation
// PageSize > 0: Math.Ceiling(TotalRecords / PageSize)
// PageSize <= 0: 0
// Invalid values auto-clamped
list.ToPagedResult(0, -1); // pageNumber=1, pageSize=10
// A pageNumber beyond the available data returns an empty page,
// not a wrapped/negative-skip result (overflow-safe for large pageNumber)using Light.Extensions;
// IsFailed
result.IsFailed(); // true if !IsSuccess (also true, not a throw, when result is null)
// ToHttpStatusCode
result.ToHttpStatusCode(); // Success -> 200, NotFound -> 404, etc.
// works for any IResult, not just ResultBase-derived types
// (resolves via ResultCode.FromName(result.Code))
// ToPagedResult
list.ToPagedResult(pageNumber, pageSize);
list.ToPagedResult(iPage);
nullList.ToPagedResult(); // Error result (no throw)
// ToPaged
list.ToPaged(pageNumber, pageSize);
list.ToPaged(iPage);
nullList.ToPaged(); // empty Paged<T> (no throw)Status is a public field — not serialized by System.Text.Json or Newtonsoft.Json by default.
{
"RequestId": "6da2dcec-6292-4030-994f-b8a467c1681f",
"Code": "success",
"IsSuccess": true,
"Message": "",
"Data": { ... }
}No
Status,HttpStatus, orNamefields leak into JSON.
Code setter automatically restores Status via ResultCode.FromName():
var json = JsonSerializer.Serialize(result);
var restored = JsonSerializer.Deserialize<Result>(json);
restored.Code; // "not_found"
restored.Status; // ResultCode.NotFound (singleton)
restored.IsSuccess; // falseOnly explicit method calls with required parameters throw:
| Method | When | Exception |
|---|---|---|
new ResultCode(null) |
name is null | ArgumentNullException |
Result.From(null) |
status is null | ArgumentNullException |
Result<T>.From(null) |
status is null | ArgumentNullException |
ToPagedResult(null IPage) |
page is null | ArgumentNullException |
ToPaged(null IPage) |
page is null | ArgumentNullException |
JsonSerializer.Deserialize<ResultCode>(json) |
JSON is missing the "Name" field |
ArgumentNullException |
ToPaged(null list)andToPagedResult(null list)are both null-safe (return an emptyPaged<T>/ an Error result respectively) — only a nullIPagepage argument throws, since it's a required parameter object, not the data being paged.
ResultCode's JSON deserialization binds to its constructor by matching parameter names to JSON properties; a JSON payload missing"Name"passesnullthrough to the constructor's required parameter, which throws. Missing"HttpStatus"/"IsSuccess"fields instead silently default to500/false— only the requirednameparameter throws.
Implicit operators never throw custom exceptions.
[ApiExplorerSettings(IgnoreApi = true)]
public virtual IActionResult Ok<T>(T data)
{
var result = data as IResult ?? Result<T>.Success(data);
var statusCode = (int)result.ToHttpStatusCode();
return StatusCode(statusCode, result);
// data null -> Error result (no throw)
// data valid -> Success result
}src/Result/Contracts/
├── ResultCode.cs — Smart enum with built-in codes
├── IResult.cs — IResult, IResult<T> interfaces
├── ResultBase.cs — Abstract base (Status field, Code property)
├── Result.cs — Non-generic result
├── ResultOfT.cs — Generic Result<T> with implicit operators
├── IPage.cs — IPage interface
├── IPaged.cs — IPaged, IPaged<T> interfaces
├── Paged.cs — Paged, Paged<T> classes
├── PagedResult.cs — PagedResult<T>
src/Result/Extensions/
├── ResultExtensions.cs — IsFailed, ToHttpStatusCode
└── PagedExtensions.cs — ToPaged, ToPagedResult
- .NET Standard 2.0 (
netstandard2.0) - C# 7.3 compatible
- Zero external dependencies
MIT