Pitasoft.Client 7.0.6

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pitasoft.Client" Version="7.0.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pitasoft.Client" Version="7.0.6" />
                    
Directory.Packages.props
<PackageReference Include="Pitasoft.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pitasoft.Client --version 7.0.6
                    
#r "nuget: Pitasoft.Client, 7.0.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pitasoft.Client@7.0.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pitasoft.Client&version=7.0.6
                    
Install as a Cake Addin
#tool nuget:?package=Pitasoft.Client&version=7.0.6
                    
Install as a Cake Tool

Pitasoft.Client

NuGet Version NuGet Downloads License .NET Build Status

English | Castellano


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: RestServiceBase provides methods for GET, POST, PUT, PATCH, and DELETE operations, including batch processing and body-based deletions.
  • Logging Support: Built-in integration with ILogger for 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 StatusResult values.
  • Paginated Results: Built-in support for ResultPaged<T> via GetPagedAsync, with PagingParameters and IParameters overloads, and IsEmpty convenience property.
  • Bearer Token Authentication: Automatic injection of Authorization: Bearer headers via the ITokenProvider interface.
  • Token Refresh / 401 Retry: Override TryRefreshTokenAsync to implement refresh token logic with automatic request retry.
  • File Upload: UploadAsync sends MultipartFormDataContent for uploading files and form data.
  • File Download with Progress: DownloadAsync streams binary content with optional IProgress<long> reporting.
  • Connection Error Detection: Distinguishes network-level errors (SocketException) from generic HTTP errors using StatusResult.ConnectionError.
  • Modern HttpClient Management: Full support for IHttpClientFactory for efficient connection management, socket pooling, and DI integration.
  • DI Extensions: AddRestService extension methods for registering services fluently in Blazor, MAUI, and Avalonia Program.cs / MauiProgram.cs.
  • Flexible Configuration: SettingRestService centralizes base URL, JSON options, token provider, default headers, and request timeout.
  • Query Helpers: QueryHelpers safely 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.Result for consistent and expressive response handling.

Performance Optimizations (.NET 8/9/10)

  • Zero-allocation URI parsing: ReadOnlySpan<char> and AsSpan() avoid unnecessary string allocations during query string manipulation.
  • Fast character search: SearchValues<char> enables ultra-fast detection of ? and # control characters.
  • Memory efficiency: StringBuilder pre-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: PagingParameters serializes as ?Page=1&PageSize=20 in the query string. Custom IParameters classes must implement GetParameters() returning IEnumerable<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: RestServiceBase proporciona 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 ILogger para 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> mediante GetPagedAsync, con sobrecargas para PagingParameters e IParameters, y propiedad de conveniencia IsEmpty.
  • Autenticación con Bearer Token: Inyección automática de cabeceras Authorization: Bearer a través de la interfaz ITokenProvider.
  • Refresco de Token / Reintento 401: Sobrescribe TryRefreshTokenAsync para implementar lógica de refresh token con reintento automático de la petición.
  • Subida de Archivos: UploadAsync envía MultipartFormDataContent para subir ficheros y formularios.
  • Descarga de Archivos con Progreso: DownloadAsync descarga contenido binario con reporte opcional mediante IProgress<long>.
  • Detección de Error de Conexión: Distingue errores de red (SocketException) de errores HTTP genéricos usando StatusResult.ConnectionError.
  • Gestión Moderna de HttpClient: Soporte completo para IHttpClientFactory con gestión eficiente de conexiones, pooling de sockets e integración con DI.
  • Extensiones de DI: Métodos de extensión AddRestService para registrar servicios de forma fluida en Program.cs / MauiProgram.cs de Blazor, MAUI y Avalonia.
  • Configuración Flexible: SettingRestService centraliza URL base, opciones JSON, proveedor de token, cabeceras predeterminadas y timeout de petición.
  • Helpers de Consulta: QueryHelpers construye query strings de forma segura y eficiente desde diccionarios, IParameters o 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.Result para un manejo de respuestas consistente y expresivo.

Optimizaciones de Rendimiento (.NET 8/9/10)

  • Análisis de URIs sin asignaciones: ReadOnlySpan<char> y AsSpan() 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 StringBuilder y 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: PagingParameters se serializa como ?Page=1&PageSize=20 en la query string. Las clases IParameters personalizadas deben implementar GetParameters() devolviendo IEnumerable<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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed