Pitasoft.Result
7.2.2
See the version list below for details.
dotnet add package Pitasoft.Result --version 7.2.2
NuGet\Install-Package Pitasoft.Result -Version 7.2.2
<PackageReference Include="Pitasoft.Result" Version="7.2.2" />
<PackageVersion Include="Pitasoft.Result" Version="7.2.2" />
<PackageReference Include="Pitasoft.Result" />
paket add Pitasoft.Result --version 7.2.2
#r "nuget: Pitasoft.Result, 7.2.2"
#:package Pitasoft.Result@7.2.2
#addin nuget:?package=Pitasoft.Result&version=7.2.2
#tool nuget:?package=Pitasoft.Result&version=7.2.2
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;. IsSuccess/IsFailureProperties: Available directly on every result instance (result.IsSuccess,result.IsFailure) without needing extension methods.- 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, code, errs) = u; // deconstruction with result code
// 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 properties
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;
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>.
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).
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>>.
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;
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. |
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/result.IsFailure: Properties available directly on every result instance — no extension call needed.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.ToPaged(): Convert a result to aResultPaged<T>preserving state.ToResultEntity()/ToResultEntities(): Convert between result types preserving metadata.ToOkResult()/ToAddedResult()/ToUpdatedResult()/ToDeletedResult(): Convert an entity to aResultEntity<T>with the corresponding success status.
Functional API (Match, Map, Bind, Ensure, Tap, TapAsync)
Import Pitasoft.Result.Extensions to use functional-style transformations.
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));
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));
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");
Exception-safe Factory Methods (Try)
All Try variants automatically catch exceptions. OperationCanceledException is mapped to CancelOperation instead of Exception.
| Method | Returns | Notes |
|---|---|---|
Result.Try(action) |
Result |
Sync, no return value |
Result.Try<T>(func) |
ResultEntity<T> |
Sync, returns entity |
Result.TryAsync(action, ct?) |
Task<Result> |
Async, supports CancellationToken |
Result.TryAsync<T>(func, ct?) |
Task<ResultEntity<T>> |
Async entity, supports CancellationToken |
Result.TryEntities<T>(func) |
ResultEntities<T> |
Sync collection |
Result.TryEntitiesAsync<T>(func) |
Task<ResultEntities<T>> |
Async collection |
Result.TryPagedAsync<T>(func, page, pageSize) |
Task<ResultPaged<T>> |
Async paged collection |
// 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
- Prefer using the provided static factories (e.g.,
Result.Ok(),Result.Added()) or the explicit extension methods (e.g.,entity.ToOkResult()). They are very small and are annotated withAggressiveInliningto minimize overhead. ResultEntities<T>andResultPaged<T>useIReadOnlyList<T>for theEntitiesproperty. Collections are materialized efficiently into a list only when necessary.- Direct iteration is supported via
foreach. Internally, its enumerator delegates to the underlying collection without usingyield, avoiding extra state-machine allocations. - When creating error results from exceptions, pass
deep: falseunless you explicitly need inner exception details. This reduces allocations in theErrorCollection. - Use
Result.TryandResult.TryAsyncto simplify your code while ensuring all exceptions are caught and wrapped in aResultorResultEntity<T>. - For JSON payload size,
ResultPaged<T>automatically ignores calculated properties likeTotalPages,HasNextPage, andHasPreviousPageduring serialization. - Use
result.IsSuccess/result.IsFailureproperties instead of extension method calls in hot paths — they are simple property reads with no method dispatch overhead.
Benchmark results for core operations (Mean time / Allocated memory):
The library is optimized to minimize allocations and maximize throughput. Benchmark results for core operations (Mean time / Allocated memory):
| Operation | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
| Result.Ok | 17.0 ns / 64 B | 17.1 ns / 64 B | 17.5 ns / 64 B |
| ResultEntity.Ok | 17.7 ns / 72 B | 17.1 ns / 72 B | 18.3 ns / 72 B |
| ResultEntities.Ok | 17.2 ns / 80 B | 17.3 ns / 80 B | 17.7 ns / 80 B |
| Error (with Exception) | 43.2 ns / 408 B | 48.3 ns / 408 B | 37.9 ns / 408 B |
Note: Benchmarks performed on Apple M-series (Arm64).
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;. - Propiedades
IsSuccess/IsFailure: Disponibles directamente en cada instancia de resultado (result.IsSuccess,result.IsFailure) sin necesidad de métodos de extensión. - 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, code, errs) = u; // deconstrucción con código de resultado
// 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 propiedades
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;
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>.
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).
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>>.
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;
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. |
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/result.IsFailure: Propiedades disponibles directamente en cada instancia de resultado, sin necesidad de llamada a extensión.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.ToPaged(): Convierte un resultado aResultPaged<T>preservando el estado.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.
API Funcional (Match, Map, Bind, Ensure, Tap, TapAsync)
Importa Pitasoft.Result.Extensions para usar transformaciones de estilo funcional.
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));
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));
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");
Métodos Seguros ante Excepciones (Try)
Todas las variantes Try capturan excepciones automáticamente. OperationCanceledException se mapea a CancelOperation en lugar de Exception.
| Método | Retorna | Notas |
|---|---|---|
Result.Try(action) |
Result |
Síncrono, sin valor de retorno |
Result.Try<T>(func) |
ResultEntity<T> |
Síncrono, devuelve entidad |
Result.TryAsync(action, ct?) |
Task<Result> |
Asíncrono, soporta CancellationToken |
Result.TryAsync<T>(func, ct?) |
Task<ResultEntity<T>> |
Entidad asíncrona, soporta CancellationToken |
Result.TryEntities<T>(func) |
ResultEntities<T> |
Colección síncrona |
Result.TryEntitiesAsync<T>(func) |
Task<ResultEntities<T>> |
Colección asíncrona |
Result.TryPagedAsync<T>(func, page, pageSize) |
Task<ResultPaged<T>> |
Colección paginada asíncrona |
// 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
- Prefiere las factorías estáticas provistas (por ejemplo,
Result.Ok(),Result.Added()) o los métodos de extensión explícitos (por ejemplo,entity.ToOkResult()). Son muy pequeñas y están anotadas conAggressiveInliningpara minimizar el overhead. ResultEntities<T>yResultPaged<T>utilizanIReadOnlyList<T>para la propiedadEntities. Las colecciones se materializan eficientemente en una lista solo cuando es necesario.- La iteración directa con
foreachestá soportada. Internamente, su enumerador delega en la colección subyacente sin usaryield, evitando asignaciones extra de la máquina de estados. - Al crear resultados de error desde excepciones, usa
deep: falsesalvo que necesites explícitamente el detalle de inner exceptions. Esto reduce asignaciones enErrorCollection. - Usa
Result.TryyResult.TryAsyncpara simplificar tu código asegurando que todas las excepciones sean capturadas y envueltas en unResultoResultEntity<T>. - Para el tamaño del payload JSON,
ResultPaged<T>ignora automáticamente las propiedades calculadas comoTotalPages,HasNextPageyHasPreviousPagedurante la serialización. - Usa las propiedades
result.IsSuccess/result.IsFailureen lugar de llamadas a métodos de extensión en rutas de código críticas — son lecturas de propiedad directas sin sobrecarga de despacho de métodos.
Resultados de benchmarks para operaciones principales (Tiempo medio / Memoria asignada):
La librería está optimizada para minimizar asignaciones y maximizar el rendimiento. Resultados de benchmarks para operaciones principales (Tiempo medio / Memoria asignada):
| Operación | .NET 8 | .NET 9 | .NET 10 |
|---|---|---|---|
| Result.Ok | 17.0 ns / 64 B | 17.1 ns / 64 B | 17.5 ns / 64 B |
| ResultEntity.Ok | 17.7 ns / 72 B | 17.1 ns / 72 B | 18.3 ns / 72 B |
| ResultEntities.Ok | 17.2 ns / 80 B | 17.3 ns / 80 B | 17.7 ns / 80 B |
| Error (con Excepción) | 43.2 ns / 408 B | 48.3 ns / 408 B | 37.9 ns / 408 B |
Nota: Benchmarks realizados en Apple M-series (Arm64).
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.6)
-
net8.0
- Pitasoft.Error (>= 5.3.6)
-
net9.0
- Pitasoft.Error (>= 5.3.6)
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 | 157 | 4/19/2026 |
| 7.3.2 | 218 | 4/3/2026 |
| 7.3.1 | 357 | 3/26/2026 |
| 7.2.9 | 160 | 3/24/2026 |
| 7.2.8 | 138 | 3/23/2026 |
| 7.2.7 | 140 | 3/22/2026 |
| 7.2.6 | 170 | 3/20/2026 |
| 7.2.5 | 142 | 3/20/2026 |
| 7.2.4 | 206 | 3/13/2026 |
| 7.2.3 | 143 | 3/11/2026 |
| 7.2.2 | 164 | 3/10/2026 |
| 7.2.1 | 155 | 3/9/2026 |
| 7.1.4 | 194 | 3/2/2026 |
| 7.1.3 | 181 | 2/24/2026 |
| 7.1.2 | 148 | 2/24/2026 |
| 7.1.1 | 189 | 2/23/2026 |
| 7.0.2 | 174 | 2/12/2026 |
| 7.0.1 | 201 | 1/26/2026 |
| 6.5.7 | 390 | 5/27/2025 |
| 6.5.6 | 359 | 5/27/2025 |