Pitasoft.Result
7.2.8
See the version list below for details.
dotnet add package Pitasoft.Result --version 7.2.8
NuGet\Install-Package Pitasoft.Result -Version 7.2.8
<PackageReference Include="Pitasoft.Result" Version="7.2.8" />
<PackageVersion Include="Pitasoft.Result" Version="7.2.8" />
<PackageReference Include="Pitasoft.Result" />
paket add Pitasoft.Result --version 7.2.8
#r "nuget: Pitasoft.Result, 7.2.8"
#:package Pitasoft.Result@7.2.8
#addin nuget:?package=Pitasoft.Result&version=7.2.8
#tool nuget:?package=Pitasoft.Result&version=7.2.8
Pitasoft.Result
English
Pitasoft.Result is a .NET library designed to standardize responses from REST services and internal application layers. It provides a robust set of classes to wrap data, status codes, and error collections, facilitating a unified communication contract between APIs, services, and clients.
Features
- Standardized Responses: Unify your API outputs using
Result,ResultEntity<T>,ResultEntities<T>,ResultPaged<T>, andResultBatch<T>. - Automatic Timestamps: All results now include a
CalculationTime(DateTimeOffset?) to track when they were generated, via theIHasCalculationTimestampinterface. - Fluent Error Handling: Easily add validation errors, exceptions, or business rule violations using a fluent interface. Includes
Result.TryandResult.TryAsyncfor automatic exception handling, with built-in support forCancellationTokenandOperationCanceledException→CancelOperation. - Rich Status Management: Built-in
StatusResultenum covering common API scenarios (Success, Not Found, Forbidden, Conflict, Validation Errors, Database Errors, etc.). IncludesStatusResultExtensionsfor semantic categorization (IsSuccess,IsInfrastructureError,IsBusinessError,IsTransientError,IsError). - Batch Operations: Specialized support for processing multiple items with
ResultBatch<T>usingIReadOnlyList<T>for efficient access. - Pagination Support:
ResultPaged<T>provides full paging metadata (TotalCount, Page, PageSize, TotalPages, etc.). Includes a full set of static factory methods in theResultclass (e.g.,Result.ErrorPaged,Result.DatabaseErrorPaged,Result.NotFoundPaged) andTryPagedAsyncfor exception-safe paged queries. - Async Support: Native support for
IAsyncEnumerable<T>andTask<T>withMaterializeAsyncandToResultEntitiesAsync. IncludesMapAsync,BindAsync,MatchAsync, andTapAsyncextensions. - Improved Serialization: Custom JSON converters for
ResultEntities<T>andResultPaged<T>ensure clean and predictable API outputs. - Implicit Conversions: Convert
StatusResultor entities directly to result types with zero boilerplate. - Deconstruction: Use C# deconstruction to extract values and status easily:
var (status, user) = result;. - Extensions: Useful helper methods to check for success, specific errors, or state. Including
ToOkResult(),ToAddedResult(),ToUpdatedResult(), andToDeletedResult()for converting entities to results. - Functional API:
Match,MatchAsync,Map,MapAsync,Tap,TapAsync,Bind, andEnsurefor fluent result processing. All operations are available forResult,ResultEntity<T>,ResultEntities<T>, andResultPaged<T>. - Combine Results: Aggregate multiple results into one with
Result.Combine()andResult.CombineAsync(). - Multi-value Parameters:
IParameters.GetParameters()returnsIEnumerable<KeyValuePair<string, string>>, supporting multiple values for the same key (e.g.,?tag=a&tag=b). ErrorCollectionImprovements:Emptyis now a property (safe, non-shared instance). DirectCountproperty for O(1) access.- Performance: High-performance implementation with minimal allocations,
AggressiveInlining, and efficient collection materialization usingIReadOnlyList<T>.
Installation
dotnet add package Pitasoft.Result
Quick start
// 1) Simple result
Result r1 = Result.Ok();
Result r2 = Result.ValidationError().AddError("Email", "Invalid");
// 2) Entity
ResultEntity<User> u = Result.Ok(new User { Id = 1, Name = "John" });
var (status, user) = u; // deconstruction
var (s, e) = u; // status and entity
var (st, code, errs) = (Result)u; // deconstruction with result code from base Result
// 3) Paged entities
var users = new List<User> { new() { Id = 1, Name = "John" } };
ResultPaged<User> paged = Result.OkPaged(users, totalCount: 100, page: 1, pageSize: 10);
foreach (var it in paged) { /* iterate directly */ }
// 4) Try/Catch automation (with CancellationToken support)
var result = Result.Try(() => DoWork());
var resultAsync = await Result.TryAsync(async ct => await DoWorkAsync(ct), cancellationToken);
// 5) Try variants for collections
ResultEntities<User> entities = Result.TryEntities(() => GetUsers());
ResultEntities<User> entitiesAsync = await Result.TryEntitiesAsync(async () => await GetUsersAsync());
ResultPaged<User> pagedResult = await Result.TryPagedAsync(
async () => await GetPagedAsync(), page: 1, pageSize: 20);
// 6) Functional extensions (available on all result types)
var dto = u.Map(x => new UserDto(x!.Id, x.Name));
var okOrThrow = u.Match(
onSuccess: () => dto,
onFailure: res => throw new InvalidOperationException(res.Status.ToString()));
// MatchAsync on ResultEntities / ResultPaged
var response = await pagedResult.MatchAsync(
onSuccess: async p => await BuildPagedDto(p),
onFailure: async r => await HandleError(r));
// 7) Combine results (sync and async)
var combined = Result.Combine(Result.Ok(), Result.Error().AddError("E", "err"));
var combinedAsync = await Result.CombineAsync(Task1Async(), Task2Async());
// 8) IsSuccess / IsFailure as extension methods
if (result.IsSuccess()) { /* ... */ }
if (result.IsFailure()) { /* ... */ }
// 9) StatusResult semantic extensions
if (result.Status.IsInfrastructureError()) { /* retry logic */ }
if (result.Status.IsBusinessError()) { /* return 422 */ }
// 10) Calculation Timestamp
DateTimeOffset? time = result.CalculationTime;
CalculationTime is normalized to UTC when assigned, so consumers can treat it as a stable cross-system timestamp.
Core Components
1. Simple Result (Result)
Used for operations that don't return data, only a completion status and optional errors.
public Result DeleteUser(int id)
{
if (id <= 0)
return Result.ValidationError().AddError("id", "Invalid ID");
var deleted = _repository.Delete(id);
if (!deleted) return Result.NotExists();
return Result.Deleted();
}
// Deconstruction
var (status, errors) = DeleteUser(1);
if (status == StatusResult.Deleted) { /* ... */ }
2. Result with Entity (ResultEntity<T>)
Used for operations returning a single object.
public ResultEntity<User> GetUser(int id)
{
var user = _repository.Find(id);
if (user == null) return ResultEntity<User>.NotExists();
return ResultEntity<User>.Ok(user);
}
Implicit conversions and fluent API:
public ResultEntity<User> GetUser(int id)
{
var user = _repository.Find(id);
// T implicitly converts to ResultEntity<T>.Ok(entity)
return user ?? (ResultEntity<User>)StatusResult.NoExist;
}
// Deconstruction
var (status, user) = GetUser(1);
if (status == StatusResult.Ok) { /* ... */ }
// Fluent API
var result = ResultEntity<User>.Ok(user)
.WithCode(200)
.AddError("System", "Service throttled");
3. Result with Multiple Entities (ResultEntities<T>)
Used for lists of results. It uses IReadOnlyList<T> for the Entities property to ensure efficiency.
Direct iteration is supported via foreach as it implements IEnumerable<T>.
When an IEnumerable<T> is provided, the library materializes it into an IReadOnlyList<T> if needed. This gives stable indexing and prevents repeated enumeration of deferred sources.
public ResultEntities<User> GetActiveUsers()
{
IEnumerable<User> users = _repository.GetActive();
// Materializes the collection into IReadOnlyList
return Result.OkEntities(users);
}
Use TryEntities / TryEntitiesAsync to handle exceptions automatically:
public Task<ResultEntities<User>> GetActiveUsersAsync()
=> Result.TryEntitiesAsync(() => _repository.GetActiveAsync());
4. Paged Result (ResultPaged<T>)
Inherits from ResultEntities<T> and includes complete metadata for paginated results (TotalCount, Page, PageSize, TotalPages, HasNextPage, HasPreviousPage).
When paging metadata is provided as a complete set through the constructor, Result.OkPaged(...), MaterializePaged(...), ToPaged(...), or WithPaging(...), the library enforces these invariants:
TotalCount >= 0Page >= 1PageSize > 0
Individual property setters remain permissive enough for progressive initialization and JSON deserialization, but complete paging operations are validated to avoid inconsistent states.
Create paged results easily using the Result static class:
public ResultPaged<User> GetUsers(PagingParameters paging)
{
try
{
var (users, total) = _repository.GetAll(paging);
return Result.OkPaged(users, total, paging.Page, paging.PageSize);
}
catch (Exception ex)
{
return Result.DatabaseErrorPaged<User>(ex);
}
}
// Deconstruction
var (status, users, total, page, pageSize) = result;
Use TryPagedAsync to handle exceptions and cancellation automatically:
public Task<ResultPaged<User>> GetUsersAsync(PagingParameters paging, CancellationToken ct)
=> Result.TryPagedAsync(
async () => await _repository.GetAllAsync(paging, ct),
paging.Page, paging.PageSize);
5. Batch Result (ResultBatch<T>)
Used for processing multiple items in a single request, providing a global status and individual results for each item using IReadOnlyList<ResultEntity<T>>.
As with ResultEntities<T>, batch collections are materialized to IReadOnlyList<ResultEntity<T>> when necessary.
The batch Status represents the overall outcome of the operation, while each item in Entities keeps its own independent ResultEntity<T> status.
public ResultBatch<User> ImportUsers(List<User> users)
{
var results = users.Select(u => (ResultEntity<User>)Process(u));
return ResultBatch<User>.Ok(results);
}
// Deconstruction
var (status, results) = importResult;
// Aggregate indicators
bool hasPartialErrors = importResult.HasPartialErrors;
int successCount = importResult.SuccessCount;
int errorCount = importResult.ErrorCount;
Static factory methods are available for all common statuses:
ResultBatch<T>.Ok(entities)ResultBatch<T>.ValidationError(errors)ResultBatch<T>.DatabaseError(ex)ResultBatch<T>.NotFound()ResultBatch<T>.Forbidden()- ... and all other
StatusResultstates.
Error Handling
The library supports a fluent API for adding errors, which are stored in an ErrorCollection:
return Result.ValidationError()
.AddError("Email", "Invalid format")
.AddError("Password", "Too short")
.AddError(new Exception("Inner system error"));
Associate a numeric code with any result using WithCode():
return Result.Error().WithCode(4001);
ErrorCollection exposes a Count property for O(1) access and Empty is always a fresh, non-shared instance:
bool hasErrors = result.Errors.Count > 0;
var emptyErrors = ErrorCollection.Empty; // always returns a new instance
Status Codes (StatusResult)
| Status | Category | Description |
|---|---|---|
Ok, Added, Updated, Deleted |
Success | Successful operations. |
NoExist |
Informational | Requested resource was not found (semantic domain). |
Warning |
Informational | Completed with non-critical issues. |
CancelOperation |
Control | Operation was cancelled. |
ValidationError |
Error | Client-side input validation failed. |
DataError, DatabaseError |
Error | Issues with data processing or persistence. |
ConcurrencyError |
Error | Data was modified by another process. |
ConnectionError |
Error | Connection to an external service failed. |
HttpError |
Error | An external HTTP call returned an error. |
Unauthorized |
Security | Authentication or authorization failed. |
Forbidden |
Security | The server understood the request but refuses to authorize it (403). |
NotFound |
Error | The requested resource was not found (404). |
Conflict |
Error | The request conflicts with the current state of the server (409). |
UnprocessableEntity |
Error | Semantic errors in the request (422). |
TooManyRequests |
Error | Rate limit exceeded (429). |
ServiceUnavailable |
Error | Server is not ready to handle the request (503). |
ChangePassword |
Security | User must change their password. |
Error, Exception |
Error | Generic error or an unhandled exception. |
Status Selection Guide
Use these rules to keep status semantics consistent across services and APIs:
- Prefer
NoExistfor internal or domain flows where absence is expected and should not necessarily be treated as an error. - Prefer
NotFoundwhen the result is intended to map directly to an HTTP404response. - Use
ValidationErrorwhen the input is invalid before processing starts. - Use
UnprocessableEntitywhen the input is structurally valid but business semantics prevent execution. - Use
DataErrorfor domain or data consistency issues discovered during processing. - Use
DatabaseError,ConnectionError,HttpError, andServiceUnavailablefor infrastructure or dependency failures. - Use
Conflictwhen the requested action collides with the current state of the resource. - Use
Unauthorizedwhen authentication is missing or invalid. - Use
Forbiddenwhen the caller is authenticated but does not have permission to perform the action. - Use
Erroras a generic fallback for known failures that do not fit a more specific status. - Use
Exceptiononly when the failure comes from an unexpected exception and no better status is available.
Examples:
// Domain absence in an internal service
return Result.NotExists<User>();
// API-facing 404 response
return Result.NotFound<User>();
// Invalid input
return Result.ValidationError<User>()
.AddError("Email", "Invalid format");
// Valid request, but business rule prevents execution
return Result.UnprocessableEntity<User>()
.AddError("State", "User is archived");
// Known fallback error
return Result.Error().AddError("Order", "Unable to complete the operation");
// Unexpected exception
catch (Exception ex)
{
return Result.Exception<User>(ex);
}
StatusResult Extensions
Import Pitasoft.Result.Extensions to use semantic classification on StatusResult values:
if (result.Status.IsSuccess()) { /* Ok / Added / Updated / Deleted */ }
if (result.Status.IsInfrastructureError()) { /* DatabaseError / ConcurrencyError / ConnectionError / HttpError / ServiceUnavailable */ }
if (result.Status.IsBusinessError()) { /* ValidationError / DataError / NoExist / Unauthorized / Forbidden / NotFound / Conflict / UnprocessableEntity / ChangePassword */ }
if (result.Status.IsTransientError()) { /* Error / Exception / CancelOperation / ConcurrencyError / ConnectionError / HttpError / TooManyRequests / ServiceUnavailable */ }
if (result.Status.IsError()) { /* General or specialized error states (Validation, Data, Database, Concurrency, HTTP, Exception, etc.) */ }
These are especially useful in middleware or retry policies to decide how to handle a failure without inspecting the Status value directly.
Extensions
Import Pitasoft.Result.Extensions to use these helper methods on any IResult:
result.IsSuccess(): Returnstrueif status isOk,Added,Updated, orDeleted.result.IsError(): Returnstruefor most error-related statuses.result.IsFailure(): Returnstruefor any non-successful state, excludingNone,NoExist, andWarning. CoversUnauthorized,Exception,CancelOperation, and more.result.IsOk(): Returnstrueif the status is exactlyOk.result.IsWarning(): Returnstrueif the status isWarning.result.IsNotExist(): Returnstrueif the resource was not found (NoExist).result.IsUnauthorized(): Returnstrueif the status isUnauthorized.result.IsForbidden()/result.IsNotFound()/result.IsConflict()/result.IsTooManyRequests()/result.IsUnprocessableEntity()/result.IsServiceUnavailable(): Semantic checks for HTTP-like error states.result.HasErrors(): Returnstrueif there are any errors in theErrorscollection.result.IsSuccessOrNotExist(): Useful for "Delete" operations where both cases are often handled similarly.result.IsSuccessOrWarning(): Returnstruefor successful or warning states.Materialize()/MaterializeAsync(): ConvertIEnumerable<T>orIAsyncEnumerable<T>toResultEntities<T>orResultPaged<T>.UpdateTimestamp(): Manually updates theCalculationTimeto the current UTC time onResultand derived types.ToPaged(): Convert a result to aResultPaged<T>preserving state. Requires valid paging metadata (TotalCount >= 0,Page >= 1,PageSize > 0).ToResultEntity()/ToResultEntities(): Convert between result types preserving metadata.ToOkResult()/ToAddedResult()/ToUpdatedResult()/ToDeletedResult(): Convert an entity to aResultEntity<T>with the corresponding success status.
Results also validate Status assignments against the defined StatusResult enum values. This helps detect invalid manual assignments early when working with mutable instances or custom deserialization flows.
Functional API (Match, Map, Bind, Ensure, EnsureAsync, Recover, Tap, TapAsync)
Import Pitasoft.Result.Extensions to use functional-style transformations.
Behavior notes:
MatchandTapdo not change the result. They only branch or execute side effects.Mapprojects successful values into a new result. For collection and paged results it preservesStatus,Errors,ResultCode, andCalculationTime. ForResultPaged<T>it also preserves pagination metadata.Bindshort-circuits on failure. For collection and paged results, failure propagation preserves metadata; on success, the binder owns the final result it returns.Ensure/EnsureAsynconly run on successful results. If validation fails, they return or produce a validation error instead of continuing the success flow.Recoverunwraps aResultEntity<T>into a plainT, providing a fallback only when the result is not successful.EnsureonResultPaged<T>andResultBatch<T>updates the current instance toValidationErrorand appends errors, instead of allocating a new container result.
Match — branch on success or failure
Available for Result, ResultEntity<T>, ResultEntities<T>, and ResultPaged<T>:
// Result
string msg = result.Match(
onSuccess: () => "Done",
onFailure: r => $"Error: {r.Status}");
// ResultEntity<T>
var dto = userResult.Match(
onSuccess: () => mapper.ToDto(userResult.Entity),
onFailure: r => throw new InvalidOperationException(r.Status.ToString()));
// ResultEntities<T> / ResultPaged<T>
var view = pagedResult.Match(
onSuccess: () => BuildView(pagedResult),
onFailure: r => ErrorView(r));
MatchAsync — async branch on success or failure
Available for Result, ResultEntity<T>, ResultEntities<T>, and ResultPaged<T>:
var response = await pagedResult.MatchAsync(
onSuccess: async p => await BuildPagedResponseAsync(p),
onFailure: async r => await HandleFailureAsync(r));
Map — transform the entity if successful
Available for ResultEntity<T>, ResultEntities<T>, and ResultPaged<T>:
ResultEntity<UserDto> dto = userResult.Map(user => mapper.ToDto(user));
ResultEntities<UserDto> dtos = usersResult.Map(user => mapper.ToDto(user));
ResultPaged<UserDto> paged = pagedResult.Map(user => mapper.ToDto(user));
If the source result is not successful, Map does not execute the projection and propagates the failure state instead.
MapAsync — chain async transformations
ResultEntity<UserDto> dto = await userResult.MapAsync(async u => await EnrichAsync(u));
ResultEntities<UserDto> dtos = await usersResult.MapAsync(async u => await EnrichAsync(u));
ResultPaged<UserDto> paged = await pagedResult.MapAsync(async u => await EnrichAsync(u));
Tap — execute an action without changing the result
result.Tap(() => _logger.LogInformation("Operation successful"))
.Tap(user => _cache.Set(user));
TapAsync — async side-effect without changing the result
Works on any result type (Result, ResultEntity<T>, ResultEntities<T>, ResultPaged<T>):
var result = await GetUserAsync(id)
.TapAsync(async () => await _auditService.LogAsync("user fetched"));
// Typed variant
var result = await GetUserAsync(id)
.TapAsync(async user => await _cache.SetAsync(user));
Bind — chain operations that return results (FlatMap)
Available for ResultEntity<T>, ResultEntities<T>, and ResultPaged<T>:
ResultEntity<Order> result = GetUser(id)
.Bind(user => CreateOrder(user));
ResultPaged<OrderDto> paged = GetPagedOrders(page, size)
.Bind(o => EnrichOrder(o));
Bind only invokes the binder when the source result is successful. If the source has already failed, the chain stops and the failure state is propagated.
Ensure — validate a condition
On Result (base):
Result result = CheckPermissions(userId)
.Ensure(() => _quota.HasCapacity(), "Quota exceeded", "Quota");
On ResultEntity<T>:
ResultEntity<User> result = GetUser(id)
.Ensure(u => u.Age >= 18, "User must be an adult", "Age");
For Result and ResultEntity<T>, a failed Ensure returns a validation result. For ResultPaged<T> and ResultBatch<T>, a failed Ensure mutates the current instance to ValidationError and adds the corresponding error.
EnsureAsync — validate an asynchronous condition
Perform validations that require I/O, like database checks, within the fluent chain:
ResultEntity<User> result = await GetUser(id)
.EnsureAsync(async u => await _repo.IsEmailUniqueAsync(u.Email), "Email already exists", "Email");
Recover — provide a fallback value on failure
Safely handle failures by providing a default value or a recovery function:
// Simple fallback
User user = GetUser(id).Recover(new User { Name = "Guest" });
// Recovery function
User user = GetUser(id).Recover(r => new User { Name = $"Guest (Error: {r.Status})" });
Recover is intentionally an unwrap operation for ResultEntity<T>: after calling it, you are working with a plain value instead of a result object.
Exception-safe Factory Methods (Try)
All Try variants automatically catch exceptions. OperationCanceledException is mapped to CancelOperation instead of Exception.
You can now optionally provide a mapException function to transform specific exceptions into meaningful StatusResult codes.
| Method | Returns | Notes |
|---|---|---|
Result.Try(action, deep?, mapEx?) |
Result |
Sync, no return value |
Result.Try<T>(func, deep?, mapEx?) |
ResultEntity<T> |
Sync, returns entity |
Result.TryAsync(action, ct?, deep?, mapEx?) |
Task<Result> |
Async, supports CancellationToken |
Result.TryAsync<T>(func, ct?, deep?, mapEx?) |
Task<ResultEntity<T>> |
Async entity, supports CancellationToken |
Result.TryEntities<T>(func, deep?, mapEx?) |
ResultEntities<T> |
Sync collection |
Result.TryEntitiesAsync<T>(func, deep?, mapEx?) |
Task<ResultEntities<T>> |
Async collection |
Result.TryPagedAsync<T>(func, page, pageSize, deep?, mapEx?) |
Task<ResultPaged<T>> |
Async paged collection |
// Automatic mapping of exceptions to status codes
var result = Result.Try(() => _repo.Get(id),
mapException: ex => ex is KeyNotFoundException ? StatusResult.NotFound : StatusResult.Error);
// CancellationToken support — OperationCanceledException → CancelOperation
var result = await Result.TryAsync(
async ct => await _service.ProcessAsync(ct),
cancellationToken);
// Entities
ResultEntities<Product> products = Result.TryEntities(() => _repo.GetAll());
// Paged
ResultPaged<Product> page = await Result.TryPagedAsync(
async () => await _repo.GetPagedAsync(1, 20),
page: 1, pageSize: 20);
Combine Results
Aggregate multiple Result instances into one. The combined result is Ok if all succeed; otherwise it carries all errors from failed results.
// Synchronous
Result combined = Result.Combine(
ValidateName(dto.Name),
ValidateEmail(dto.Email),
ValidateAge(dto.Age));
// Asynchronous
Result combined = await Result.CombineAsync(
ValidateNameAsync(dto.Name),
ValidateEmailAsync(dto.Email));
Pagination and Entity Parameters
Use PagingParameters to receive pagination input and EntityParameters for filtering, searching, and sorting.
GetParameters() returns IEnumerable<KeyValuePair<string, string>>, which supports multiple values for the same key (useful for array-style query strings like ?tag=a&tag=b):
public ResultPaged<User> GetUsers(EntityParameters parameters)
{
// parameters.Page, parameters.PageSize, parameters.Search, parameters.Query, parameters.Order, parameters.Attrs
var (users, total) = _repository.GetAll(parameters);
return Result.OkPaged(users, total, parameters.Page, parameters.PageSize);
}
// Convert to Dictionary when a single-value map is sufficient
var dict = parameters.GetParameters().ToDictionary(kv => kv.Key, kv => kv.Value);
// Or use directly with HttpClient query builders that accept IEnumerable<KVP>
var query = QueryString.Create(parameters.GetParameters());
Batch Helper (Batch<T>)
Use Batch<T> to describe a set of entities and the action to perform on them:
var batch = Batch<User>.Append(newUsers);
var batch = Batch<User>.Update(modifiedUsers);
var batch = Batch<User>.Delete(removedUsers);
Performance
Performance tips
- Factory Fast Paths: Common
Resultstates such asNotExists()andValidationError()use small inlined factory helpers, keeping creation overhead low without changing result mutability semantics. - Parameter Optimization:
PagingParameters.GetParameters()andEntityParameters.GetParameters()now useyield returninstead of creating temporary lists, improving memory efficiency from $O(n)$ to $O(1)$ space. - Collection Efficiency:
ResultEntities<T>andResultPaged<T>useIReadOnlyList<T>for theEntitiesproperty. Collections are materialized efficiently into a list only when necessary. - Aggressive Inlining: Static factories and extension methods are annotated with
AggressiveInliningto minimize overhead. - Direct Iteration: Supported via
foreach. Internally, its enumerator delegates to the underlying collection without usingyield, avoiding extra state-machine allocations. - JSON Payload:
ResultPaged<T>automatically ignores calculated properties likeTotalPages,HasNextPage, andHasPreviousPageduring serialization to reduce payload size. - Status checks:
IsSuccess(),IsFailure()and the other semantic extension methods are annotated withAggressiveInliningto keep their overhead minimal in hot paths. - Exception handling: When creating error results from exceptions manually, pass
deep: falseunless you explicitly need inner exception details to reduce allocations inErrorCollection.
Benchmark results for representative operations (Mean time / Allocated memory):
The library is optimized to minimize allocations and maximize throughput. The current Pitasoft.Result.Benchmarks suite also includes the new functional and paging scenarios introduced in recent revisions.
Quick take:
.NET 10leads the representative creation and functional paths in this benchmark set, especially around paging andResultBatch<T>transformations..NET 8stays very competitive and currently delivers the strongestSystem.Text.Jsonserialization numbers on this machine, while also remaining the LTS baseline..NET 9works correctly and lands between both runtimes in most scenarios, but it is not the clear winner in this profile.
| Operation | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Result.Ok |
18.38 ns / 64 B | 19.22 ns / 64 B | 19.91 ns / 64 B |
Result.Error(Exception) |
44.12 ns / 376 B | 48.85 ns / 376 B | 42.78 ns / 376 B |
ResultEntities.Ok |
23.64 ns / 72 B | 24.28 ns / 72 B | 20.32 ns / 72 B |
Result.OkPaged |
24.35 ns / 88 B | 25.28 ns / 88 B | 19.65 ns / 88 B |
ResultBatch.Ok(entities) |
24.17 ns / 72 B | 24.78 ns / 72 B | 23.03 ns / 72 B |
ResultEntity.Map |
25.57 ns / 72 B | 23.69 ns / 72 B | 20.92 ns / 72 B |
ResultPaged.WithPaging |
19.15 ns / 88 B | 19.93 ns / 88 B | 17.07 ns / 88 B |
ResultPaged.Bind (failure path) |
22.97 ns / 88 B | 24.58 ns / 88 B | 20.73 ns / 88 B |
ResultBatch.Map (failure path) |
24.00 ns / 96 B | 25.22 ns / 96 B | 22.51 ns / 96 B |
ResultBatch.Bind (success path) |
107.04 ns / 416 B | 112.56 ns / 416 B | 100.61 ns / 416 B |
ResultBatch.Ensure (failure path) |
92.90 ns / 568 B | 95.04 ns / 568 B | 81.35 ns / 568 B |
Methodology:
- BenchmarkDotNet
v0.15.8 InProcessNoEmitToolchain- Apple M5 / macOS Tahoe 26.3.1 / Arm64
- Detailed reports are generated under BenchmarkDotNet.Artifacts/results
To regenerate the representative benchmark suite and keep separate snapshots for .NET 8, .NET 9, and .NET 10, run:
./scripts/run-benchmarks.sh
You can also restrict the execution to specific runtimes:
./scripts/run-benchmarks.sh net10.0
The script stores per-runtime reports under BenchmarkDotNet.Artifacts/snapshots/<framework> and refreshes the benchmark tables in this README from those snapshots automatically.
If you already have fresh snapshots and only want to rebuild the tables, run:
./scripts/update-benchmark-readme.sh
Representative JSON serialization benchmarks (System.Text.Json):
| Operation | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Serialize Result |
216.8 ns / 656 B | 220.6 ns / 656 B | 197.1 ns / 656 B |
Deserialize Result |
481.0 ns / 1344 B | 484.6 ns / 1320 B | 500.5 ns / 1320 B |
Serialize ResultEntities |
411.1 ns / 944 B | 404.8 ns / 944 B | 431.5 ns / 944 B |
Deserialize ResultEntities |
1,039.9 ns / 1152 B | 1,019.7 ns / 1152 B | 1,128.0 ns / 1152 B |
Serialize ResultPaged |
471.2 ns / 1144 B | 470.7 ns / 1144 B | 496.1 ns / 1144 B |
Deserialize ResultPaged |
1,312.0 ns / 1376 B | 1,267.2 ns / 1376 B | 1,414.9 ns / 1376 B |
Microbenchmarks for status and result checks:
These numbers are useful as relative signals, not as absolute promises. When BenchmarkDotNet reports 0.0000 ns in these tiny checks, it means the runtime optimized the path so aggressively that the cost is indistinguishable from the empty-method baseline in this setup.
| Operation | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Status direct success check |
0.2312 ns / 0 B | 0.5399 ns / 0 B | 0.0175 ns / 0 B |
Status.IsSuccess() |
0.4051 ns / 0 B | 0.1336 ns / 0 B | 0.0000 ns / 0 B |
Status.IsFailure() |
0.2283 ns / 0 B | 0.0000 ns / 0 B | 0.1625 ns / 0 B |
IResult.IsSuccess() |
0.3246 ns / 0 B | 0.0000 ns / 0 B | 0.2668 ns / 0 B |
IResult.IsFailure() |
0.2608 ns / 0 B | 0.0740 ns / 0 B | 0.2546 ns / 0 B |
IResult.HasErrors() without errors |
0.2572 ns / 0 B | 0.0000 ns / 0 B | 0.0411 ns / 0 B |
IResult.HasErrors() with errors |
0.5401 ns / 0 B | 0.0000 ns / 0 B | 0.3177 ns / 0 B |
Status.IsError() |
0.2573 ns / 0 B | 0.0000 ns / 0 B | 0.2570 ns / 0 B |
Castellano
Pitasoft.Result es una librería .NET diseñada para estandarizar las respuestas de los servicios REST y las capas internas de la aplicación. Proporciona un conjunto robusto de clases para envolver datos, códigos de estado y colecciones de errores, facilitando un contrato de comunicación unificado entre APIs, servicios y clientes.
Características
- Respuestas Estandarizadas: Unifica las salidas de tu API usando
Result,ResultEntity<T>,ResultEntities<T>,ResultPaged<T>yResultBatch<T>. - Marcas de Tiempo Automáticas: Todos los resultados incluyen ahora
CalculationTime(DateTimeOffset?) para rastrear cuándo fueron generados, a través de la interfazIHasCalculationTimestamp. - Gestión de Errores Fluida: Añade fácilmente errores de validación, excepciones o violaciones de reglas de negocio mediante una interfaz fluida. Incluye
Result.TryyResult.TryAsynccon soporte nativo paraCancellationTokeny conversión automática deOperationCanceledException→CancelOperation. - Gestión de Estados Enriquecida: Enumerado
StatusResultintegrado que cubre escenarios comunes (Éxito, No Encontrado, Prohibido, Conflicto, Errores de Validación, Errores de Base de Datos, etc.). IncluyeStatusResultExtensionspara clasificación semántica (IsSuccess,IsInfrastructureError,IsBusinessError,IsTransientError,IsError). - Operaciones por Lote: Soporte especializado para procesar múltiples elementos con
ResultBatch<T>usandoIReadOnlyList<T>para un acceso eficiente. - Soporte para Paginación:
ResultPaged<T>proporciona metadatos completos de paginación (TotalCount, Page, PageSize, TotalPages, etc.). Incluye un conjunto completo de métodos de factoría estáticos en la claseResult(ej.,Result.ErrorPaged,Result.DatabaseErrorPaged,Result.NotFoundPaged) yTryPagedAsyncpara consultas paginadas con manejo automático de excepciones. - Soporte Async: Soporte nativo para
IAsyncEnumerable<T>yTask<T>conMaterializeAsyncyToResultEntitiesAsync. Incluye extensionesMapAsync,BindAsync,MatchAsyncyTapAsync. - Serialización Mejorada: Conversores JSON personalizados para
ResultEntities<T>yResultPaged<T>garantizan salidas de API limpias y predecibles. - Conversiones Implícitas: Convierte
StatusResulto entidades directamente a tipos de resultado sin código repetitivo. - Deconstrucción: Usa la deconstrucción de C# para extraer valores y estado fácilmente:
var (status, user) = result;. - Extensiones: Métodos de ayuda útiles para comprobar éxito, errores específicos o estado. Incluyendo
ToOkResult(),ToAddedResult(),ToUpdatedResult()yToDeletedResult()para convertir entidades en resultados. - API Funcional:
Match,MatchAsync,Map,MapAsync,Tap,TapAsync,BindyEnsurepara el procesamiento fluido de resultados. Todas las operaciones están disponibles paraResult,ResultEntity<T>,ResultEntities<T>yResultPaged<T>. - Combinar Resultados: Agrega múltiples resultados en uno solo con
Result.Combine()yResult.CombineAsync(). - Parámetros Multivalor:
IParameters.GetParameters()devuelveIEnumerable<KeyValuePair<string, string>>, soportando múltiples valores para la misma clave (ej.,?tag=a&tag=b). - Mejoras en
ErrorCollection:Emptyes ahora una propiedad (instancia nueva y segura). PropiedadCountdirecta con acceso O(1). - Rendimiento: Implementación de alto rendimiento con mínimas asignaciones,
AggressiveInliningy materialización eficiente de colecciones medianteIReadOnlyList<T>.
Instalación
dotnet add package Pitasoft.Result
Inicio rápido
// 1) Resultado simple
Result r1 = Result.Ok();
Result r2 = Result.ValidationError().AddError("Email", "No válido");
// 2) Entidad
ResultEntity<User> u = Result.Ok(new User { Id = 1, Name = "John" });
var (status, user) = u; // deconstrucción
var (s, e) = u; // estado y entidad
var (st, code, errs) = (Result)u; // deconstrucción con código de resultado desde Result base
// 3) Colección paginada
var users = new List<User> { new() { Id = 1, Name = "John" } };
ResultPaged<User> paginado = Result.OkPaged(users, totalCount: 100, page: 1, pageSize: 10);
foreach (var it in paginado) { /* iteración directa */ }
// 4) Automatización Try/Catch (con soporte CancellationToken)
var resultado = Result.Try(() => HacerTrabajo());
var resultadoAsync = await Result.TryAsync(async ct => await HacerTrabajoAsync(ct), cancellationToken);
// 5) Variantes Try para colecciones
ResultEntities<User> entidades = Result.TryEntities(() => ObtenerUsuarios());
ResultEntities<User> entidadesAsync = await Result.TryEntitiesAsync(async () => await ObtenerUsuariosAsync());
ResultPaged<User> resultadoPaginado = await Result.TryPagedAsync(
async () => await ObtenerPaginadoAsync(), page: 1, pageSize: 20);
// 6) Extensiones funcionales (disponibles en todos los tipos de resultado)
var dto = u.Map(x => new UserDto(x!.Id, x.Name));
var okOTira = u.Match(
onSuccess: () => dto,
onFailure: res => throw new InvalidOperationException(res.Status.ToString()));
// MatchAsync en ResultEntities / ResultPaged
var respuesta = await resultadoPaginado.MatchAsync(
onSuccess: async p => await ConstruirDtoPaginado(p),
onFailure: async r => await ManejarError(r));
// 7) Combinar resultados (síncrono y asíncrono)
var combinado = Result.Combine(Result.Ok(), Result.Error().AddError("E", "err"));
var combinadoAsync = await Result.CombineAsync(Tarea1Async(), Tarea2Async());
// 8) IsSuccess / IsFailure como métodos de extensión
if (resultado.IsSuccess()) { /* ... */ }
if (resultado.IsFailure()) { /* ... */ }
// 9) Extensiones semánticas de StatusResult
if (resultado.Status.IsInfrastructureError()) { /* lógica de reintento */ }
if (resultado.Status.IsBusinessError()) { /* devolver 422 */ }
// 10) Marca de tiempo de cálculo
DateTimeOffset? tiempo = resultado.CalculationTime;
CalculationTime se normaliza a UTC al asignarse, por lo que puede tratarse como una marca temporal estable entre sistemas.
Componentes Principales
1. Resultado Simple (Result)
Utilizado para operaciones que no devuelven datos, solo un estado de finalización y errores opcionales.
public Result DeleteUser(int id)
{
if (id <= 0)
return Result.ValidationError().AddError("id", "ID no válido");
var deleted = _repository.Delete(id);
if (!deleted) return Result.NotExists();
return Result.Deleted();
}
// Deconstrucción
var (status, errors) = DeleteUser(1);
if (status == StatusResult.Deleted) { /* ... */ }
2. Resultado con Entidad (ResultEntity<T>)
Utilizado para operaciones que devuelven un único objeto.
public ResultEntity<User> GetUser(int id)
{
var user = _repository.Find(id);
if (user == null) return ResultEntity<User>.NotExists();
return ResultEntity<User>.Ok(user);
}
Conversiones implícitas y API fluida:
public ResultEntity<User> GetUser(int id)
{
var user = _repository.Find(id);
// T se convierte implícitamente a ResultEntity<T>.Ok(entity)
return user ?? (ResultEntity<User>)StatusResult.NoExist;
}
// Deconstrucción
var (status, user) = GetUser(1);
if (status == StatusResult.Ok) { /* ... */ }
// API Fluida
var result = ResultEntity<User>.Ok(user)
.WithCode(200)
.AddError("System", "Servicio saturado");
3. Resultado con Múltiples Entidades (ResultEntities<T>)
Utilizado para listas de resultados. Utiliza IReadOnlyList<T> para la propiedad Entities para garantizar la eficiencia.
Se puede iterar directamente sobre el objeto mediante foreach, ya que implementa IEnumerable<T>.
Cuando se proporciona un IEnumerable<T>, la librería lo materializa a IReadOnlyList<T> cuando es necesario. Esto da indexación estable y evita reenumerar fuentes diferidas.
public ResultEntities<User> GetActiveUsers()
{
IEnumerable<User> users = _repository.GetActive();
// Materializa la colección en IReadOnlyList
return Result.OkEntities(users);
}
Usa TryEntities / TryEntitiesAsync para manejar excepciones automáticamente:
public Task<ResultEntities<User>> GetActiveUsersAsync()
=> Result.TryEntitiesAsync(() => _repository.GetActiveAsync());
4. Resultado Paginado (ResultPaged<T>)
Hereda de ResultEntities<T> e incluye metadatos completos para resultados paginados (TotalCount, Page, PageSize, TotalPages, HasNextPage, HasPreviousPage).
Cuando los metadatos de paginación se proporcionan como un conjunto completo mediante el constructor, Result.OkPaged(...), MaterializePaged(...), ToPaged(...) o WithPaging(...), la librería aplica estas invariantes:
TotalCount >= 0Page >= 1PageSize > 0
Los setters individuales siguen siendo lo bastante permisivos para inicialización progresiva y deserialización JSON, pero las operaciones de paginación completas se validan para evitar estados incoherentes.
Crea resultados paginados fácilmente usando la clase estática Result:
public ResultPaged<User> GetUsers(PagingParameters paging)
{
try
{
var (users, total) = _repository.GetAll(paging);
return Result.OkPaged(users, total, paging.Page, paging.PageSize);
}
catch (Exception ex)
{
return Result.DatabaseErrorPaged<User>(ex);
}
}
// Deconstrucción
var (status, users, total, pagina, tamañoPagina) = result;
Usa TryPagedAsync para manejar excepciones y cancelación automáticamente:
public Task<ResultPaged<User>> GetUsersAsync(PagingParameters paging, CancellationToken ct)
=> Result.TryPagedAsync(
async () => await _repository.GetAllAsync(paging, ct),
paging.Page, paging.PageSize);
5. Resultado por Lote (ResultBatch<T>)
Utilizado para procesar múltiples elementos en una sola petición, proporcionando un estado global y resultados individuales para cada elemento usando IReadOnlyList<ResultEntity<T>>.
Igual que en ResultEntities<T>, las colecciones del lote se materializan a IReadOnlyList<ResultEntity<T>> cuando hace falta.
El Status del lote representa el resultado global de la operación, mientras que cada elemento de Entities conserva su propio estado independiente como ResultEntity<T>.
public ResultBatch<User> ImportUsers(List<User> users)
{
var results = users.Select(u => (ResultEntity<User>)Process(u));
return ResultBatch<User>.Ok(results);
}
// Deconstrucción
var (status, results) = importResult;
// Indicadores agregados
bool hayErroresParciales = importResult.HasPartialErrors;
int totalCorrectos = importResult.SuccessCount;
int totalErrores = importResult.ErrorCount;
Los métodos de factoría estáticos están disponibles para todos los estados comunes:
ResultBatch<T>.Ok(entities)ResultBatch<T>.ValidationError(errors)ResultBatch<T>.DatabaseError(ex)ResultBatch<T>.NotFound()ResultBatch<T>.Forbidden()- ... y todos los demás estados de
StatusResult.
Gestión de Errores
La librería soporta una API fluida para añadir errores, que se almacenan en una ErrorCollection:
return Result.ValidationError()
.AddError("Email", "Formato no válido")
.AddError("Password", "Demasiado corta")
.AddError(new Exception("Error interno del sistema"));
Asocia un código numérico a cualquier resultado con WithCode():
return Result.Error().WithCode(4001);
ErrorCollection expone una propiedad Count para acceso O(1) y Empty siempre devuelve una instancia nueva y segura:
bool tieneErrores = result.Errors.Count > 0;
var erroresVacios = ErrorCollection.Empty; // siempre devuelve una instancia nueva
Códigos de Estado (StatusResult)
| Estado | Categoría | Descripción |
|---|---|---|
Ok, Added, Updated, Deleted |
Éxito | Operaciones exitosas. |
NoExist |
Informativo | El recurso solicitado no fue encontrado (dominio semántico). |
Warning |
Informativo | Completado con problemas no críticos. |
CancelOperation |
Control | La operación fue cancelada. |
ValidationError |
Error | Falló la validación de entrada del cliente. |
DataError, DatabaseError |
Error | Problemas en el procesamiento o persistencia de datos. |
ConcurrencyError |
Error | Los datos fueron modificados por otro proceso. |
ConnectionError |
Error | Falló la conexión a un servicio externo. |
HttpError |
Error | Una llamada HTTP externa devolvió un error. |
Unauthorized |
Seguridad | Falló la autenticación o autorización. |
Forbidden |
Seguridad | El servidor entendió la petición pero rehúsa autorizarla (403). |
NotFound |
Error | El recurso solicitado no fue encontrado (404). |
Conflict |
Error | La petición entra en conflicto con el estado actual del servidor (409). |
UnprocessableEntity |
Error | Errores semánticos en la petición (422). |
TooManyRequests |
Error | Límite de peticiones excedido (429). |
ServiceUnavailable |
Error | El servidor no está listo para manejar la petición (503). |
ChangePassword |
Seguridad | El usuario debe cambiar su contraseña. |
Error, Exception |
Error | Error genérico o una excepción no controlada. |
Guía de Selección de Estados
Usa estas reglas para mantener una semántica consistente entre servicios y APIs:
- Prefiere
NoExistpara flujos internos o de dominio donde la ausencia es esperable y no debe tratarse necesariamente como error. - Prefiere
NotFoundcuando el resultado vaya a mapearse directamente a una respuesta HTTP404. - Usa
ValidationErrorcuando la entrada es inválida antes de comenzar el procesamiento. - Usa
UnprocessableEntitycuando la entrada es estructuralmente válida pero la semántica de negocio impide ejecutar la operación. - Usa
DataErrorpara inconsistencias de dominio o de datos detectadas durante el procesamiento. - Usa
DatabaseError,ConnectionError,HttpErroryServiceUnavailablepara fallos de infraestructura o dependencias externas. - Usa
Conflictcuando la acción solicitada entra en conflicto con el estado actual del recurso. - Usa
Unauthorizedcuando falta autenticación o es inválida. - Usa
Forbiddencuando el llamador está autenticado pero no tiene permisos para ejecutar la acción. - Usa
Errorcomo fallback genérico para fallos conocidos que no encajan en un estado más específico. - Usa
Exceptionsolo cuando el fallo proviene de una excepción inesperada y no exista un estado mejor.
Ejemplos:
// Ausencia de dominio en un servicio interno
return Result.NotExists<User>();
// Respuesta 404 orientada a API
return Result.NotFound<User>();
// Entrada inválida
return Result.ValidationError<User>()
.AddError("Email", "Formato no válido");
// Petición válida, pero una regla de negocio impide continuar
return Result.UnprocessableEntity<User>()
.AddError("State", "El usuario está archivado");
// Error conocido de fallback
return Result.Error().AddError("Order", "No se pudo completar la operación");
// Excepción inesperada
catch (Exception ex)
{
return Result.Exception<User>(ex);
}
Extensiones de StatusResult
Importa Pitasoft.Result.Extensions para usar clasificación semántica sobre valores de StatusResult:
if (resultado.Status.IsSuccess()) { /* Ok / Added / Updated / Deleted */ }
if (resultado.Status.IsInfrastructureError()) { /* DatabaseError / ConcurrencyError / ConnectionError / HttpError / ServiceUnavailable */ }
if (resultado.Status.IsBusinessError()) { /* ValidationError / DataError / NoExist / Unauthorized / Forbidden / NotFound / Conflict / UnprocessableEntity / ChangePassword */ }
if (resultado.Status.IsTransientError()) { /* Error / Exception / CancelOperation / ConcurrencyError / ConnectionError / HttpError / TooManyRequests / ServiceUnavailable */ }
if (resultado.Status.IsError()) { /* Estados de error generales o especializados (Validación, Datos, BD, Concurrencia, HTTP, Excepción, etc.) */ }
Son especialmente útiles en middleware o políticas de reintento para decidir cómo manejar un fallo sin inspeccionar el valor de Status directamente.
Extensiones
Importa Pitasoft.Result.Extensions para usar estos métodos de ayuda en cualquier IResult:
result.IsSuccess(): Devuelvetruesi el estado esOk,Added,UpdatedoDeleted.result.IsError(): Devuelvetruepara la mayoría de los estados relacionados con errores.result.IsFailure(): Devuelvetruepara cualquier estado no exitoso, excluyendoNone,NoExistyWarning. CubreUnauthorized,Exception,CancelOperationy más.result.IsOk(): Devuelvetruesi el estado es exactamenteOk.result.IsWarning(): Devuelvetruesi el estado esWarning.result.IsNotExist(): Devuelvetruesi el recurso no fue encontrado (NoExist).result.IsUnauthorized(): Devuelvetruesi el estado esUnauthorized.result.IsForbidden()/result.IsNotFound()/result.IsConflict()/result.IsTooManyRequests()/result.IsUnprocessableEntity()/result.IsServiceUnavailable(): Comprobaciones semánticas para estados de error tipo HTTP.result.HasErrors(): Devuelvetruesi hay algún error en la colecciónErrors.result.IsSuccessOrNotExist(): Útil para operaciones "Delete" donde ambos casos suelen manejarse de forma similar.result.IsSuccessOrWarning(): Devuelvetruepara estados exitosos o de advertencia.Materialize()/MaterializeAsync(): ConvierteIEnumerable<T>oIAsyncEnumerable<T>aResultEntities<T>oResultPaged<T>.UpdateTimestamp(): Actualiza manualmente elCalculationTimea la hora UTC actual enResulty tipos derivados.ToPaged(): Convierte un resultado aResultPaged<T>preservando el estado. Requiere metadatos de paginación válidos (TotalCount >= 0,Page >= 1,PageSize > 0).ToResultEntity()/ToResultEntities(): Convierte entre tipos de resultado preservando metadatos.ToOkResult()/ToAddedResult()/ToUpdatedResult()/ToDeletedResult(): Convierte una entidad en unResultEntity<T>con el estado de éxito correspondiente.
Los resultados también validan las asignaciones a Status contra los valores definidos en el enum StatusResult. Esto ayuda a detectar pronto asignaciones manuales inválidas al trabajar con instancias mutables o flujos de deserialización personalizados.
API Funcional (Match, Map, Bind, Ensure, EnsureAsync, Recover, Tap, TapAsync)
Importa Pitasoft.Result.Extensions para usar transformaciones de estilo funcional.
Notas de comportamiento:
MatchyTapno modifican el resultado. Solo bifurcan el flujo o ejecutan efectos secundarios.Mapproyecta valores exitosos a un nuevo resultado. En resultados de colección y paginados conservaStatus,Errors,ResultCodeyCalculationTime. EnResultPaged<T>además conserva los metadatos de paginación.Bindcorta la cadena en caso de fallo. En resultados de colección y paginados, la propagación del fallo conserva metadatos; en éxito, el binder pasa a ser dueño del resultado final que devuelve.Ensure/EnsureAsyncsolo se ejecutan sobre resultados exitosos. Si la validación falla, devuelven o producen un error de validación en lugar de continuar el flujo exitoso.Recoverdesempaqueta unResultEntity<T>a unTplano, aportando un valor de respaldo solo cuando el resultado no es exitoso.EnsuresobreResultPaged<T>yResultBatch<T>actualiza la instancia actual aValidationErrory agrega errores, en lugar de crear un nuevo resultado contenedor.
Match — bifurcar según éxito o error
Disponible para Result, ResultEntity<T>, ResultEntities<T> y ResultPaged<T>:
// Result
string msg = resultado.Match(
onSuccess: () => "Completado",
onFailure: r => $"Error: {r.Status}");
// ResultEntity<T>
var dto = userResult.Match(
onSuccess: () => mapper.ToDto(userResult.Entity),
onFailure: r => throw new InvalidOperationException(r.Status.ToString()));
// ResultEntities<T> / ResultPaged<T>
var vista = pagedResult.Match(
onSuccess: () => ConstruirVista(pagedResult),
onFailure: r => VistaError(r));
MatchAsync — bifurcación asíncrona según éxito o error
Disponible para Result, ResultEntity<T>, ResultEntities<T> y ResultPaged<T>:
var respuesta = await pagedResult.MatchAsync(
onSuccess: async p => await ConstruirRespuestaPaginadaAsync(p),
onFailure: async r => await ManejarFalloAsync(r));
Map — transformar la entidad si el resultado es exitoso
Disponible para ResultEntity<T>, ResultEntities<T> y ResultPaged<T>:
ResultEntity<UserDto> dto = userResult.Map(user => mapper.ToDto(user));
ResultEntities<UserDto> dtos = usersResult.Map(user => mapper.ToDto(user));
ResultPaged<UserDto> paged = pagedResult.Map(user => mapper.ToDto(user));
Si el resultado origen no es exitoso, Map no ejecuta la proyección y propaga el estado de fallo.
MapAsync — encadenar transformaciones asíncronas
ResultEntity<UserDto> dto = await userResult.MapAsync(async u => await EnriquecerAsync(u));
ResultEntities<UserDto> dtos = await usersResult.MapAsync(async u => await EnriquecerAsync(u));
ResultPaged<UserDto> paged = await pagedResult.MapAsync(async u => await EnriquecerAsync(u));
Tap — ejecutar una acción sin cambiar el resultado
result.Tap(() => _logger.LogInformation("Operación exitosa"))
.Tap(user => _cache.Set(user));
TapAsync — efecto secundario asíncrono sin cambiar el resultado
Disponible para cualquier tipo de resultado (Result, ResultEntity<T>, ResultEntities<T>, ResultPaged<T>):
var result = await ObtenerUsuarioAsync(id)
.TapAsync(async () => await _auditoría.RegistrarAsync("usuario obtenido"));
// Variante tipada
var result = await ObtenerUsuarioAsync(id)
.TapAsync(async user => await _cache.SetAsync(user));
Bind — encadenar operaciones que devuelven resultados (FlatMap)
Disponible para ResultEntity<T>, ResultEntities<T> y ResultPaged<T>:
ResultEntity<Order> result = GetUser(id)
.Bind(user => CreateOrder(user));
ResultPaged<OrderDto> paged = GetPagedOrders(page, size)
.Bind(o => EnrichOrder(o));
Bind solo invoca el binder cuando el resultado origen es exitoso. Si ya existe un fallo, la cadena se detiene y se propaga ese estado.
Ensure — validar una condición
Sobre Result (base):
Result result = VerificarPermisos(userId)
.Ensure(() => _cuota.TieneCapacidad(), "Cuota superada", "Cuota");
Sobre ResultEntity<T>:
ResultEntity<User> result = GetUser(id)
.Ensure(u => u.Age >= 18, "El usuario debe ser mayor de edad", "Edad");
En Result y ResultEntity<T>, un Ensure fallido devuelve un resultado de validación. En ResultPaged<T> y ResultBatch<T>, un Ensure fallido muta la instancia actual a ValidationError y agrega el error correspondiente.
EnsureAsync — validar una condición asíncrona
Realiza validaciones que requieren I/O, como comprobaciones en base de datos, dentro de la cadena fluida:
ResultEntity<User> result = await GetUser(id)
.EnsureAsync(async u => await _repo.EsEmailUnicoAsync(u.Email), "Email ya existe", "Email");
Recover — proporcionar un valor de respaldo en caso de fallo
Maneja fallos de forma segura proporcionando un valor por defecto o una función de recuperación:
// Valor por defecto simple
User user = GetUser(id).Recover(new User { Name = "Invitado" });
// Función de recuperación
User user = GetUser(id).Recover(r => new User { Name = $"Invitado (Error: {r.Status})" });
Recover es intencionadamente una operación de desempaquetado sobre ResultEntity<T>: después de llamarla, ya trabajas con un valor plano y no con un objeto resultado.
Métodos Seguros ante Excepciones (Try)
Todas las variantes Try capturan excepciones automáticamente. OperationCanceledException se mapea a CancelOperation en lugar de Exception.
Ahora puedes proporcionar opcionalmente una función mapException para transformar excepciones específicas en códigos StatusResult significativos.
| Método | Retorna | Notas |
|---|---|---|
Result.Try(action, deep?, mapEx?) |
Result |
Síncrono, sin valor de retorno |
Result.Try<T>(func, deep?, mapEx?) |
ResultEntity<T> |
Síncrono, devuelve entidad |
Result.TryAsync(action, ct?, deep?, mapEx?) |
Task<Result> |
Asíncrono, soporta CancellationToken |
Result.TryAsync<T>(func, ct?, deep?, mapEx?) |
Task<ResultEntity<T>> |
Entidad asíncrona, soporta CancellationToken |
Result.TryEntities<T>(func, deep?, mapEx?) |
ResultEntities<T> |
Colección síncrona |
Result.TryEntitiesAsync<T>(func, deep?, mapEx?) |
Task<ResultEntities<T>> |
Colección asíncrona |
Result.TryPagedAsync<T>(func, page, pageSize, deep?, mapEx?) |
Task<ResultPaged<T>> |
Colección paginada asíncrona |
// Mapeo automático de excepciones a códigos de estado
var result = Result.Try(() => _repo.Get(id),
mapException: ex => ex is KeyNotFoundException ? StatusResult.NotFound : StatusResult.Error);
// Soporte CancellationToken — OperationCanceledException → CancelOperation
var resultado = await Result.TryAsync(
async ct => await _servicio.ProcesarAsync(ct),
cancellationToken);
// Colección
ResultEntities<Product> productos = Result.TryEntities(() => _repo.ObtenerTodos());
// Paginado
ResultPaged<Product> pagina = await Result.TryPagedAsync(
async () => await _repo.ObtenerPaginadoAsync(1, 20),
page: 1, pageSize: 20);
Combinar Resultados
Agrega múltiples instancias de Result en una sola. El resultado combinado es Ok si todos tienen éxito; en caso contrario acumula todos los errores de los fallidos.
// Síncrono
Result combinado = Result.Combine(
ValidarNombre(dto.Name),
ValidarEmail(dto.Email),
ValidarEdad(dto.Age));
// Asíncrono
Result combinado = await Result.CombineAsync(
ValidarNombreAsync(dto.Name),
ValidarEmailAsync(dto.Email));
Parámetros de Paginación y Entidad
Usa PagingParameters para recibir la entrada de paginación y EntityParameters para filtrado, búsqueda y ordenación.
GetParameters() devuelve IEnumerable<KeyValuePair<string, string>>, lo que permite múltiples valores para la misma clave (útil para query strings tipo array como ?tag=a&tag=b):
public ResultPaged<User> GetUsers(EntityParameters parametros)
{
// parametros.Page, parametros.PageSize, parametros.Search, parametros.Query, parametros.Order, parametros.Attrs
var (users, total) = _repository.GetAll(parametros);
return Result.OkPaged(users, total, parametros.Page, parametros.PageSize);
}
// Convertir a Dictionary cuando un mapa de valor único es suficiente
var dict = parametros.GetParameters().ToDictionary(kv => kv.Key, kv => kv.Value);
// O usar directamente con constructores de query de HttpClient que acepten IEnumerable<KVP>
var query = QueryString.Create(parametros.GetParameters());
Helper de Lote (Batch<T>)
Usa Batch<T> para describir un conjunto de entidades y la acción a realizar sobre ellas:
var batch = Batch<User>.Append(newUsers);
var batch = Batch<User>.Update(modifiedUsers);
var batch = Batch<User>.Delete(removedUsers);
Rendimiento
Consejos de rendimiento
- Factorías rápidas: Los estados comunes de
ResultcomoNotExists()yValidationError()utilizan helpers de factoría pequeños e inlineados, manteniendo bajo el coste de creación sin cambiar la semántica mutable del resultado. - Optimización de Parámetros:
PagingParameters.GetParameters()yEntityParameters.GetParameters()ahora utilizanyield returnen lugar de crear listas temporales, mejorando la eficiencia de memoria de $O(n)$ a $O(1)$ de espacio. - Eficiencia en Colecciones:
ResultEntities<T>yResultPaged<T>utilizanIReadOnlyList<T>para la propiedadEntities. Las colecciones se materializan eficientemente en una lista solo cuando es necesario. - Aggressive Inlining: Las factorías estáticas y los métodos de extensión están anotados con
AggressiveInliningpara minimizar el overhead. - Iteración Directa: Soportada mediante
foreach. Internamente, su enumerador delega en la colección subyacente sin usaryield, evitando asignaciones extra de la máquina de estados. - Carga JSON:
ResultPaged<T>ignora automáticamente las propiedades calculadas comoTotalPages,HasNextPageyHasPreviousPagedurante la serialización para reducir el tamaño del payload. - Comprobaciones de estado:
IsSuccess(),IsFailure()y el resto de extensiones semánticas están anotadas conAggressiveInliningpara minimizar el overhead en rutas de código críticas. - Gestión de Excepciones: Al crear resultados de error desde excepciones manualmente, usa
deep: falsesalvo que necesites explícitamente el detalle de inner exceptions para reducir asignaciones enErrorCollection.
Resultados de benchmarks para operaciones principales (Tiempo medio / Memoria asignada):
La librería está optimizada para minimizar asignaciones y maximizar el rendimiento. La suite actual de Pitasoft.Result.Benchmarks ya incluye también los escenarios nuevos de API funcional y paginación.
Lectura rápida:
.NET 10lidera las rutas representativas de creación y API funcional en esta batería, especialmente en paginación y transformaciones deResultBatch<T>..NET 8sigue siendo muy competitivo y ahora mismo ofrece los mejores números de serialización conSystem.Text.Jsonen esta máquina, además de seguir siendo la base LTS..NET 9funciona correctamente y suele quedar entre ambos runtimes en la mayoría de escenarios, pero no es el claro ganador en este perfil.
| Operación | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Result.Ok |
18.38 ns / 64 B | 19.22 ns / 64 B | 19.91 ns / 64 B |
Result.Error(Exception) |
44.12 ns / 376 B | 48.85 ns / 376 B | 42.78 ns / 376 B |
ResultEntities.Ok |
23.64 ns / 72 B | 24.28 ns / 72 B | 20.32 ns / 72 B |
Result.OkPaged |
24.35 ns / 88 B | 25.28 ns / 88 B | 19.65 ns / 88 B |
ResultBatch.Ok(entities) |
24.17 ns / 72 B | 24.78 ns / 72 B | 23.03 ns / 72 B |
ResultEntity.Map |
25.57 ns / 72 B | 23.69 ns / 72 B | 20.92 ns / 72 B |
ResultPaged.WithPaging |
19.15 ns / 88 B | 19.93 ns / 88 B | 17.07 ns / 88 B |
ResultPaged.Bind (ruta de fallo) |
22.97 ns / 88 B | 24.58 ns / 88 B | 20.73 ns / 88 B |
ResultBatch.Map (ruta de fallo) |
24.00 ns / 96 B | 25.22 ns / 96 B | 22.51 ns / 96 B |
ResultBatch.Bind (ruta de éxito) |
107.04 ns / 416 B | 112.56 ns / 416 B | 100.61 ns / 416 B |
ResultBatch.Ensure (ruta de fallo) |
92.90 ns / 568 B | 95.04 ns / 568 B | 81.35 ns / 568 B |
Metodología:
- BenchmarkDotNet
v0.15.8 InProcessNoEmitToolchain- Apple M5 / macOS Tahoe 26.3.1 / Arm64
- Los reportes detallados se generan en BenchmarkDotNet.Artifacts/results
Para regenerar la suite representativa de benchmarks y guardar snapshots separados para .NET 8, .NET 9 y .NET 10, ejecuta:
./scripts/run-benchmarks.sh
También puedes limitar la ejecución a runtimes concretos:
./scripts/run-benchmarks.sh net10.0
El script guarda los reportes por runtime en BenchmarkDotNet.Artifacts/snapshots/<framework> y actualiza automáticamente las tablas de benchmarks de este README a partir de esos snapshots.
Si ya tienes snapshots recientes y solo quieres reconstruir las tablas, ejecuta:
./scripts/update-benchmark-readme.sh
Benchmarks representativos de serialización JSON (System.Text.Json):
| Operación | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Serialize Result |
216.8 ns / 656 B | 220.6 ns / 656 B | 197.1 ns / 656 B |
Deserialize Result |
481.0 ns / 1344 B | 484.6 ns / 1320 B | 500.5 ns / 1320 B |
Serialize ResultEntities |
411.1 ns / 944 B | 404.8 ns / 944 B | 431.5 ns / 944 B |
Deserialize ResultEntities |
1,039.9 ns / 1152 B | 1,019.7 ns / 1152 B | 1,128.0 ns / 1152 B |
Serialize ResultPaged |
471.2 ns / 1144 B | 470.7 ns / 1144 B | 496.1 ns / 1144 B |
Deserialize ResultPaged |
1,312.0 ns / 1376 B | 1,267.2 ns / 1376 B | 1,414.9 ns / 1376 B |
Microbenchmarks de comprobaciones de estado y resultado:
Estos valores son utiles como senal relativa, no como promesa absoluta. Cuando BenchmarkDotNet muestra 0.0000 ns en checks tan pequenos, significa que el runtime optimizo esa ruta tanto que su coste resulta indistinguible del baseline de un metodo vacio en esta configuracion.
| Operación | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
Comprobacion directa de exito en Status |
0.2312 ns / 0 B | 0.5399 ns / 0 B | 0.0175 ns / 0 B |
Status.IsSuccess() |
0.4051 ns / 0 B | 0.1336 ns / 0 B | 0.0000 ns / 0 B |
Status.IsFailure() |
0.2283 ns / 0 B | 0.0000 ns / 0 B | 0.1625 ns / 0 B |
IResult.IsSuccess() |
0.3246 ns / 0 B | 0.0000 ns / 0 B | 0.2668 ns / 0 B |
IResult.IsFailure() |
0.2608 ns / 0 B | 0.0740 ns / 0 B | 0.2546 ns / 0 B |
IResult.HasErrors() sin errores |
0.2572 ns / 0 B | 0.0000 ns / 0 B | 0.0411 ns / 0 B |
IResult.HasErrors() con errores |
0.5401 ns / 0 B | 0.0000 ns / 0 B | 0.3177 ns / 0 B |
Status.IsError() |
0.2573 ns / 0 B | 0.0000 ns / 0 B | 0.2570 ns / 0 B |
Autor
Sebastián Martínez Pérez
License
Copyright © 2019-2026 Pitasoft, S.L. Licensed under the LICENSE.txt provided in this repository.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Pitasoft.Error (>= 5.3.14)
-
net8.0
- Pitasoft.Error (>= 5.3.14)
-
net9.0
- Pitasoft.Error (>= 5.3.14)
NuGet packages (9)
Showing the top 5 NuGet packages that depend on Pitasoft.Result:
| Package | Downloads |
|---|---|
|
Pitasoft.Client
.NET library designed to simplify the consumption of RESTful services. It provides a robust base class and helpers to handle HTTP requests, JSON serialization, and common API patterns. |
|
|
Pitasoft.Web
Librerias basicas de aplicaciones web |
|
|
Pitasoft.Blazor.Result
Application of the functionalities of the Pitasoft.Blazor package, adding functionalities of Pitasoft.Result. |
|
|
Pitasoft.Mail
E-Mail server service. |
|
|
Pitasoft.Result.AspNetCore
ASP.NET Core integration for Pitasoft.Result. Provides HTTP result mapping, JSON helpers, DI-based configuration, and exception handling for MVC and Minimal APIs. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 7.4.1 | 159 | 4/19/2026 |
| 7.3.2 | 220 | 4/3/2026 |
| 7.3.1 | 359 | 3/26/2026 |
| 7.2.9 | 162 | 3/24/2026 |
| 7.2.8 | 140 | 3/23/2026 |
| 7.2.7 | 142 | 3/22/2026 |
| 7.2.6 | 172 | 3/20/2026 |
| 7.2.5 | 144 | 3/20/2026 |
| 7.2.4 | 208 | 3/13/2026 |
| 7.2.3 | 145 | 3/11/2026 |
| 7.2.2 | 166 | 3/10/2026 |
| 7.2.1 | 157 | 3/9/2026 |
| 7.1.4 | 196 | 3/2/2026 |
| 7.1.3 | 183 | 2/24/2026 |
| 7.1.2 | 150 | 2/24/2026 |
| 7.1.1 | 191 | 2/23/2026 |
| 7.0.2 | 176 | 2/12/2026 |
| 7.0.1 | 203 | 1/26/2026 |
| 6.5.7 | 393 | 5/27/2025 |
| 6.5.6 | 361 | 5/27/2025 |