Astrol.Shared.Repository wraps HttpClient to move data between your Blazor client and your API: it resolves URLs against the API address, sends and reads JSON, uploads files, adds the language and the token, and always returns an HttpResponseWrapper<T>. No try/catch needed: HTTP errors, network failures and timeouts come back in the result itself.
Install the package and register it in Program.cs after the HttpClient. The address can be fixed or come from ClientSetting (production or development). Relative URLs ("api/clients" or "/api/clients") are resolved against that address; if the address has a path, end it with / (https://host/api/). Absolute http/https URLs are accepted too.
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; } }}Inject IRepository and check IsSuccess (or Error). When it failed, ErrorMessage already has a message ready to show, StatusCode the HTTP status (null when there was no response) and IsNetworkError tells whether the server did not answer.
@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) and Put<TResponse>(url, files) send a list of IBrowserFile (for example the one from AstrolUploadFile) as multipart/form-data. Use fieldName to change the field name and fields to add text fields.
<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) { ... }Register an implementation of IRepositoryTokenProvider and every request will carry Authorization: Bearer {token}. Do not modify HttpClient.DefaultRequestHeaders: the HttpClient is shared by the whole app.
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);By default JSON is read and written with reflection: in WebAssembly each type has to be analyzed in the browser the first time it is used, which delays the first load of each screen, and reflection prevents trimming from shrinking the app. With UseJsonContext, the types you declare in a JsonSerializerContext use code generated at compile time: no reflection and faster from the first call.
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 (every method accepts optional language and cancellationToken):
HttpResponseWrapper<T>:
RepositoryOptions (AddAstrolRepository(url, options => ...)):