Pitasoft.Result.AspNetCore
2.1.2
dotnet add package Pitasoft.Result.AspNetCore --version 2.1.2
NuGet\Install-Package Pitasoft.Result.AspNetCore -Version 2.1.2
<PackageReference Include="Pitasoft.Result.AspNetCore" Version="2.1.2" />
<PackageVersion Include="Pitasoft.Result.AspNetCore" Version="2.1.2" />
<PackageReference Include="Pitasoft.Result.AspNetCore" />
paket add Pitasoft.Result.AspNetCore --version 2.1.2
#r "nuget: Pitasoft.Result.AspNetCore, 2.1.2"
#:package Pitasoft.Result.AspNetCore@2.1.2
#addin nuget:?package=Pitasoft.Result.AspNetCore&version=2.1.2
#tool nuget:?package=Pitasoft.Result.AspNetCore&version=2.1.2
Pitasoft.Result.AspNetCore
English
Pitasoft.Result.AspNetCore adapts Pitasoft.Result contracts to ASP.NET Core so the same domain/application result can be returned consistently from:
- MVC / Web API controllers through
IActionResult - Minimal APIs through
Microsoft.AspNetCore.Http.IResult - .NET clients such as
Pitasoft.Client, preserving payload shape for entities and collections inAvalonia UI,MAUI,WPF, andBlazor
This package is especially useful when your application services already return Result, ResultEntity<T>, ResultEntities<T> or ResultPaged<T> and you want a thin HTTP boundary instead of duplicating status mapping in every endpoint.
Installation
dotnet add package Pitasoft.Result.AspNetCore
Quick start
Minimal API:
using Pitasoft.Result;
using Pitasoft.Result.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResultAspNetCore();
var app = builder.Build();
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
{
var result = service.GetById(id);
return result.ToHttpResult();
});
app.Run();
MVC / Web API:
[HttpGet("{id:int}")]
public IActionResult GetProduct(int id)
{
var result = _service.GetById(id);
return result.ToActionResult();
}
This is the recommended starting point:
- register
AddResultAspNetCore()once - return
Pitasoft.Resultcontracts from the application layer - use
ToActionResult()orToHttpResult()at the HTTP boundary
Configuration precedence
When more than one configuration source is available, the adapter applies them in this order:
- explicit per-call overrides such as
result.ToHttpResult(customMapper, customOptions) - global DI configuration registered through
AddResultAspNetCore(...) - built-in defaults from
ResultHttpStatusCodesandResultJsonSerializerOptions
What it provides
ToActionResult()- converts a
Pitasoft.Result.IResultinto an ASP.NET CoreIActionResult
- converts a
ToHttpResult()- converts a
Pitasoft.Result.IResultinto an ASP.NET Core Minimal API result
- converts a
AddResultAspNetCore(...)- registers a global adapter configuration through dependency injection for status mapping and JSON serialization
AddResultExceptionHandling()- registers a global exception handler that maps unhandled exceptions to
Pitasoft.Resultcontracts; returns anIResultExceptionHandlingBuilderso additional mappers can be chained via.AddExceptionMapper<T>()
- registers a global exception handler that maps unhandled exceptions to
IResultExceptionMapper- implement this interface to plug a custom mapper into the chain;
CanMap(exception)controls whether your mapper handles a given exception type
- implement this interface to plug a custom mapper into the chain;
ResultJsonSerializerOptions- creates or enriches
JsonSerializerOptionssoPitasoft.Resultentity, collection, paged, and structured-error payloads serialize and deserialize correctly
- creates or enriches
ResultHttpStatusCodes- exposes the default
StatusResult -> HTTPmapping and can be reused as a customization baseline
- exposes the default
ToActionResultAsync()/ToHttpResultAsync()- async variants for
Task<IResult>that eliminate theawaitboilerplate at the HTTP boundary
- async variants for
AddResultFilter()onIMvcBuilder- registers
ResultActionFilterglobally so MVC actions can returnIResultdirectly without callingToActionResult()
- registers
AddResultFilter()onRouteHandlerBuilder/RouteGroupBuilder- adds
ResultEndpointFilterto Minimal API endpoints or groups so handlers can returnIResultdirectly without callingToHttpResult()
- adds
[ProducesResult(...)]attribute +AddResultDocumentation()- declares the
StatusResultvalues an MVC action or controller can return and automatically registers the corresponding HTTP status codes asProducesResponseTypeentries in the OpenAPI description
- declares the
WithResultDocumentation(...)onRouteHandlerBuilder/RouteGroupBuilder- registers the declared
StatusResultvalues as HTTP response type metadata on Minimal API endpoints so they appear in OpenAPI / Swagger documentation
- registers the declared
AddResultValidationFilter()- replaces ASP.NET Core's default
ModelStateInvalidFilterso that invalid model state produces aPitasoft.ResultValidationErrorresponse instead ofValidationProblemDetails, keeping the error contract consistent
- replaces ASP.NET Core's default
SuppressModelStateValidation()- suppresses the built-in
ModelStateInvalidFilterwithout adding any replacement — use this when validation runs inside action bodies viaPitasoft.ValidationorPitasoft.FluentValidation
- suppresses the built-in
QueryParameters,EntityParameters, andPagingParameters- Minimal API bindable parameter objects with
BindAsync(...)implementations for query/filter/search/sort, paging-only, or combined scenarios
- Minimal API bindable parameter objects with
HttpContextQueryExtensions- helper methods such as
TryGetQueryValue<T>(),GetQueryValue<T>(), andGetQueryValueOrDefault<T>()to simplify typed query-string access when implementing custom Minimal API parameter binding
- helper methods such as
Why it matters
Without a shared adapter, it is easy for ASP.NET Core endpoints to drift from the semantics already expressed by StatusResult.
Examples of drift this package helps avoid:
- returning
200 OKfor every result, even when the domain saysNotFoundorConflict - losing
NoExistvsNotFoundintent at the HTTP boundary - breaking collection payloads consumed by
Pitasoft.Client - reimplementing the same
switchoverStatusResultin many controllers or endpoints
Default HTTP mapping
ToHttpResult() and ToActionResult() map StatusResult to HTTP response codes using the following default policy:
StatusResult |
HTTP |
|---|---|
Ok, Warning, NoExist |
200 OK |
Added |
201 Created |
Updated |
200 OK |
Deleted |
204 No Content by default, 200 OK when the result carries a payload |
ValidationError |
400 Bad Request |
Unauthorized, ChangePassword |
401 Unauthorized |
Forbidden |
403 Forbidden |
NotFound |
404 Not Found |
Conflict, ConcurrencyError |
409 Conflict |
UnprocessableEntity |
422 Unprocessable Entity |
TooManyRequests |
429 Too Many Requests |
ServiceUnavailable, ConnectionError, HttpError |
503 Service Unavailable |
None, CancelOperation, DataError, DatabaseError, Error, Exception |
500 Internal Server Error |
Important semantic note
NoExist is intentionally mapped to 200 OK by default.
That is not an accident. In the Pitasoft result model, NoExist usually means:
- the query completed correctly
- the requested data does not exist
- the outcome is still functional, not an HTTP transport failure
If you need explicit HTTP 404 semantics, return StatusResult.NotFound instead.
Deleted also deserves a note:
- plain
Result.Deleted()maps to204 No Content Result.Deleted(entity)orResult.DeletedEntities(...)maps to200 OKso the payload can be preserved
Global configuration with DI
If you want to change the default behavior once for the whole application, register the adapter in IServiceCollection.
using Microsoft.AspNetCore.Http;
using Pitasoft.Result.AspNetCore;
builder.Services.AddResultAspNetCore(options =>
{
options.StatusCodeMapper = status =>
status == StatusResult.NoExist
? StatusCodes.Status404NotFound
: ResultHttpStatusCodes.Get(status);
options.SerializerOptions = ResultJsonSerializerOptions.Create(o =>
{
o.PropertyNamingPolicy = null;
});
});
Once registered, plain calls such as result.ToActionResult() and result.ToHttpResult() will use the configured mapping and serializer options automatically when executed inside ASP.NET Core.
Global exception handling
If your application throws unhandled exceptions, you can convert them to Pitasoft.Result responses with:
builder.Services.AddResultAspNetCore();
builder.Services.AddResultExceptionHandling();
var app = builder.Build();
app.UseExceptionHandler(_ => { });
Default exception mapping:
ValidationException→ValidationErrorUnauthorizedAccessException→UnauthorizedKeyNotFoundException→NotFoundOperationCanceledException→CancelOperation- any other exception →
Exception
Exception mapper chain
AddResultExceptionHandling() returns an IResultExceptionHandlingBuilder. Call .AddExceptionMapper<T>() to register additional mappers before the built-in fallback:
builder.Services.AddResultExceptionHandling()
.AddExceptionMapper<DbExceptionMapper>()
.AddExceptionMapper<HttpClientExceptionMapper>();
Mappers are evaluated in registration order. The first whose CanMap(exception) returns true handles the exception. DefaultResultExceptionMapper is always the final fallback and handles any exception not matched by an earlier mapper.
Implement IResultExceptionMapper to create a custom mapper:
public sealed class DbExceptionMapper : IResultExceptionMapper
{
public bool CanMap(Exception exception) =>
exception is TimeoutException;
public IResult Map(Exception exception) =>
new Result(StatusResult.ServiceUnavailable);
}
Register it before UseExceptionHandler:
builder.Services.AddResultExceptionHandling()
.AddExceptionMapper<DbExceptionMapper>();
Host styles
Pitasoft.Result.AspNetCore is designed to support both common ASP.NET Core styles without changing your application-layer result contracts:
ToHttpResult()for Minimal APIsToActionResult()for MVC / Web API controllersAddResultFilter()for automatic conversion without an explicit call in either styleAddResultExceptionHandling()for shared exception-to-result behavior in either style[ProducesResult]/WithResultDocumentation()for OpenAPI documentation in either styleAddResultValidationFilter()/SuppressModelStateValidation()for unified validation error contracts in MVC
Automatic result conversion
Instead of calling ToActionResult() or ToHttpResult() at every endpoint, register a filter once and let handlers return IResult directly.
MVC / Web API — ResultActionFilter
Register globally via AddResultFilter() after AddControllers():
builder.Services.AddControllers().AddResultFilter();
Actions then return IResult directly — the same three-level configuration precedence applies:
[HttpGet("{id:int}")]
public IResult GetProduct(int id) => _service.GetById(id);
[HttpPost]
public Task<IResult> CreateProduct([FromBody] CreateRequest request) =>
_service.CreateAsync(request);
Minimal APIs — ResultEndpointFilter
Apply per-endpoint, per-group, or globally:
// Per-endpoint
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
service.GetById(id)).AddResultFilter();
// Per-group
var api = app.MapGroup("/api").AddResultFilter();
api.MapGet("/products/{id:int}", (int id, IProductService service) => service.GetById(id));
// Global — all endpoints under the root
var api = app.MapGroup("").AddResultFilter();
Return values that are not Pitasoft.Result.IResult — such as strings or TypedResults — are passed through unchanged.
OpenAPI documentation
Declare the StatusResult values each endpoint can return and let the package register the corresponding HTTP status codes automatically in the OpenAPI description.
MVC / Web API — [ProducesResult] attribute
Enable the convention once in Program.cs:
builder.Services.AddControllers()
.AddResultFilter()
.AddResultDocumentation();
Then annotate each action or controller with [ProducesResult]:
[HttpGet("{id:int}")]
[ProducesResult(StatusResult.Ok, StatusResult.NoExist)]
public IResult GetProduct(int id) => _service.GetById(id);
[HttpPost]
[ProducesResult(StatusResult.Added, StatusResult.ValidationError)]
public IResult CreateProduct([FromBody] CreateRequest request) =>
_service.Create(request);
When applied to a controller class the declared statuses are inherited by all actions that do not carry their own [ProducesResult] attribute.
Duplicate HTTP codes — for example StatusResult.Ok and StatusResult.NoExist both map to 200 — are deduplicated automatically.
Minimal APIs — WithResultDocumentation()
Chain WithResultDocumentation() after the endpoint mapping:
api.MapGet("/products/{id:int}", (int id, IProductService service) =>
service.GetById(id))
.WithResultDocumentation(StatusResult.Ok, StatusResult.NoExist);
api.MapPost("/products", (CreateRequest request, IProductService service) =>
service.Create(request))
.WithResultDocumentation(StatusResult.Added, StatusResult.ValidationError);
Apply to a whole group to share documentation across endpoints:
var api = app.MapGroup("/api")
.AddResultFilter()
.WithResultDocumentation(StatusResult.Ok, StatusResult.NotFound);
ModelState validation
By default, ASP.NET Core's [ApiController] attribute intercepts invalid model state before the action executes and returns a ValidationProblemDetails payload (application/problem+json). This format differs from the Pitasoft.Result contract used everywhere else.
AddResultValidationFilter() replaces that behavior so validation errors are returned as a standard ValidationError result:
builder.Services.AddControllers()
.AddResultFilter()
.AddResultDocumentation()
.AddResultValidationFilter(); // replaces ModelStateInvalidFilter
A POST /products/validate with a missing Name field will now return:
{
"status": 7,
"errors": {
"Name": ["The Name field is required."]
}
}
instead of the default ValidationProblemDetails shape.
When validation runs inside the action — SuppressModelStateValidation()
When you use Pitasoft.Validation, Pitasoft.FluentValidation, or any other library that validates inside the action body, the built-in ModelStateInvalidFilter would still short-circuit the request before the action can validate. Call SuppressModelStateValidation() to disable it without registering ResultValidationFilter:
builder.Services.AddControllers()
.AddResultFilter()
.SuppressModelStateValidation(); // disables built-in filter, validation runs in action body
Async extension methods
When application services return Task<IResult>, chain the conversion without an intermediate await:
// MVC / Web API
[HttpGet("{id:int}")]
public Task<IActionResult> GetProduct(int id) =>
_service.GetByIdAsync(id).ToActionResultAsync();
// Minimal API
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
service.GetByIdAsync(id).ToHttpResultAsync());
All overloads from the synchronous methods are available in async form:
| Method | Variants |
|---|---|
ToActionResultAsync() |
default, custom mapper, IServiceProvider, HttpContext |
ToHttpResultAsync() |
default, JsonSerializerOptions, custom mapper, IServiceProvider, HttpContext |
Usage in MVC / Web API
using Microsoft.AspNetCore.Mvc;
using Pitasoft.Result.AspNetCore;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id:int}")]
public IActionResult GetUser(int id)
{
var result = _userService.GetUserById(id);
return result.ToActionResult();
}
}
Practical MVC example
[HttpPut("{id:int}")]
public IActionResult UpdateUser(int id, UpdateUserRequest request)
{
var result = _userService.UpdateUser(id, request);
// The HTTP code is chosen from result.Status:
// Updated -> 200
// ValidationError -> 400
// NotFound -> 404
// Conflict -> 409
return result.ToActionResult();
}
Custom MVC status mapping
[HttpGet("{id:int}")]
public IActionResult GetUser(int id)
{
var result = _userService.GetUserById(id);
return result.ToActionResult(status =>
status == StatusResult.NoExist
? StatusCodes.Status404NotFound
: ResultHttpStatusCodes.Get(status));
}
Usage in Minimal APIs
using Pitasoft.Result.AspNetCore;
app.MapGet("/users/{id:int}", (int id, IUserService userService) =>
{
var result = userService.GetUserById(id);
return result.ToHttpResult();
});
Practical Minimal API example
app.MapPost("/users", (CreateUserRequest request, IUserService userService) =>
{
var result = userService.CreateUser(request);
// Added -> 201
// ValidationError -> 400
// Conflict -> 409
return result.ToHttpResult();
});
Custom Minimal API status mapping
app.MapGet("/legacy-products/{id:int}", (int id, IProductService service) =>
{
var result = service.GetById(id);
return result.ToHttpResult(
status => status == StatusResult.NoExist
? StatusCodes.Status404NotFound
: ResultHttpStatusCodes.Get(status));
});
Working with collections
When you want to return a collection, prefer an explicit collection result:
app.MapGet("/products", (IProductService service) =>
{
var products = service.GetProducts();
return Result.OkEntities(products).ToHttpResult();
});
This is especially important when the response will be consumed by Pitasoft.Client, because the payload shape must remain a ResultEntities<T> contract.
Recommended collection patterns
var listResult = Result.OkEntities(products);
var pagedResult = Result.OkPaged(products, totalCount, page, pageSize);
Avoid relying on ambiguous overload resolution for collection-like objects such as List<T> if you need an explicit collection result contract at the HTTP boundary.
Working with paged results
Paged endpoints should return ResultPaged<T> explicitly:
app.MapGet("/products/paged", (int page, int pageSize, IProductService service) =>
{
var result = service.GetPaged(page, pageSize);
return result.ToHttpResult();
});
Recommended creation pattern:
var pagedResult = Result.OkPaged(items, totalCount, page, pageSize);
This preserves:
EntitiesTotalCountPagePageSize- pagination-derived helper metadata
Minimal API query binding helpers
The package also includes Minimal API-friendly parameter binders for common query scenarios.
QueryParameters
QueryParameters is the query-only contract. Use it when you need filtering, search, sorting, or attribute selection without paging:
attrsquerysearchorder
using Pitasoft.Result.AspNetCore.Parameters;
app.MapGet("/products/search", (QueryParameters parameters, IProductService service) =>
{
var result = service.Search(parameters);
return result.ToHttpResult();
});
PagingParameters
PagingParameters can be used directly in Minimal API handlers. It binds:
pagepageSize- legacy aliases
indexandsize
using Pitasoft.Result.AspNetCore.Parameters;
app.MapGet("/products", (PagingParameters parameters, IProductService service) =>
{
var result = service.GetPaged(parameters.Page, parameters.PageSize);
return result.ToHttpResult();
});
EntityParameters
EntityParameters combines both concerns in a single contract:
attrsquerysearchorderpage/pageSizeindex/size
using Pitasoft.Result.AspNetCore.Parameters;
app.MapGet("/products/search", (EntityParameters parameters, IProductService service) =>
{
var result = service.Search(parameters);
return result.ToHttpResult();
});
Practical example: paged product search
This is a realistic Minimal API endpoint that combines query binding with an explicit paged result contract:
using Pitasoft.Result;
using Pitasoft.Result.AspNetCore;
using Pitasoft.Result.AspNetCore.Parameters;
app.MapGet("/products", (EntityParameters parameters, IProductService service) =>
{
var items = service.Search(
search: parameters.Search,
query: parameters.Query,
order: parameters.Order,
attrs: parameters.Attrs,
page: parameters.Page ?? 1,
pageSize: parameters.PageSize ?? 25);
var totalCount = service.Count(parameters);
return Result.OkPaged(
items,
totalCount,
parameters.Page ?? 1,
parameters.PageSize ?? 25)
.ToHttpResult();
});
Example query string:
/products?search=laptop&order=name&page=2&pageSize=20
In that example:
searchbinds toEntityParameters.Searchorderbinds toEntityParameters.Orderpagebinds toEntityParameters.PagepageSizebinds toEntityParameters.PageSize
The same endpoint also accepts the aliases:
/products?search=laptop&order=name&index=2&size=20
This keeps Minimal API handlers concise while preserving a client-friendly ResultPaged<T> payload.
Custom BindAsync helpers
If you create your own Minimal API parameter object, use HttpContextQueryExtensions to keep typed query parsing small and predictable:
public sealed class ProductFilter
{
public string? Category { get; init; }
public bool? ActiveOnly { get; init; }
public static ValueTask<ProductFilter?> BindAsync(HttpContext context, ParameterInfo _)
{
var filter = new ProductFilter
{
Category = context.GetQueryValueOrDefault<string>("category"),
ActiveOnly = context.GetQueryValueOrDefault<bool?>("activeOnly")
};
return ValueTask.FromResult<ProductFilter?>(filter);
}
}
These helpers are especially useful when you want consistent query parsing without repeating Request.Query plumbing inside every endpoint.
ResultJsonSerializerOptions
ResultJsonSerializerOptions is the helper provided by this package to configure JsonSerializerOptions for Pitasoft result payloads.
Available methods
Create(...)
Creates a new JsonSerializerOptions instance using JsonSerializerDefaults.Web and adds the converters required for:
ResultEntities<T>ResultPaged<T>- structured error payloads such as
ErrorCollection
using Pitasoft.Result.AspNetCore;
var options = ResultJsonSerializerOptions.Create();
You can also customize the options:
var options = ResultJsonSerializerOptions.Create(o =>
{
o.WriteIndented = true;
o.PropertyNameCaseInsensitive = true;
});
Add(...)
Adds the required Pitasoft converters to an existing JsonSerializerOptions instance.
using System.Text.Json;
using Pitasoft.Result.AspNetCore;
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
ResultJsonSerializerOptions.Add(options);
ResultHttpStatusCodes
ResultHttpStatusCodes.Get(status) exposes the default status mapping used by the adapter.
This is useful when you want to customize one or two statuses while preserving the default behavior for the rest.
var httpCode = ResultHttpStatusCodes.Get(StatusResult.Conflict);
Using with Pitasoft.Client
If your client customizes JsonSerializerOptions, use ResultJsonSerializerOptions to keep collection payloads compatible.
This is the recommended integration model when your ASP.NET Core backend is consumed by .NET frontends such as:
Avalonia UIMAUIWPFBlazor
The intended contract is:
- the API returns
Pitasoft.Resultcontracts adapted throughPitasoft.Result.AspNetCore - the frontend consumes those contracts through
Pitasoft.Client - the UI interprets
StatusResultinstead of inventing a second error model
Example with an existing HttpClient
using Microsoft.Extensions.Logging.Abstractions;
using Pitasoft.Client;
using Pitasoft.Result;
using Pitasoft.Result.AspNetCore;
public sealed class ProductsClient : RestServiceBase
{
public ProductsClient(HttpClient client)
: base(client, ResultJsonSerializerOptions.Create(), NullLogger.Instance)
{
}
public Task<ResultEntities<ProductDto>> GetProductsAsync(CancellationToken cancellationToken = default) =>
GetAsync("products", new ResultEntities<ProductDto>(), cancellationToken);
public Task<ResultPaged<ProductDto>> GetProductsPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default) =>
GetAsync($"products/paged?page={page}&pageSize={pageSize}", new ResultPaged<ProductDto>(), cancellationToken);
public Task<ResultEntity<ProductDto>> GetProductAsync(int id, CancellationToken cancellationToken = default) =>
GetAsync($"products/{id}", new ResultEntity<ProductDto>(), cancellationToken);
}
public sealed record ProductDto(int Id, string Name, decimal Price);
Frontend consumption guideline
For .NET frontends, keep the same interpretation rules regardless of UI framework:
Ok,Added,Updated,Deleted- normal success flow
NoExist- empty state or "no data" state
ValidationError- form validation or inline error display
Conflict,ConcurrencyError- refresh, retry, or conflict message
Unauthorized- sign-in flow
Forbidden- access denied UI
NotFound- missing resource or navigation fallback
TooManyRequests,ServiceUnavailable- retry UX or temporary unavailable message
This allows the same client service layer to be reused across Avalonia UI, MAUI, WPF, and Blazor.
Example starting from custom options
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Pitasoft.Client;
using Pitasoft.Result.AspNetCore;
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
ResultJsonSerializerOptions.Add(options);
var client = new ProductsClient(httpClient, options);
End-to-end examples
Example 1: NoExist as a functional result
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
{
var product = service.Find(id);
var result = product is null
? Result.NotExists<ProductDto>()
: Result.Ok(product);
return result.ToHttpResult();
});
Expected behavior:
- if product exists →
200 OKwithStatus = Ok - if product does not exist →
200 OKwithStatus = NoExist
Example 2: explicit HTTP 404
app.MapGet("/customers/{id:int}", (int id, ICustomerService service) =>
{
var customer = service.Find(id);
var result = customer is null
? Result.NotFound<CustomerDto>()
: Result.Ok(customer);
return result.ToHttpResult();
});
Expected behavior:
- if customer exists →
200 OK - if customer does not exist →
404 Not FoundwithStatus = NotFound
Example 3: validation failure
app.MapPost("/orders", (CreateOrderRequest request, IOrderService service) =>
{
var result = service.Create(request);
return result.ToHttpResult();
});
Expected behavior:
- valid request →
201or200depending on returned status - invalid request →
400 Bad RequestwithStatus = ValidationError
Example 4: paged endpoint
app.MapGet("/products/paged", (int page, int pageSize, IProductService service) =>
{
var result = service.GetPaged(page, pageSize);
return result.ToHttpResult();
});
Expected behavior:
- the response body remains a
ResultPaged<T>contract - pagination metadata is preserved for clients
Pitasoft.Clientcan deserialize the paged result when usingResultJsonSerializerOptions
Example 5: conflict with structured errors
app.MapPost("/products/conflict", () =>
{
var errors = ErrorCollection.Create("sku", "A product with the same SKU already exists.");
return Result.Conflict<ProductDto>(errors).ToHttpResult();
});
Expected behavior:
- HTTP
409 Conflict Status = Conflict- structured errors remain in the response payload
Testing guidance
The repository includes tests for:
- MVC and Minimal API status mapping
- collection and single-entity serialization
- paged result serialization
- compatibility with
Pitasoft.Client NoExistvsNotFoundsemantics- structured validation and conflict payloads at the HTTP boundary
ResultActionFilterandResultEndpointFilterconversion behavior- async extension methods (
ToActionResultAsync,ToHttpResultAsync) [ProducesResult]attribute storage andProducesResultConventionbehavior (action/controller precedence, deduplication, multi-status)ResultValidationFiltershort-circuit behavior,ErrorCollectionconstruction fromModelState, empty key normalization, and blank message filtering- exception mapper chain:
CanMaprouting, fallback toDefaultResultExceptionMapper,AddExceptionMapper<T>()registration order
If you add new result mapping behavior, update tests accordingly.
License
This project is licensed under the terms described in LICENSE.txt.
Castellano
Pitasoft.Result.AspNetCore adapta los contratos de Pitasoft.Result a ASP.NET Core para que un mismo resultado de dominio o aplicación pueda devolverse de forma coherente desde:
- controladores MVC / Web API mediante
IActionResult - Minimal APIs mediante
Microsoft.AspNetCore.Http.IResult - clientes .NET como
Pitasoft.Client, preservando la forma del payload para entidades y colecciones enAvalonia UI,MAUI,WPFyBlazor
Este paquete es especialmente útil cuando tus servicios de aplicación ya devuelven Result, ResultEntity<T>, ResultEntities<T> o ResultPaged<T> y quieres una frontera HTTP fina, sin repetir el mapeo de estados en cada endpoint.
Instalación
dotnet add package Pitasoft.Result.AspNetCore
Inicio rápido
Minimal API:
using Pitasoft.Result;
using Pitasoft.Result.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResultAspNetCore();
var app = builder.Build();
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
{
var result = service.GetById(id);
return result.ToHttpResult();
});
app.Run();
MVC / Web API:
[HttpGet("{id:int}")]
public IActionResult GetProduct(int id)
{
var result = _service.GetById(id);
return result.ToActionResult();
}
Este es el punto de partida recomendado:
- registra
AddResultAspNetCore()una sola vez - devuelve contratos
Pitasoft.Resultdesde la capa de aplicación - usa
ToActionResult()oToHttpResult()en la frontera HTTP
Precedencia de configuración
Cuando hay más de una fuente de configuración, el adaptador aplica este orden:
- overrides explícitos por llamada como
result.ToHttpResult(customMapper, customOptions) - configuración global registrada con
AddResultAspNetCore(...) - valores por defecto de
ResultHttpStatusCodesyResultJsonSerializerOptions
Qué ofrece
ToActionResult()- convierte un
Pitasoft.Result.IResulten unIActionResultde ASP.NET Core
- convierte un
ToHttpResult()- convierte un
Pitasoft.Result.IResulten un resultado de Minimal API
- convierte un
AddResultAspNetCore(...)- registra una configuración global del adaptador mediante inyección de dependencias para el mapeo HTTP y la serialización JSON
AddResultExceptionHandling()- registra un manejador global de excepciones que transforma errores no controlados en contratos
Pitasoft.Result; retorna unIResultExceptionHandlingBuilderpara encadenar mappers adicionales mediante.AddExceptionMapper<T>()
- registra un manejador global de excepciones que transforma errores no controlados en contratos
IResultExceptionMapper- implementa esta interfaz para conectar un mapper personalizado a la cadena;
CanMap(exception)controla si tu mapper gestiona un tipo de excepción concreto
- implementa esta interfaz para conectar un mapper personalizado a la cadena;
ResultJsonSerializerOptions- crea o enriquece
JsonSerializerOptionspara que los payloads de entidad, colección, paginación y errores estructurados dePitasoft.Resultse serialicen y deserialicen correctamente
- crea o enriquece
ResultHttpStatusCodes- expone el mapeo por defecto
StatusResult -> HTTPy puede reutilizarse como base de personalización
- expone el mapeo por defecto
ToActionResultAsync()/ToHttpResultAsync()- variantes asíncronas para
Task<IResult>que eliminan el boilerplate deawaiten la frontera HTTP
- variantes asíncronas para
AddResultFilter()sobreIMvcBuilder- registra
ResultActionFilterglobalmente para que los actions de MVC puedan retornarIResultdirectamente sin llamar aToActionResult()
- registra
AddResultFilter()sobreRouteHandlerBuilder/RouteGroupBuilder- añade
ResultEndpointFiltera endpoints o grupos de Minimal APIs para que los handlers retornenIResultdirectamente sin llamar aToHttpResult()
- añade
- atributo
[ProducesResult(...)]+AddResultDocumentation()- declara los valores
StatusResultque un action o controlador puede devolver y registra automáticamente los códigos HTTP correspondientes como entradasProducesResponseTypeen la descripción OpenAPI
- declara los valores
WithResultDocumentation(...)sobreRouteHandlerBuilder/RouteGroupBuilder- registra los valores
StatusResultdeclarados como metadata de tipo de respuesta HTTP en endpoints de Minimal APIs para que aparezcan en la documentación OpenAPI / Swagger
- registra los valores
AddResultValidationFilter()- sustituye el
ModelStateInvalidFilterpor defecto de ASP.NET Core para que el estado de modelo inválido devuelva una respuestaValidationErrordePitasoft.Resulten lugar deValidationProblemDetails, manteniendo el contrato de error consistente
- sustituye el
SuppressModelStateValidation()- suprime el filtro incorporado sin añadir ningún reemplazo — úsalo cuando la validación se ejecute dentro del action mediante
Pitasoft.ValidationoPitasoft.FluentValidation
- suprime el filtro incorporado sin añadir ningún reemplazo — úsalo cuando la validación se ejecute dentro del action mediante
Por qué importa
Sin un adaptador compartido, es muy fácil que ASP.NET Core se desalineé de la semántica que ya expresa StatusResult.
Ejemplos de desalineación que este paquete ayuda a evitar:
- devolver siempre
200 OK, incluso cuando el dominio diceNotFoundoConflict - perder la diferencia entre
NoExistyNotFound - romper payloads de colección consumidos por
Pitasoft.Client - reimplementar el mismo
switchsobreStatusResulten muchos endpoints
Mapeo HTTP por defecto
ToHttpResult() y ToActionResult() convierten StatusResult a HTTP con esta política por defecto:
StatusResult |
HTTP |
|---|---|
Ok, Warning, NoExist |
200 OK |
Added |
201 Created |
Updated |
200 OK |
Deleted |
204 No Content por defecto, 200 OK cuando el resultado lleva payload |
ValidationError |
400 Bad Request |
Unauthorized, ChangePassword |
401 Unauthorized |
Forbidden |
403 Forbidden |
NotFound |
404 Not Found |
Conflict, ConcurrencyError |
409 Conflict |
UnprocessableEntity |
422 Unprocessable Entity |
TooManyRequests |
429 Too Many Requests |
ServiceUnavailable, ConnectionError, HttpError |
503 Service Unavailable |
None, CancelOperation, DataError, DatabaseError, Error, Exception |
500 Internal Server Error |
Nota semántica importante
NoExist se mapea a 200 OK de forma intencionada.
En el modelo de resultados de Pitasoft, normalmente significa que:
- la consulta se ejecutó correctamente
- los datos solicitados no existen
- el resultado sigue siendo funcional, no un fallo de transporte HTTP
Si necesitas semántica HTTP 404 explícita, devuelve StatusResult.NotFound.
Deleted también tiene un matiz importante:
Result.Deleted()plano se mapea a204 No ContentResult.Deleted(entity)oResult.DeletedEntities(...)se mapea a200 OKpara poder conservar el payload
Configuración global con DI
Si quieres cambiar el comportamiento por defecto una sola vez para toda la aplicación, registra el adaptador en IServiceCollection.
using Microsoft.AspNetCore.Http;
using Pitasoft.Result.AspNetCore;
builder.Services.AddResultAspNetCore(options =>
{
options.StatusCodeMapper = status =>
status == StatusResult.NoExist
? StatusCodes.Status404NotFound
: ResultHttpStatusCodes.Get(status);
options.SerializerOptions = ResultJsonSerializerOptions.Create(o =>
{
o.PropertyNamingPolicy = null;
});
});
Una vez registrado, llamadas normales como result.ToActionResult() y result.ToHttpResult() usarán automáticamente el mapping y las opciones JSON configuradas cuando se ejecuten dentro de ASP.NET Core.
Manejo global de excepciones
Si tu aplicación lanza excepciones no controladas, puedes convertirlas a respuestas Pitasoft.Result con:
builder.Services.AddResultAspNetCore();
builder.Services.AddResultExceptionHandling();
var app = builder.Build();
app.UseExceptionHandler(_ => { });
Mapeo por defecto de excepciones:
ValidationException→ValidationErrorUnauthorizedAccessException→UnauthorizedKeyNotFoundException→NotFoundOperationCanceledException→CancelOperation- cualquier otra excepción →
Exception
Cadena de mappers de excepción
AddResultExceptionHandling() retorna un IResultExceptionHandlingBuilder. Llama a .AddExceptionMapper<T>() para registrar mappers adicionales antes del fallback incorporado:
builder.Services.AddResultExceptionHandling()
.AddExceptionMapper<DbExceptionMapper>()
.AddExceptionMapper<HttpClientExceptionMapper>();
Los mappers se evalúan en orden de registro. El primero cuyo CanMap(exception) retorne true gestiona la excepción. DefaultResultExceptionMapper es siempre el fallback final y gestiona cualquier excepción que los mappers anteriores no hayan manejado.
Implementa IResultExceptionMapper para crear un mapper personalizado:
public sealed class DbExceptionMapper : IResultExceptionMapper
{
public bool CanMap(Exception exception) =>
exception is TimeoutException;
public IResult Map(Exception exception) =>
new Result(StatusResult.ServiceUnavailable);
}
Regístralo antes de UseExceptionHandler:
builder.Services.AddResultExceptionHandling()
.AddExceptionMapper<DbExceptionMapper>();
Estilos de host
Pitasoft.Result.AspNetCore está pensado para soportar los dos estilos habituales de ASP.NET Core sin cambiar los contratos de resultados de tu capa de aplicación:
ToHttpResult()para Minimal APIsToActionResult()para controladores MVC / Web APIAddResultFilter()para conversión automática sin llamada explícita en cualquiera de los dos estilosAddResultExceptionHandling()para un comportamiento compartido de excepción a resultado en cualquiera de los dos estilos[ProducesResult]/WithResultDocumentation()para documentación OpenAPI en cualquiera de los dos estilosAddResultValidationFilter()/SuppressModelStateValidation()para un contrato de error de validación unificado en MVC
Conversión automática de resultados
En lugar de llamar a ToActionResult() o ToHttpResult() en cada endpoint, registra un filtro una sola vez y deja que los handlers retornen IResult directamente.
MVC / Web API — ResultActionFilter
Registra globalmente mediante AddResultFilter() tras AddControllers():
builder.Services.AddControllers().AddResultFilter();
Los actions retornan IResult directamente — se aplica la misma precedencia de configuración de tres niveles:
[HttpGet("{id:int}")]
public IResult GetProduct(int id) => _service.GetById(id);
[HttpPost]
public Task<IResult> CreateProduct([FromBody] CreateRequest request) =>
_service.CreateAsync(request);
Minimal APIs — ResultEndpointFilter
Aplica por endpoint, por grupo o globalmente:
// Por endpoint
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
service.GetById(id)).AddResultFilter();
// Por grupo
var api = app.MapGroup("/api").AddResultFilter();
api.MapGet("/products/{id:int}", (int id, IProductService service) => service.GetById(id));
// Global — todos los endpoints bajo la raíz
var api = app.MapGroup("").AddResultFilter();
Los valores de retorno que no sean Pitasoft.Result.IResult — como cadenas de texto o resultados TypedResults — se pasan sin modificar.
Documentación OpenAPI
Declara los valores StatusResult que puede devolver cada endpoint y deja que el paquete registre automáticamente los códigos HTTP correspondientes en la descripción OpenAPI.
MVC / Web API — atributo [ProducesResult]
Activa la convención una sola vez en Program.cs:
builder.Services.AddControllers()
.AddResultFilter()
.AddResultDocumentation();
Luego anota cada action o controlador con [ProducesResult]:
[HttpGet("{id:int}")]
[ProducesResult(StatusResult.Ok, StatusResult.NoExist)]
public IResult GetProduct(int id) => _service.GetById(id);
[HttpPost]
[ProducesResult(StatusResult.Added, StatusResult.ValidationError)]
public IResult CreateProduct([FromBody] CreateRequest request) =>
_service.Create(request);
Cuando se aplica a la clase del controlador, los estados declarados son heredados por todos los actions que no lleven su propio atributo [ProducesResult].
Los códigos HTTP duplicados — por ejemplo StatusResult.Ok y StatusResult.NoExist se mapean ambos a 200 — se deduplicanan de forma automática.
Minimal APIs — WithResultDocumentation()
Encadena WithResultDocumentation() tras el mapeo del endpoint:
api.MapGet("/products/{id:int}", (int id, IProductService service) =>
service.GetById(id))
.WithResultDocumentation(StatusResult.Ok, StatusResult.NoExist);
api.MapPost("/products", (CreateRequest request, IProductService service) =>
service.Create(request))
.WithResultDocumentation(StatusResult.Added, StatusResult.ValidationError);
Aplícalo a un grupo entero para compartir documentación entre endpoints:
var api = app.MapGroup("/api")
.AddResultFilter()
.WithResultDocumentation(StatusResult.Ok, StatusResult.NotFound);
Validación de ModelState
Por defecto, el atributo [ApiController] de ASP.NET Core intercepta el estado de modelo inválido antes de que se ejecute el action y devuelve un payload ValidationProblemDetails (application/problem+json). Este formato difiere del contrato Pitasoft.Result utilizado en el resto de la aplicación.
AddResultValidationFilter() sustituye ese comportamiento para que los errores de validación se devuelvan como un resultado ValidationError estándar:
builder.Services.AddControllers()
.AddResultFilter()
.AddResultDocumentation()
.AddResultValidationFilter(); // sustituye ModelStateInvalidFilter
Un POST /products/validate con el campo Name vacío devolverá ahora:
{
"status": 7,
"errors": {
"Name": ["The Name field is required."]
}
}
en lugar del formato ValidationProblemDetails por defecto.
Cuando la validación ocurre dentro del action — SuppressModelStateValidation()
Cuando se usa Pitasoft.Validation, Pitasoft.FluentValidation u otra librería que valida dentro del action, el ModelStateInvalidFilter incorporado cortocircuitaría la petición antes de que el action pueda aplicar su propia lógica. Llama a SuppressModelStateValidation() para desactivarlo sin registrar ResultValidationFilter:
builder.Services.AddControllers()
.AddResultFilter()
.SuppressModelStateValidation(); // desactiva el filtro incorporado, la validación ocurre en el action
Extensiones asíncronas
Cuando los servicios de aplicación retornan Task<IResult>, encadena la conversión sin un await intermedio:
// MVC / Web API
[HttpGet("{id:int}")]
public Task<IActionResult> GetProduct(int id) =>
_service.GetByIdAsync(id).ToActionResultAsync();
// Minimal API
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
service.GetByIdAsync(id).ToHttpResultAsync());
Todas las sobrecargas de los métodos síncronos están disponibles en forma asíncrona:
| Método | Variantes |
|---|---|
ToActionResultAsync() |
default, mapper personalizado, IServiceProvider, HttpContext |
ToHttpResultAsync() |
default, JsonSerializerOptions, mapper personalizado, IServiceProvider, HttpContext |
Uso en MVC / Web API
using Microsoft.AspNetCore.Mvc;
using Pitasoft.Result.AspNetCore;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id:int}")]
public IActionResult GetUser(int id)
{
var result = _userService.GetUserById(id);
return result.ToActionResult();
}
}
Ejemplo práctico en MVC
[HttpPut("{id:int}")]
public IActionResult UpdateUser(int id, UpdateUserRequest request)
{
var result = _userService.UpdateUser(id, request);
// Updated -> 200
// ValidationError -> 400
// NotFound -> 404
// Conflict -> 409
return result.ToActionResult();
}
Mapeo MVC personalizado
[HttpGet("{id:int}")]
public IActionResult GetUser(int id)
{
var result = _userService.GetUserById(id);
return result.ToActionResult(status =>
status == StatusResult.NoExist
? StatusCodes.Status404NotFound
: ResultHttpStatusCodes.Get(status));
}
Uso en Minimal APIs
using Pitasoft.Result.AspNetCore;
app.MapGet("/users/{id:int}", (int id, IUserService userService) =>
{
var result = userService.GetUserById(id);
return result.ToHttpResult();
});
Ejemplo práctico en Minimal APIs
app.MapPost("/users", (CreateUserRequest request, IUserService userService) =>
{
var result = userService.CreateUser(request);
// Added -> 201
// ValidationError -> 400
// Conflict -> 409
return result.ToHttpResult();
});
Mapeo personalizado en Minimal APIs
app.MapGet("/legacy-products/{id:int}", (int id, IProductService service) =>
{
var result = service.GetById(id);
return result.ToHttpResult(
status => status == StatusResult.NoExist
? StatusCodes.Status404NotFound
: ResultHttpStatusCodes.Get(status));
});
Trabajo con colecciones
Cuando quieras devolver una colección, es preferible crear el resultado de colección de forma explícita:
app.MapGet("/products", (IProductService service) =>
{
var products = service.GetProducts();
return Result.OkEntities(products).ToHttpResult();
});
Esto es especialmente importante si la respuesta va a ser consumida por Pitasoft.Client, porque la forma del payload debe seguir siendo un contrato ResultEntities<T>.
Patrones recomendados para colecciones
var listResult = Result.OkEntities(products);
var pagedResult = Result.OkPaged(products, totalCount, page, pageSize);
Evita depender de una resolución ambigua de sobrecargas para objetos tipo List<T> si necesitas un contrato de colección explícito en la frontera HTTP.
Trabajo con resultados paginados
Los endpoints paginados deberían devolver ResultPaged<T> de forma explícita:
app.MapGet("/products/paged", (int page, int pageSize, IProductService service) =>
{
var result = service.GetPaged(page, pageSize);
return result.ToHttpResult();
});
Patrón recomendado de creación:
var pagedResult = Result.OkPaged(items, totalCount, page, pageSize);
Esto preserva:
EntitiesTotalCountPagePageSize- la metadata auxiliar derivada de la paginación
ResultJsonSerializerOptions
ResultJsonSerializerOptions es la utilidad que proporciona este paquete para configurar JsonSerializerOptions en payloads de Pitasoft.Result.
Métodos disponibles
Create(...)
Crea una nueva instancia de JsonSerializerOptions usando JsonSerializerDefaults.Web y añade los conversores necesarios para:
ResultEntities<T>ResultPaged<T>- payloads de error estructurado como
ErrorCollection
using Pitasoft.Result.AspNetCore;
var options = ResultJsonSerializerOptions.Create();
También puedes personalizar las opciones:
var options = ResultJsonSerializerOptions.Create(o =>
{
o.WriteIndented = true;
o.PropertyNameCaseInsensitive = true;
});
Add(...)
Añade los conversores necesarios de Pitasoft sobre unas JsonSerializerOptions existentes.
using System.Text.Json;
using Pitasoft.Result.AspNetCore;
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
ResultJsonSerializerOptions.Add(options);
ResultHttpStatusCodes
ResultHttpStatusCodes.Get(status) expone el mapeo por defecto usado por el adaptador.
Es útil cuando quieres personalizar uno o dos estados sin reimplementar el resto del comportamiento.
var httpCode = ResultHttpStatusCodes.Get(StatusResult.Conflict);
Uso con Pitasoft.Client
Si tu cliente personaliza JsonSerializerOptions, utiliza ResultJsonSerializerOptions para mantener compatibles los payloads de colección.
Este es el modelo de integración recomendado cuando tu backend ASP.NET Core es consumido desde frontends .NET como:
Avalonia UIMAUIWPFBlazor
El contrato esperado es:
- la API devuelve contratos
Pitasoft.Resultadaptados mediantePitasoft.Result.AspNetCore - el frontend consume esos contratos mediante
Pitasoft.Client - la UI interpreta
StatusResulten lugar de inventar un segundo modelo de errores
Ejemplo con HttpClient existente
using Microsoft.Extensions.Logging.Abstractions;
using Pitasoft.Client;
using Pitasoft.Result;
using Pitasoft.Result.AspNetCore;
public sealed class ProductsClient : RestServiceBase
{
public ProductsClient(HttpClient client)
: base(client, ResultJsonSerializerOptions.Create(), NullLogger.Instance)
{
}
public Task<ResultEntities<ProductDto>> GetProductsAsync(CancellationToken cancellationToken = default) =>
GetAsync("products", new ResultEntities<ProductDto>(), cancellationToken);
public Task<ResultPaged<ProductDto>> GetProductsPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default) =>
GetAsync($"products/paged?page={page}&pageSize={pageSize}", new ResultPaged<ProductDto>(), cancellationToken);
public Task<ResultEntity<ProductDto>> GetProductAsync(int id, CancellationToken cancellationToken = default) =>
GetAsync($"products/{id}", new ResultEntity<ProductDto>(), cancellationToken);
}
public sealed record ProductDto(int Id, string Name, decimal Price);
Guía de consumo para frontend
En frontends .NET, conviene mantener las mismas reglas de interpretación sin importar el framework UI:
Ok,Added,Updated,Deleted- flujo normal de éxito
NoExist- estado vacío o "sin datos"
ValidationError- validación de formulario o errores inline
Conflict,ConcurrencyError- refresco, reintento o mensaje de conflicto
Unauthorized- flujo de login
Forbidden- UI de acceso denegado
NotFound- recurso inexistente o fallback de navegación
TooManyRequests,ServiceUnavailable- UX de reintento o mensaje de indisponibilidad temporal
Esto permite reutilizar la misma capa de servicios cliente en Avalonia UI, MAUI, WPF y Blazor.
Ejemplo partiendo de opciones propias
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Pitasoft.Client;
using Pitasoft.Result.AspNetCore;
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
ResultJsonSerializerOptions.Add(options);
var client = new ProductsClient(httpClient, options);
Ejemplos end-to-end
Ejemplo 1: NoExist como resultado funcional
app.MapGet("/products/{id:int}", (int id, IProductService service) =>
{
var product = service.Find(id);
var result = product is null
? Result.NotExists<ProductDto>()
: Result.Ok(product);
return result.ToHttpResult();
});
Comportamiento esperado:
- si el producto existe →
200 OKconStatus = Ok - si el producto no existe →
200 OKconStatus = NoExist
Ejemplo 2: 404 HTTP explícito
app.MapGet("/customers/{id:int}", (int id, ICustomerService service) =>
{
var customer = service.Find(id);
var result = customer is null
? Result.NotFound<CustomerDto>()
: Result.Ok(customer);
return result.ToHttpResult();
});
Comportamiento esperado:
- si el cliente existe →
200 OK - si el cliente no existe →
404 Not FoundconStatus = NotFound
Ejemplo 3: error de validación
app.MapPost("/orders", (CreateOrderRequest request, IOrderService service) =>
{
var result = service.Create(request);
return result.ToHttpResult();
});
Comportamiento esperado:
- request válida →
201o200según el estado devuelto - request inválida →
400 Bad RequestconStatus = ValidationError
Ejemplo 4: endpoint paginado
app.MapGet("/products/paged", (int page, int pageSize, IProductService service) =>
{
var result = service.GetPaged(page, pageSize);
return result.ToHttpResult();
});
Comportamiento esperado:
- el cuerpo sigue siendo un contrato
ResultPaged<T> - la metadata de paginación se preserva para el cliente
Pitasoft.Clientpuede deserializar el resultado paginado usandoResultJsonSerializerOptions
Ejemplo 5: conflicto con errores estructurados
app.MapPost("/products/conflict", () =>
{
var errors = ErrorCollection.Create("sku", "A product with the same SKU already exists.");
return Result.Conflict<ProductDto>(errors).ToHttpResult();
});
Comportamiento esperado:
- HTTP
409 Conflict Status = Conflict- los errores estructurados se mantienen en el payload de respuesta
Guía de testing
El repositorio incluye pruebas para:
- mapeo de estados en MVC y Minimal API
- serialización de entidad individual y colecciones
- serialización de resultados paginados
- compatibilidad con
Pitasoft.Client - semántica
NoExistvsNotFound - payloads estructurados de validación y conflicto en la frontera HTTP
- comportamiento de conversión de
ResultActionFilteryResultEndpointFilter - extensiones asíncronas (
ToActionResultAsync,ToHttpResultAsync) - almacenamiento del atributo
[ProducesResult]y comportamiento deProducesResultConvention(precedencia action/controlador, deduplicación, múltiples estados) - comportamiento de cortocircuito de
ResultValidationFilter, construcción deErrorCollectiondesdeModelState, normalización de claves vacías y filtrado de mensajes en blanco - cadena de mappers de excepción: enrutamiento por
CanMap, fallback aDefaultResultExceptionMapper, orden de registro conAddExceptionMapper<T>()
Si amplías el comportamiento de mapeo, actualiza también las pruebas.
Autor
Sebastián Martínez Pérez
Licencia
Copyright © 2026 Pitasoft, S.L.
Licenciado bajo los términos de la LICENSE.txt incluida en este repositorio.
| 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.Audit.Abstractions (>= 1.0.1)
- Pitasoft.Result (>= 7.3.2)
-
net8.0
- Pitasoft.Audit.Abstractions (>= 1.0.1)
- Pitasoft.Result (>= 7.3.2)
-
net9.0
- Pitasoft.Audit.Abstractions (>= 1.0.1)
- Pitasoft.Result (>= 7.3.2)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Pitasoft.Result.AspNetCore:
| Package | Downloads |
|---|---|
|
Pitasoft.FluentValidation.AspNetCore
ASP.NET Core integration for FluentValidation. Provides Minimal API endpoint filters and MVC action filters to validate request parameters using FluentValidation.IValidator<T> and returns Pitasoft.Result validation responses. |
|
|
Pitasoft.Validation.AspNetCore
ASP.NET Core integration for Pitasoft.Validation. Provides Minimal API endpoint filters and MVC action filters to validate request parameters using IValidator<T>, IValidatorAsync<T>, IChecker<T>, and ICheckerAsync<T>. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Initial public package release with MVC and Minimal API adapters for Pitasoft.Result,
JSON serialization helpers for entity, collection, paged, and structured error contracts,
configurable HTTP status mapping, DI-based configuration, and global exception handling support.