Astrol.Shared.Repository envuelve HttpClient para mover la información entre su cliente Blazor y su API: resuelve las URL contra la dirección de la API, envía y lee JSON, sube archivos, añade el idioma y el token, y devuelve siempre un HttpResponseWrapper<T>. No hace falta try/catch: los errores HTTP, los fallos de red y los timeouts llegan en el propio resultado.
Instale el paquete y regístrelo en Program.cs después del HttpClient. La dirección puede ser fija o salir de ClientSetting (producción o desarrollo). Las URL relativas ("api/clients" o "/api/clients") se resuelven contra esa dirección; si la dirección tiene ruta, termínela en / (https://host/api/). También se aceptan URL absolutas http/https.
dotnet add package Astrol.Shared.Repository// Program.csusing Astrol.Shared.Repository; builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); // a) Fixed API URLbuilder.Services.AddAstrolRepository("https://api.my-app.com/"); // b) From appsettings.json (wwwroot/appsettings.json in WebAssembly)var clientSetting = builder.Configuration.GetSection("ClientSetting").Get<ClientSetting>()!;builder.Services.AddAstrolRepository(clientSetting, options =>{ options.DefaultLanguage = null; // null = app language (CultureInfo.CurrentUICulture) options.ThrowExceptions = false; // network errors in the wrapper, no exceptions options.UseJsonContext(AppJsonContext.Default); // source-generated JSON (section 6)});// wwwroot/appsettings.json{ "ClientSetting": { "EnvironmentProduction": false, "Address": { "Development": "https://localhost:7001/", "Production": "https://api.my-app.com/" } }}@using Astrol.Shared.Repository@inject IRepository Repository <AstrolButton Text="Read JSON" OnClick='() => Load("sample-data/clients.json")' /><AstrolButton Text="Error 404" OnClick='() => Load("sample-data/missing.json")' /><AstrolButton Text="No connection" OnClick='() => Load("http://localhost:9/api/clients")' /> @if (_result is not null){ <div>IsSuccess: @_result.IsSuccess</div> <div>StatusCode: @_result.StatusCode</div> <div>IsNetworkError: @_result.IsNetworkError</div> <div>ErrorMessage: @_result.ErrorMessage</div>}@code { private HttpResponseWrapper<List<ClientDto>>? _result; private async Task Load(string url) { _result = await Repository.Get<List<ClientDto>>(url); } public class ClientDto { public int Id { get; set; } public string? FullName { get; set; } public decimal Points { get; set; } }}Inyecte IRepository y compruebe IsSuccess (o Error). Si falló, ErrorMessage ya trae el mensaje listo para mostrar, StatusCode el estado HTTP (null si no hubo respuesta) e IsNetworkError indica si el servidor no respondió.
@inject IRepository Repository@inject IAstrolToast Toast // GET: list, object, text (Get<string>) or file (Get<byte[]>)var clients = await Repository.Get<List<ClientDto>>("api/clients?page=1");if (clients.IsSuccess) _clients = clients.Response;else Toast.Error(clients.ErrorMessage!); // POST / PUT / PATCH without and with a responsevar saved = await Repository.Post("api/clients", newClient);var created = await Repository.Post<ClientDto, ClientDto>("api/clients", newClient);var updated = await Repository.Put<ClientDto, ClientDto>($"api/clients/{id}", client);var patched = await Repository.Patch($"api/clients/{id}", new { FullName = "Ana" }); // DELETEvar deleted = await Repository.Delete($"api/clients/{id}"); // Telling errors apartif (deleted.IsNetworkError) Toast.Warning("No connection to the server.");else if (deleted.StatusCode == HttpStatusCode.Unauthorized) Navigation.NavigateTo("/login");else if (deleted.Error) Toast.Error(deleted.ErrorMessage!); // Language and cancellation (e.g. when leaving the page)var result = await Repository.Get<ClientDto>("api/clients/1", "en", _cts.Token); // Any other content or methodvar csv = await Repository.Send<string>(HttpMethod.Post, "api/report", new StringContent("a;b"));Post<TResponse>(url, files) y Put<TResponse>(url, files) envían una lista de IBrowserFile (por ejemplo la de AstrolUploadFile) como multipart/form-data. Con fieldName cambia el nombre del campo y con fields añade campos de texto.
<AstrolUploadFile Status="@_statusUpload" Save="SaveFiles" OnClose="s => _statusUpload = s" />@code { private bool _statusUpload; private async Task SaveFiles(List<IBrowserFile> files) { var result = await Repository.Post<List<string>>("api/documents", files, fieldName: "files", // form field name (default "files") fields: new Dictionary<string, string> { ["folder"] = "invoices" }); // extra text fields if (result.Error) throw new Exception(result.ErrorMessage); // AstrolUploadFile shows it as an error _statusUpload = false; }}// API (ASP.NET Core)[HttpPost("api/documents")]public async Task<List<string>> Upload([FromForm] List<IFormFile> files, [FromForm] string folder) { ... }Registre una implementación de IRepositoryTokenProvider y cada petición llevará Authorization: Bearer {token}. No modifique HttpClient.DefaultRequestHeaders: el HttpClient se comparte en toda la aplicación.
public class TokenProvider : IRepositoryTokenProvider{ private readonly IJSRuntime _js; public TokenProvider(IJSRuntime js) => _js = js; // null or empty = the request is sent without a token public async ValueTask<string?> GetTokenAsync(CancellationToken cancellationToken = default) => await _js.InvokeAsync<string?>("localStorage.getItem", cancellationToken, "token");} // Program.csbuilder.Services.AddScoped<IRepositoryTokenProvider, TokenProvider>();builder.Services.AddAstrolRepository(clientSetting);Por defecto el JSON se lee y escribe con reflexión: en WebAssembly, la primera vez que se usa cada tipo hay que analizarlo en el navegador, lo que retrasa la primera carga de cada pantalla, y la reflexión impide que el trimming reduzca el tamaño de la aplicación. Con UseJsonContext los tipos que declare en un JsonSerializerContext usan código generado al compilar: sin reflexión y más rápido desde la primera llamada.
using System.Text.Json.Serialization; // One class per project: list every type you send or read with IRepository// (also the collections: List<ClientDto>, ClientDto[] ...).[JsonSerializable(typeof(ClientDto))][JsonSerializable(typeof(List<ClientDto>))][JsonSerializable(typeof(UserToken))]internal partial class AppJsonContext : JsonSerializerContext{} // Program.csbuilder.Services.AddAstrolRepository(clientSetting, options => options.UseJsonContext(AppJsonContext.Default));IRepository (todos los métodos aceptan language y cancellationToken opcionales):
HttpResponseWrapper<T>:
RepositoryOptions (AddAstrolRepository(url, options => ...)):