Pitasoft.Client
7.0.6
See the version list below for details.
dotnet add package Pitasoft.Client --version 7.0.6
NuGet\Install-Package Pitasoft.Client -Version 7.0.6
<PackageReference Include="Pitasoft.Client" Version="7.0.6" />
<PackageVersion Include="Pitasoft.Client" Version="7.0.6" />
<PackageReference Include="Pitasoft.Client" />
paket add Pitasoft.Client --version 7.0.6
#r "nuget: Pitasoft.Client, 7.0.6"
#:package Pitasoft.Client@7.0.6
#addin nuget:?package=Pitasoft.Client&version=7.0.6
#tool nuget:?package=Pitasoft.Client&version=7.0.6
Pitasoft.Client
English
Pitasoft.Client is a .NET library designed to simplify the consumption of RESTful services from any application type — including Blazor, Avalonia UI, and MAUI. It provides a robust abstract base class, authentication support, DI extensions, and helpers to handle HTTP requests, JSON serialization, file uploads/downloads, and common API patterns.
Features
- Base REST Service:
RestServiceBaseprovides methods for GET, POST, PUT, PATCH, and DELETE operations, including batch processing and body-based deletions. - Logging Support: Built-in integration with
ILoggerfor request/response tracing and error logging. - Thread-safe Authentication: Bearer tokens are applied per-request, ensuring safety in concurrent environments like Blazor.
- Enhanced Error Mapping: Maps specific HTTP codes (400, 401, 403, 404, 409, 422, 429, 503) to descriptive
StatusResultvalues. - Paginated Results: Built-in support for
ResultPaged<T>viaGetPagedAsync, withPagingParametersandIParametersoverloads, andIsEmptyconvenience property. - Bearer Token Authentication: Automatic injection of
Authorization: Bearerheaders via theITokenProviderinterface. - Token Refresh / 401 Retry: Override
TryRefreshTokenAsyncto implement refresh token logic with automatic request retry. - File Upload:
UploadAsyncsendsMultipartFormDataContentfor uploading files and form data. - File Download with Progress:
DownloadAsyncstreams binary content with optionalIProgress<long>reporting. - Connection Error Detection: Distinguishes network-level errors (
SocketException) from generic HTTP errors usingStatusResult.ConnectionError. - Modern HttpClient Management: Full support for
IHttpClientFactoryfor efficient connection management, socket pooling, and DI integration. - DI Extensions:
AddRestServiceextension methods for registering services fluently in Blazor, MAUI, and AvaloniaProgram.cs/MauiProgram.cs. - Flexible Configuration:
SettingRestServicecentralizes base URL, JSON options, token provider, default headers, and request timeout. - Query Helpers:
QueryHelperssafely and efficiently builds query strings from dictionaries,IParameters, or key-value pairs. - High Performance: Optimized for .NET 8, 9, and 10 using
SearchValues<char>,ReadOnlySpan<char>, and efficient memory management. - Result Handling: Integrated with
Pitasoft.Resultfor consistent and expressive response handling.
Performance Optimizations (.NET 8/9/10)
- Zero-allocation URI parsing:
ReadOnlySpan<char>andAsSpan()avoid unnecessary string allocations during query string manipulation. - Fast character search:
SearchValues<char>enables ultra-fast detection of?and#control characters. - Memory efficiency:
StringBuilderpre-sizing and optimized JSON deserialization paths.
| Method | Mean | Ratio | Allocated |
|---|---|---|---|
AddSingleQueryString |
34.31 ns | 1.00 | 456 B |
AddMultipleQueryStrings |
118.62 ns | 1.00 | 960 B |
Installation
dotnet add package Pitasoft.Client
Core Concepts
Result Types (from Pitasoft.Result)
All methods return a result object that encapsulates the response status, data, and errors:
| Type | Description |
|---|---|
Result |
Simple result with no entity payload. Used for DELETE operations. |
ResultEntity<T> |
Result containing a single entity of type T. |
ResultEntities<T> |
Result containing a collection of entities of type T. |
ResultPaged<T> |
Result with a paged collection: includes Items, Page, PageSize, TotalCount, and IsEmpty. |
ResultBatch<T> |
Result from a batch (bulk) POST operation. |
Status Values (StatusResult)
| Value | Description |
|---|---|
Ok |
HTTP 200: The request succeeded. |
Added |
HTTP 201: Entity added successfully. |
Updated |
HTTP 200/204: Entity updated successfully. |
Deleted |
HTTP 200/204: Entity deleted successfully. |
NoExist |
The requested entity does not exist. |
Warning |
Operation completed with warnings. |
CancelOperation |
The request was cancelled. |
ValidationError |
HTTP 400: Validation error in input data. |
DataError |
Error in processed data. |
DatabaseError |
Error while accessing the database. |
ConcurrencyError |
HTTP 409: Concurrency error while updating data. |
ConnectionError |
Network error (e.g., SocketException). |
HttpError |
An unexpected HTTP error occurred. |
Error |
A business-level error occurred. |
Unauthorized |
HTTP 401: Authentication is required. |
Forbidden |
HTTP 403: The server refused to authorize the request. |
NotFound |
HTTP 404: The requested resource was not found. |
Conflict |
HTTP 409: The request conflicts with the current state. |
UnprocessableEntity |
HTTP 422: Semantic errors in the request. |
TooManyRequests |
HTTP 429: Rate limit exceeded. |
ServiceUnavailable |
HTTP 503: The server is temporarily unavailable. |
ChangePassword |
The server signals a password change is required. |
Exception |
An unhandled exception has occurred. |
Usage
1. Define Your Service
Inherit from RestServiceBase and inject IHttpClientFactory (recommended) or pass an HttpClient directly.
public interface IProductService
{
Task<ResultEntities<Product>> GetAllAsync(CancellationToken ct = default);
Task<ResultPaged<Product>> GetPagedAsync(int page, int pageSize, CancellationToken ct = default);
Task<ResultEntity<Product>> GetByIdAsync(int id, CancellationToken ct = default);
Task<ResultEntity<Product>> CreateAsync(Product product, CancellationToken ct = default);
Task<ResultEntity<Product>> UpdateAsync(int id, Product product, CancellationToken ct = default);
Task<ResultEntity<Product>> PatchAsync(int id, object patch, CancellationToken ct = default);
Task<Result> DeleteAsync(int id, CancellationToken ct = default);
Task<Result> DeactivateAsync(int id, string reason, CancellationToken ct = default);
Task<ResultEntity<Stream>> GetExportStreamAsync(int id, CancellationToken ct = default);
}
public class ProductService : RestServiceBase, IProductService
{
// Recommended: use IHttpClientFactory for proper connection management and logging
public ProductService(IHttpClientFactory factory, ILogger<ProductService> logger)
: base(factory, nameof(ProductService), logger) { }
public Task<ResultEntities<Product>> GetAllAsync(CancellationToken ct = default)
=> GetsAsync<Product>("api/products", ct);
public Task<ResultPaged<Product>> GetPagedAsync(int page, int pageSize, CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products", new PagingParameters { Page = page, PageSize = pageSize }, ct);
public Task<ResultEntity<Product>> GetByIdAsync(int id, CancellationToken ct = default)
=> GetAsync<Product>($"api/products/{id}", ct);
public Task<ResultEntity<Product>> CreateAsync(Product product, CancellationToken ct = default)
=> PostAsync("api/products", product, ct);
public Task<ResultEntity<Product>> UpdateAsync(int id, Product product, CancellationToken ct = default)
=> PutAsync($"api/products/{id}", product, ct);
public Task<ResultEntity<Product>> PatchAsync(int id, object patch, CancellationToken ct = default)
=> base.PatchAsync<object, Product>($"api/products/{id}", patch, ct);
public Task<Result> DeleteAsync(int id, CancellationToken ct = default)
=> base.DeleteAsync($"api/products/{id}", ct);
public Task<Result> DeactivateAsync(int id, string reason, CancellationToken ct = default)
=> base.DeleteAsync($"api/products/{id}", new { Reason = reason }, ct);
public Task<ResultEntity<Stream>> GetExportStreamAsync(int id, CancellationToken ct = default)
=> DownloadStreamAsync($"api/products/{id}/export", ct);
}
2. Register in Dependency Injection
Use the built-in AddRestService extension methods in Program.cs (Blazor / MAUI) or App.axaml.cs (Avalonia):
// Option A — Simple: base URL + optional JSON options
builder.Services.AddRestService<IProductService, ProductService>(
baseUrl: "https://api.example.com/",
jsonSerializerOptions: new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
// Option B — SettingRestService: full configuration object
builder.Services.AddRestService<IProductService, ProductService>(new SettingRestService
{
UriString = "https://api.example.com/",
JsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true },
TokenProvider = new MyTokenProvider(),
DefaultHeaders = new Dictionary<string, string>
{
{ "X-Api-Version", "2" },
{ "X-Tenant-Id", "acme" }
},
Timeout = TimeSpan.FromSeconds(30)
});
// Option C — Without interface (concrete type only)
builder.Services.AddRestService<ProductService>("https://api.example.com/");
// Option D — Manual IHttpClientFactory (full control)
builder.Services.AddHttpClient(nameof(ProductService), client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
builder.Services.AddScoped<IProductService, ProductService>();
3. Consume the Service
// In a Blazor component, MAUI ViewModel, or Avalonia ViewModel:
var result = await _productService.GetPagedAsync(page: 1, pageSize: 20);
if (result.Status == StatusResult.Ok)
{
// result.Items → IEnumerable<Product>
// result.Page → current page
// result.PageSize
// result.TotalCount
Console.WriteLine($"Page {result.Page} — {result.TotalCount} total products");
}
Authentication — Bearer Token (ITokenProvider)
Implement ITokenProvider to supply tokens from any source (in-memory, SecureStorage in MAUI, AuthenticationStateProvider in Blazor, etc.).
// 1. Implement ITokenProvider
public class MyTokenProvider : ITokenProvider
{
private string? _token;
public void SetToken(string? token) => _token = token;
public Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(_token);
}
RestServiceBase automatically calls GetTokenAsync before every request and sets the Authorization: Bearer {token} header. If the token is null or empty, the header is removed.
// 2. Register with DI
builder.Services.AddSingleton<ITokenProvider, MyTokenProvider>();
builder.Services.AddRestService<IProductService, ProductService>(new SettingRestService
{
UriString = "https://api.example.com/",
TokenProvider = serviceProvider.GetRequiredService<ITokenProvider>()
});
Blazor WASM — Integration with AuthenticationStateProvider
public class BlazorTokenProvider : ITokenProvider
{
private readonly ILocalStorageService _localStorage;
public BlazorTokenProvider(ILocalStorageService localStorage)
=> _localStorage = localStorage;
public async Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
=> await _localStorage.GetItemAsStringAsync("access_token");
}
MAUI — Integration with SecureStorage
public class MauiTokenProvider : ITokenProvider
{
public async Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
=> await SecureStorage.Default.GetAsync("access_token");
}
Token Refresh / 401 Retry (TryRefreshTokenAsync)
When the server returns HTTP 401, RestServiceBase calls TryRefreshTokenAsync. If it returns true, the request is automatically retried once with the new token.
public class ProductService : RestServiceBase, IProductService
{
private readonly IAuthService _authService;
public ProductService(IHttpClientFactory factory, IAuthService authService)
: base(factory, nameof(ProductService))
{
_authService = authService;
}
protected override async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken = default)
{
var refreshed = await _authService.RefreshTokenAsync(cancellationToken);
return refreshed; // true → retry the original request; false → propagate Unauthorized
}
}
Paginated Results (GetPagedAsync)
GetPagedAsync returns a ResultPaged<T> with pagination metadata.
// With PagingParameters (Page + PageSize)
public Task<ResultPaged<Product>> GetPagedAsync(int page, int pageSize, CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products", new PagingParameters { Page = page, PageSize = pageSize }, ct);
// With IParameters (custom filter + paging)
public Task<ResultPaged<Product>> SearchAsync(ProductParameters parameters, CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products", parameters, ct);
// Without parameters (query string already embedded in path)
public Task<ResultPaged<Product>> GetPagedRawAsync(CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products?page=1&pageSize=10", ct);
Note:
PagingParametersserializes as?Page=1&PageSize=20in the query string. CustomIParametersclasses must implementGetParameters()returningIEnumerable<KeyValuePair<string, string>>.
File Upload (UploadAsync)
public async Task<ResultEntity<UploadResponse>> UploadAvatarAsync(Stream fileStream, string fileName, CancellationToken ct = default)
{
var content = new MultipartFormDataContent();
content.Add(new StreamContent(fileStream), "file", fileName);
return await UploadAsync<UploadResponse>("api/users/avatar", content, ct);
}
File Download with Progress (DownloadAsync)
public async Task<ResultEntity<byte[]>> DownloadReportAsync(
int reportId,
IProgress<long>? progress = null,
CancellationToken ct = default)
{
return await DownloadAsync($"api/reports/{reportId}/download", progress, ct);
}
// Usage — e.g. in a MAUI / Avalonia ViewModel:
var progress = new Progress<long>(bytesRead => Console.WriteLine($"Downloaded: {bytesRead} bytes"));
var result = await _reportService.DownloadReportAsync(42, progress, cancellationToken);
if (result.Status == StatusResult.Ok)
{
await File.WriteAllBytesAsync("report.pdf", result.Entity!, cancellationToken);
}
PATCH — Partial Update (PatchAsync)
Use PatchAsync to send partial entity updates without replacing the entire resource.
// Same input/output type
public Task<ResultEntity<Product>> PatchNameAsync(int id, string newName, CancellationToken ct = default)
=> PatchAsync($"api/products/{id}", new { Name = newName }, ct);
// Different input/output types
public Task<ResultEntity<ProductDetail>> PatchStatusAsync(int id, StatusPatch patch, CancellationToken ct = default)
=> PatchAsync<StatusPatch, ProductDetail>($"api/products/{id}", patch, ct);
// Custom result type
public Task<MyCustomResult> PatchWithCustomResultAsync(int id, object patch, MyCustomResult initial, CancellationToken ct = default)
=> PatchAsync($"api/products/{id}", patch, initial, ct);
Connection Error Handling
RestServiceBase distinguishes three error scenarios automatically:
| Scenario | StatusResult |
Cause |
|---|---|---|
| Cancelled by user | CancelOperation |
OperationCanceledException |
| No network / connection refused | ConnectionError |
HttpRequestException + SocketException |
| Unexpected HTTP error | HttpError |
Any other Exception |
var result = await _productService.GetAllAsync();
switch (result.Status)
{
case StatusResult.Ok:
// Use result data
break;
case StatusResult.ConnectionError:
// Show offline banner / retry button
break;
case StatusResult.Unauthorized:
// Redirect to login
break;
case StatusResult.ValidationError:
// Show result.Errors to user
break;
case StatusResult.CancelOperation:
// Request was cancelled — no action needed
break;
}
Default Headers and Timeout (SettingRestService)
var setting = new SettingRestService
{
UriString = "https://api.example.com/",
Timeout = TimeSpan.FromSeconds(15),
DefaultHeaders = new Dictionary<string, string>
{
{ "X-Api-Key", "your-api-key" },
{ "X-Api-Version", "2" },
{ "X-Tenant-Id", "acme" }
}
};
// Pass directly to the RestServiceBase constructor:
public class ProductService : RestServiceBase
{
public ProductService(SettingRestService setting) : base(setting) { }
}
Query Helpers (QueryHelpers)
QueryHelpers provides static methods to safely compose query strings. All values are URL-encoded automatically.
// Single key-value
var uri = QueryHelpers.AddQueryString("api/products", "category", "electronics");
// → "api/products?category=electronics"
// Dictionary
var uri = QueryHelpers.AddQueryString("api/products", new Dictionary<string, string>
{
{ "category", "electronics" },
{ "inStock", "true" }
});
// → "api/products?category=electronics&inStock=true"
// IParameters (e.g. PagingParameters or custom EntityParameters)
var uri = QueryHelpers.AddQueryString("api/products", new PagingParameters { Page = 2, PageSize = 10 });
// → "api/products?Page=2&PageSize=10"
// Preserves existing query string and anchors
var uri = QueryHelpers.AddQueryString("api/products?sort=asc#section", "page", "3");
// → "api/products?sort=asc&page=3#section"
Virtual Hooks (Override in Derived Classes)
RestServiceBase exposes protected virtual methods to customize behaviour without re-implementing the full request pipeline:
| Method | When Called | Default |
|---|---|---|
OnUnauthorized(IResult) |
HTTP 401 received and not retried | no-op |
OnError(IResult) |
Unexpected HTTP error (non-400/401) | no-op |
OnChangePassword(IResult) |
Server signals StatusResult.ChangePassword |
no-op |
HandleSatisfactoryAsync(...) |
After a successful HTTP response | Routes to OnUnauthorized/OnChangePassword/OnError |
HandleErrorsAsync(...) |
After a failed HTTP response | Sets Status, parses 400 errors, calls OnUnauthorized/OnError |
TryRefreshTokenAsync(...) |
On HTTP 401 before propagating | Returns false |
public class ProductService : RestServiceBase
{
protected override void OnUnauthorized(IResult result)
{
// e.g. fire an event, navigate to login, clear local state
}
protected override void OnError(IResult result)
{
// e.g. log to Sentry, show a toast notification
}
protected override async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken = default)
{
// Return true if token was refreshed and the request should be retried
return await _authService.RefreshAsync(cancellationToken);
}
}
All Available Constructors
RestServiceBase supports seven construction patterns:
// 1. Base URI only
protected RestServiceBase(string uriString, ILogger? logger = null)
// 2. Base URI + JSON options
protected RestServiceBase(string uriString, JsonSerializerOptions jsonSerializerOptions, ILogger? logger = null)
// 3. Existing HttpClient
protected RestServiceBase(HttpClient client, ILogger? logger = null)
// 4. Existing HttpClient + JSON options
protected RestServiceBase(HttpClient client, JsonSerializerOptions jsonSerializerOptions, ILogger? logger = null)
// 5. IHttpClientFactory + named client (recommended for DI)
protected RestServiceBase(IHttpClientFactory httpClientFactory, string clientName, ILogger? logger = null)
// 6. IHttpClientFactory + named client + JSON options
protected RestServiceBase(IHttpClientFactory httpClientFactory, string clientName, JsonSerializerOptions jsonSerializerOptions, ILogger? logger = null)
// 7. SettingRestService (full configuration: URI, JSON, token, headers, timeout)
protected RestServiceBase(SettingRestService setting, ILogger? logger = null)
Interfaces
IRestService
Public interface implemented by RestServiceBase. Exposes the following read-only properties:
public interface IRestService
{
JsonSerializerOptions? JsonSerializerOptions { get; }
HttpClient Client { get; }
ITokenProvider? TokenProvider { get; }
}
ITokenProvider
public interface ITokenProvider
{
Task<string?> GetTokenAsync(CancellationToken cancellationToken = default);
}
Castellano
Pitasoft.Client es una librería .NET diseñada para simplificar el consumo de servicios RESTful desde cualquier tipo de aplicación — incluyendo Blazor, Avalonia UI y MAUI. Proporciona una clase base abstracta robusta, soporte de autenticación, extensiones de DI y utilidades para manejar peticiones HTTP, serialización JSON, subida/descarga de archivos y patrones comunes de API.
Características
- Servicio REST Base:
RestServiceBaseproporciona métodos para operaciones GET, POST, PUT, PATCH y DELETE, incluyendo soporte para procesamiento por lotes (batch) y borrados con cuerpo. - Soporte de Logging: Integración nativa con
ILoggerpara trazas de peticiones/respuestas y registro de errores. - Autenticación Thread-safe: Los tokens Bearer se aplican por petición, garantizando seguridad en entornos concurrentes como Blazor.
- Mapeo de Errores Mejorado: Mapea códigos HTTP específicos (400, 401, 403, 404, 409, 422, 429, 503) a valores descriptivos de
StatusResult. - Resultados Paginados: Soporte nativo de
ResultPaged<T>medianteGetPagedAsync, con sobrecargas paraPagingParameterseIParameters, y propiedad de convenienciaIsEmpty. - Autenticación con Bearer Token: Inyección automática de cabeceras
Authorization: Bearera través de la interfazITokenProvider. - Refresco de Token / Reintento 401: Sobrescribe
TryRefreshTokenAsyncpara implementar lógica de refresh token con reintento automático de la petición. - Subida de Archivos:
UploadAsyncenvíaMultipartFormDataContentpara subir ficheros y formularios. - Descarga de Archivos con Progreso:
DownloadAsyncdescarga contenido binario con reporte opcional medianteIProgress<long>. - Detección de Error de Conexión: Distingue errores de red (
SocketException) de errores HTTP genéricos usandoStatusResult.ConnectionError. - Gestión Moderna de HttpClient: Soporte completo para
IHttpClientFactorycon gestión eficiente de conexiones, pooling de sockets e integración con DI. - Extensiones de DI: Métodos de extensión
AddRestServicepara registrar servicios de forma fluida enProgram.cs/MauiProgram.csde Blazor, MAUI y Avalonia. - Configuración Flexible:
SettingRestServicecentraliza URL base, opciones JSON, proveedor de token, cabeceras predeterminadas y timeout de petición. - Helpers de Consulta:
QueryHelpersconstruye query strings de forma segura y eficiente desde diccionarios,IParameterso pares clave-valor. - Alto Rendimiento: Optimizado para .NET 8, 9 y 10 utilizando
SearchValues<char>,ReadOnlySpan<char>y gestión eficiente de memoria. - Manejo de Resultados: Integrado con
Pitasoft.Resultpara un manejo de respuestas consistente y expresivo.
Optimizaciones de Rendimiento (.NET 8/9/10)
- Análisis de URIs sin asignaciones:
ReadOnlySpan<char>yAsSpan()evitan la creación innecesaria de cadenas temporales. - Búsqueda rápida de caracteres:
SearchValues<char>detecta?y#con mínima sobrecarga. - Eficiencia de memoria: Pre-dimensionamiento de
StringBuildery rutas de deserialización JSON optimizadas.
| Método | Media | Ratio | Memoria |
|---|---|---|---|
AddSingleQueryString |
34.31 ns | 1.00 | 456 B |
AddMultipleQueryStrings |
118.62 ns | 1.00 | 960 B |
Instalación
dotnet add package Pitasoft.Client
Conceptos Clave
Tipos de Resultado (de Pitasoft.Result)
Todos los métodos devuelven un objeto resultado que encapsula el estado de la respuesta, los datos y los errores:
| Tipo | Descripción |
|---|---|
Result |
Resultado simple sin payload de entidad. Se usa en operaciones DELETE. |
ResultEntity<T> |
Resultado que contiene una única entidad de tipo T. |
ResultEntities<T> |
Resultado que contiene una colección de entidades de tipo T. |
ResultPaged<T> |
Resultado con colección paginada: incluye Items, Page, PageSize, TotalCount e IsEmpty. |
ResultBatch<T> |
Resultado de una operación POST en lote (bulk). |
Valores de Estado (StatusResult)
| Valor | Descripción |
|---|---|
Ok |
HTTP 200: La petición se completó con éxito. |
Added |
HTTP 201: Entidad añadida con éxito. |
Updated |
HTTP 200/204: Entidad actualizada con éxito. |
Deleted |
HTTP 200/204: Entidad eliminada con éxito. |
NoExist |
La entidad solicitada no existe. |
Warning |
Operación completada con advertencias. |
CancelOperation |
La petición fue cancelada. |
ValidationError |
HTTP 400: Error de validación en los datos de entrada. |
DataError |
Error en los datos procesados. |
DatabaseError |
Error al acceder a la base de datos. |
ConcurrencyError |
HTTP 409: Error de concurrencia al actualizar datos. |
ConnectionError |
Error de red (ej. SocketException). |
HttpError |
Ocurrió un error HTTP inesperado. |
Error |
Ocurrió un error a nivel de negocio. |
Unauthorized |
HTTP 401: Se requiere autenticación. |
Forbidden |
HTTP 403: El servidor rechazó autorizar la petición. |
NotFound |
HTTP 404: El recurso solicitado no fue encontrado. |
Conflict |
HTTP 409: La petición entra en conflicto con el estado actual. |
UnprocessableEntity |
HTTP 422: Errores semánticos en la petición. |
TooManyRequests |
HTTP 429: Límite de peticiones excedido. |
ServiceUnavailable |
HTTP 503: El servidor no está disponible temporalmente. |
ChangePassword |
El servidor indica que se requiere cambio de contraseña. |
Exception |
Ha ocurrido una excepción no controlada. |
Uso
1. Define tu Servicio
Hereda de RestServiceBase e inyecta IHttpClientFactory (recomendado) o pasa un HttpClient directamente.
public interface IProductService
{
Task<ResultEntities<Product>> GetAllAsync(CancellationToken ct = default);
Task<ResultPaged<Product>> GetPagedAsync(int page, int pageSize, CancellationToken ct = default);
Task<ResultEntity<Product>> GetByIdAsync(int id, CancellationToken ct = default);
Task<ResultEntity<Product>> CreateAsync(Product product, CancellationToken ct = default);
Task<ResultEntity<Product>> UpdateAsync(int id, Product product, CancellationToken ct = default);
Task<ResultEntity<Product>> PatchAsync(int id, object patch, CancellationToken ct = default);
Task<Result> DeleteAsync(int id, CancellationToken ct = default);
Task<Result> DeactivateAsync(int id, string reason, CancellationToken ct = default);
Task<ResultEntity<Stream>> GetExportStreamAsync(int id, CancellationToken ct = default);
}
public class ProductService : RestServiceBase, IProductService
{
// Recomendado: usar IHttpClientFactory para gestión correcta de conexiones y logging
public ProductService(IHttpClientFactory factory, ILogger<ProductService> logger)
: base(factory, nameof(ProductService), logger) { }
public Task<ResultEntities<Product>> GetAllAsync(CancellationToken ct = default)
=> GetsAsync<Product>("api/products", ct);
public Task<ResultPaged<Product>> GetPagedAsync(int page, int pageSize, CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products", new PagingParameters { Page = page, PageSize = pageSize }, ct);
public Task<ResultEntity<Product>> GetByIdAsync(int id, CancellationToken ct = default)
=> GetAsync<Product>($"api/products/{id}", ct);
public Task<ResultEntity<Product>> CreateAsync(Product product, CancellationToken ct = default)
=> PostAsync("api/products", product, ct);
public Task<ResultEntity<Product>> UpdateAsync(int id, Product product, CancellationToken ct = default)
=> PutAsync($"api/products/{id}", product, ct);
public Task<ResultEntity<Product>> PatchAsync(int id, object patch, CancellationToken ct = default)
=> base.PatchAsync<object, Product>($"api/products/{id}", patch, ct);
public Task<Result> DeleteAsync(int id, CancellationToken ct = default)
=> base.DeleteAsync($"api/products/{id}", ct);
public Task<Result> DeactivateAsync(int id, string reason, CancellationToken ct = default)
=> base.DeleteAsync($"api/products/{id}", new { Reason = reason }, ct);
public Task<ResultEntity<Stream>> GetExportStreamAsync(int id, CancellationToken ct = default)
=> DownloadStreamAsync($"api/products/{id}/export", ct);
}
2. Registro en Inyección de Dependencias
Usa los métodos de extensión AddRestService en Program.cs (Blazor / MAUI) o App.axaml.cs (Avalonia):
// Opción A — Simple: URL base + opciones JSON opcionales
builder.Services.AddRestService<IProductService, ProductService>(
baseUrl: "https://api.example.com/",
jsonSerializerOptions: new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
// Opción B — SettingRestService: objeto de configuración completo
builder.Services.AddRestService<IProductService, ProductService>(new SettingRestService
{
UriString = "https://api.example.com/",
JsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true },
TokenProvider = new MyTokenProvider(),
DefaultHeaders = new Dictionary<string, string>
{
{ "X-Api-Version", "2" },
{ "X-Tenant-Id", "acme" }
},
Timeout = TimeSpan.FromSeconds(30)
});
// Opción C — Sin interfaz (solo tipo concreto)
builder.Services.AddRestService<ProductService>("https://api.example.com/");
// Opción D — IHttpClientFactory manual (control total)
builder.Services.AddHttpClient(nameof(ProductService), client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
builder.Services.AddScoped<IProductService, ProductService>();
3. Consumir el Servicio
// En un componente Blazor, ViewModel de MAUI o ViewModel de Avalonia:
var result = await _productService.GetPagedAsync(page: 1, pageSize: 20);
if (result.Status == StatusResult.Ok)
{
// result.Items → IEnumerable<Product>
// result.Page → página actual
// result.PageSize
// result.TotalCount
Console.WriteLine($"Página {result.Page} — {result.TotalCount} productos en total");
}
Autenticación — Bearer Token (ITokenProvider)
Implementa ITokenProvider para suministrar tokens desde cualquier fuente (en memoria, SecureStorage en MAUI, AuthenticationStateProvider en Blazor, etc.).
// 1. Implementar ITokenProvider
public class MyTokenProvider : ITokenProvider
{
private string? _token;
public void SetToken(string? token) => _token = token;
public Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(_token);
}
RestServiceBase llama automáticamente a GetTokenAsync antes de cada petición y establece la cabecera Authorization: Bearer {token}. Si el token es null o vacío, la cabecera se elimina.
// 2. Registrar en DI
builder.Services.AddSingleton<ITokenProvider, MyTokenProvider>();
builder.Services.AddRestService<IProductService, ProductService>(new SettingRestService
{
UriString = "https://api.example.com/",
TokenProvider = serviceProvider.GetRequiredService<ITokenProvider>()
});
Blazor WASM — Integración con AuthenticationStateProvider
public class BlazorTokenProvider : ITokenProvider
{
private readonly ILocalStorageService _localStorage;
public BlazorTokenProvider(ILocalStorageService localStorage)
=> _localStorage = localStorage;
public async Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
=> await _localStorage.GetItemAsStringAsync("access_token");
}
MAUI — Integración con SecureStorage
public class MauiTokenProvider : ITokenProvider
{
public async Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
=> await SecureStorage.Default.GetAsync("access_token");
}
Refresco de Token / Reintento 401 (TryRefreshTokenAsync)
Cuando el servidor devuelve HTTP 401, RestServiceBase invoca TryRefreshTokenAsync. Si devuelve true, la petición original se reintenta automáticamente una vez con el nuevo token.
public class ProductService : RestServiceBase, IProductService
{
private readonly IAuthService _authService;
public ProductService(IHttpClientFactory factory, IAuthService authService)
: base(factory, nameof(ProductService))
{
_authService = authService;
}
protected override async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken = default)
{
var refreshed = await _authService.RefreshTokenAsync(cancellationToken);
return refreshed; // true → reintenta la petición original; false → propaga Unauthorized
}
}
Resultados Paginados (GetPagedAsync)
GetPagedAsync devuelve un ResultPaged<T> con metadatos de paginación.
// Con PagingParameters (Page + PageSize)
public Task<ResultPaged<Product>> GetPagedAsync(int page, int pageSize, CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products", new PagingParameters { Page = page, PageSize = pageSize }, ct);
// Con IParameters (filtro personalizado + paginación)
public Task<ResultPaged<Product>> SearchAsync(ProductParameters parameters, CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products", parameters, ct);
// Sin parámetros (query string ya embebida en el path)
public Task<ResultPaged<Product>> GetPagedRawAsync(CancellationToken ct = default)
=> GetPagedAsync<Product>("api/products?page=1&pageSize=10", ct);
Nota:
PagingParametersse serializa como?Page=1&PageSize=20en la query string. Las clasesIParameterspersonalizadas deben implementarGetParameters()devolviendoIEnumerable<KeyValuePair<string, string>>.
Subida de Archivos (UploadAsync)
public async Task<ResultEntity<UploadResponse>> UploadAvatarAsync(Stream fileStream, string fileName, CancellationToken ct = default)
{
var content = new MultipartFormDataContent();
content.Add(new StreamContent(fileStream), "file", fileName);
return await UploadAsync<UploadResponse>("api/users/avatar", content, ct);
}
Descarga de Archivos con Progreso (DownloadAsync)
public async Task<ResultEntity<byte[]>> DownloadReportAsync(
int reportId,
IProgress<long>? progress = null,
CancellationToken ct = default)
{
return await DownloadAsync($"api/reports/{reportId}/download", progress, ct);
}
// Uso — ej. en un ViewModel de MAUI / Avalonia:
var progress = new Progress<long>(bytesRead => Console.WriteLine($"Descargados: {bytesRead} bytes"));
var result = await _reportService.DownloadReportAsync(42, progress, cancellationToken);
if (result.Status == StatusResult.Ok)
{
await File.WriteAllBytesAsync("informe.pdf", result.Entity!, cancellationToken);
}
PATCH — Actualización Parcial (PatchAsync)
Usa PatchAsync para enviar actualizaciones parciales de entidades sin reemplazar el recurso completo.
// Mismo tipo de entrada y salida
public Task<ResultEntity<Product>> PatchNameAsync(int id, string newName, CancellationToken ct = default)
=> PatchAsync($"api/products/{id}", new { Name = newName }, ct);
// Tipos diferentes de entrada y salida
public Task<ResultEntity<ProductDetail>> PatchStatusAsync(int id, StatusPatch patch, CancellationToken ct = default)
=> PatchAsync<StatusPatch, ProductDetail>($"api/products/{id}", patch, ct);
// Tipo de resultado personalizado
public Task<MyCustomResult> PatchWithCustomResultAsync(int id, object patch, MyCustomResult initial, CancellationToken ct = default)
=> PatchAsync($"api/products/{id}", patch, initial, ct);
Manejo de Errores de Conexión
RestServiceBase distingue automáticamente tres escenarios de error:
| Escenario | StatusResult |
Causa |
|---|---|---|
| Cancelado por el usuario | CancelOperation |
OperationCanceledException |
| Sin red / conexión rechazada | ConnectionError |
HttpRequestException + SocketException |
| Error HTTP inesperado | HttpError |
Cualquier otra Exception |
var result = await _productService.GetAllAsync();
switch (result.Status)
{
case StatusResult.Ok:
// Usar los datos del resultado
break;
case StatusResult.ConnectionError:
// Mostrar banner sin conexión / botón de reintento
break;
case StatusResult.Unauthorized:
// Redirigir al login
break;
case StatusResult.ValidationError:
// Mostrar result.Errors al usuario
break;
case StatusResult.CancelOperation:
// Petición cancelada — no se requiere acción
break;
}
Cabeceras Predeterminadas y Timeout (SettingRestService)
var setting = new SettingRestService
{
UriString = "https://api.example.com/",
Timeout = TimeSpan.FromSeconds(15),
DefaultHeaders = new Dictionary<string, string>
{
{ "X-Api-Key", "tu-api-key" },
{ "X-Api-Version", "2" },
{ "X-Tenant-Id", "acme" }
}
};
// Pasar directamente al constructor de RestServiceBase:
public class ProductService : RestServiceBase
{
public ProductService(SettingRestService setting) : base(setting) { }
}
Helpers de Consulta (QueryHelpers)
QueryHelpers proporciona métodos estáticos para componer query strings de forma segura. Todos los valores se codifican en URL automáticamente.
// Clave-valor simple
var uri = QueryHelpers.AddQueryString("api/products", "category", "electronics");
// → "api/products?category=electronics"
// Diccionario
var uri = QueryHelpers.AddQueryString("api/products", new Dictionary<string, string>
{
{ "category", "electronics" },
{ "inStock", "true" }
});
// → "api/products?category=electronics&inStock=true"
// IParameters (ej. PagingParameters o EntityParameters personalizados)
var uri = QueryHelpers.AddQueryString("api/products", new PagingParameters { Page = 2, PageSize = 10 });
// → "api/products?Page=2&PageSize=10"
// Conserva query string y anclas existentes
var uri = QueryHelpers.AddQueryString("api/products?sort=asc#section", "page", "3");
// → "api/products?sort=asc&page=3#section"
Hooks Virtuales (Sobrescribir en Clases Derivadas)
RestServiceBase expone métodos virtuales protegidos para personalizar el comportamiento sin reimplementar el pipeline completo de peticiones:
| Método | Cuándo se invoca | Por defecto |
|---|---|---|
OnUnauthorized(IResult) |
HTTP 401 recibido y no reintentado | sin acción |
OnError(IResult) |
Error HTTP inesperado (distinto de 400/401) | sin acción |
OnChangePassword(IResult) |
El servidor señaliza StatusResult.ChangePassword |
sin acción |
HandleSatisfactoryAsync(...) |
Tras una respuesta HTTP exitosa | Enruta a OnUnauthorized/OnChangePassword/OnError |
HandleErrorsAsync(...) |
Tras una respuesta HTTP fallida | Establece Status, parsea errores 400, invoca OnUnauthorized/OnError |
TryRefreshTokenAsync(...) |
En HTTP 401 antes de propagarlo | Devuelve false |
public class ProductService : RestServiceBase
{
protected override void OnUnauthorized(IResult result)
{
// ej. lanzar un evento, navegar al login, limpiar estado local
}
protected override void OnError(IResult result)
{
// ej. registrar en Sentry, mostrar una notificación toast
}
protected override async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken = default)
{
// Devuelve true si el token fue refrescado y la petición debe reintentarse
return await _authService.RefreshAsync(cancellationToken);
}
}
Todos los Constructores Disponibles
RestServiceBase soporta siete patrones de construcción:
// 1. Solo URI base
protected RestServiceBase(string uriString, ILogger? logger = null)
// 2. URI base + opciones JSON
protected RestServiceBase(string uriString, JsonSerializerOptions jsonSerializerOptions, ILogger? logger = null)
// 3. HttpClient existente
protected RestServiceBase(HttpClient client, ILogger? logger = null)
// 4. HttpClient existente + opciones JSON
protected RestServiceBase(HttpClient client, JsonSerializerOptions jsonSerializerOptions, ILogger? logger = null)
// 5. IHttpClientFactory + nombre de cliente (recomendado para DI)
protected RestServiceBase(IHttpClientFactory httpClientFactory, string clientName, ILogger? logger = null)
// 6. IHttpClientFactory + nombre de cliente + opciones JSON
protected RestServiceBase(IHttpClientFactory httpClientFactory, string clientName, JsonSerializerOptions jsonSerializerOptions, ILogger? logger = null)
// 7. SettingRestService (configuración completa: URI, JSON, token, cabeceras, timeout)
protected RestServiceBase(SettingRestService setting, ILogger? logger = null)
Interfaces
IRestService
Interfaz pública implementada por RestServiceBase. Expone las siguientes propiedades de solo lectura:
public interface IRestService
{
JsonSerializerOptions? JsonSerializerOptions { get; }
HttpClient Client { get; }
ITokenProvider? TokenProvider { get; }
}
ITokenProvider
public interface ITokenProvider
{
Task<string?> GetTokenAsync(CancellationToken cancellationToken = default);
}
Autor
Sebastián Martínez Pérez
Licencia
Copyright © 2021-2026 Pitasoft, S.L.
Licenciado bajo los términos de la LICENCE.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
- Microsoft.Extensions.Http (>= 10.0.5)
- Pitasoft.Result (>= 7.2.4)
-
net8.0
- Microsoft.Extensions.Http (>= 10.0.5)
- Pitasoft.Result (>= 7.2.4)
-
net9.0
- Microsoft.Extensions.Http (>= 10.0.5)
- Pitasoft.Result (>= 7.2.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 8.1.2 | 134 | 4/17/2026 |
| 8.1.1 | 147 | 3/26/2026 |
| 8.0.1 | 134 | 3/24/2026 |
| 7.0.7 | 137 | 3/20/2026 |
| 7.0.6 | 138 | 3/13/2026 |
| 7.0.5 | 144 | 3/2/2026 |
| 7.0.4 | 137 | 2/25/2026 |
| 7.0.3 | 145 | 2/12/2026 |
| 7.0.2 | 155 | 1/26/2026 |
| 7.0.1 | 160 | 1/26/2026 |
| 6.5.5 | 360 | 5/27/2025 |
| 6.5.4 | 338 | 5/27/2025 |
| 6.5.3 | 328 | 5/25/2025 |
| 6.5.2 | 342 | 5/25/2025 |
| 6.5.1 | 290 | 5/23/2025 |
| 6.5.0 | 341 | 5/19/2025 |
| 6.0.0 | 372 | 8/21/2024 |
| 5.2.2 | 377 | 5/17/2024 |
| 5.2.1 | 342 | 5/17/2024 |
| 5.2.0 | 455 | 11/20/2023 |