AstrolUI AstrolUI
Yellow Orange Red Green Blue Purple Custom color
Light mode
Repository

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.

1. Installation process

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.cs
using Astrol.Shared.Repository;
 
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
 
// a) Fixed API URL
builder.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/"
 }
 }
}

2. Demo IRepository

Note:
This documentation registers the repository with its own address. The buttons read a JSON file from the site, request a file that does not exist (404) and call a server that is down (network failure): none of them throws an exception.
@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; }
 }
}

3. Usage and error handling

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 response
var 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" });
 
// DELETE
var deleted = await Repository.Delete($"api/clients/{id}");
 
// Telling errors apart
if (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 method
var csv = await Repository.Send<string>(HttpMethod.Post, "api/report", new StringContent("a;b"));

4. Uploading files

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) { ... }

5. Authentication token

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.cs
builder.Services.AddScoped<IRepositoryTokenProvider, TokenProvider>();
builder.Services.AddAstrolRepository(clientSetting);

6. Performance: source-generated JSON

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.

  • No call and no JSON changes: names follow the policy of RepositoryOptions.JsonSerializerOptions (camelCase), not the context's JsonSourceGenerationOptions.
  • Types that are not in the context (e.g. anonymous types in Patch) keep working with reflection, unless the app turns it off (JsonSerializerIsReflectionEnabledByDefault=false).
  • Declare the types in .cs files, not inside a .razor (@code): the JSON generator cannot see classes declared in components.
  • This documentation uses it: the demo above reads List<ClientDto> with generated code.
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.cs
builder.Services.AddAstrolRepository(clientSetting, options => options.UseJsonContext(AppJsonContext.Default));

Supported parameters

IRepository (every method accepts optional language and cancellationToken):

  • Get<T>(url): reads JSON; with T = string it returns the text and with T = byte[] the bytes (downloads).
  • Post<T>(url, data), Put<T>, Patch<T>: send data as JSON without reading the response.
  • Post<T, TResponse>(url, data), Put<T, TResponse>, Patch<T, TResponse>: send JSON and read the response.
  • Delete(url) and Delete<T>(url).
  • Post<TResponse>(url, files, fieldName = "files", fields = null) and Put<TResponse>(...): files as multipart/form-data.
  • Send<TResponse>(method, url, content): any method and any HttpContent.
  • language (string): Accept-Language header; null (default) uses the app language.
  • cancellationToken: cancels the request; cancellation is thrown as OperationCanceledException.

HttpResponseWrapper<T>:

  • IsSuccess / Error (bool): result of the request.
  • Response (T): content read; default when it failed or the response was empty (204).
  • StatusCode (HttpStatusCode?): HTTP status; null when there was no response.
  • ErrorMessage (string): detail or title of a ProblemDetails, the validation errors messages (one per line), message/error of a JSON, the body text or the HTTP status.
  • IsNetworkError (bool): the server could not be reached (offline, DNS, CORS or timeout).
  • Exception (Exception): captured exception (network, timeout or invalid JSON).
  • HttpResponseMessage and GetBody(): the original response and its body as text (can be read several times). On a network failure it is a 503 response (408 on a timeout).

RepositoryOptions (AddAstrolRepository(url, options => ...)):

  • JsonSerializerOptions: JsonSerializerDefaults.Web by default (camelCase, case-insensitive). Set TypeInfoResolver to use source-generated JSON (faster and lighter in WebAssembly).
  • UseJsonContext(context): uses source-generated JSON for the context types (see section 6).
  • DefaultLanguage (string): language when the call does not give one (default null = current culture).
  • ThrowExceptions (bool): true throws network failures, timeouts and invalid JSON as before (default false).
  • AuthorizationScheme (string): token scheme (default Bearer).
  • ConfigureRequest (Action<HttpRequestMessage>): to add your own headers to every request.
Markdown for AI © 2026 AstrolUI v10.0.13