HTTP Service Wrapper (Blazor)
A single generic service every page/component calls through, instead of injecting HttpClient everywhere. It normalizes errors into the same ApiResponse<T> shape the API returns.
Interface
public interface IHttpService
{
Task<ApiResponse<TResponse>> GetAsync<TResponse>(string url);
Task<ApiResponse<TResponse>> PostAsync<TResponse, TRequest>(string url, TRequest request);
Task<ApiResponse<TResponse>> PutAsync<TResponse, TRequest>(string url, TRequest request);
Task<ApiResponse<TResponse>> DeleteAsync<TResponse>(string url);
Task<ApiResponse<TResponse>> PostIncludeFileAsync<TResponse, TRequest>(
string url, TRequest request, Expression<Func<TRequest, IBrowserFile?>> fileProperty);
}
Implementation — every call funnels through the same try/catch
Network failures, deserialization failures, whatever — they all collapse into one generic ApiResponse.Fail(...), so calling code never has to handle transport errors separately from API-level errors.
public class HttpService : IHttpService
{
private readonly HttpClient _http;
public HttpService(HttpClient http) => _http = http;
public async Task<ApiResponse<TResponse>> GetAsync<TResponse>(string url)
{
try
{
using var response = await _http.GetAsync(url);
return await response.Content.ReadFromJsonAsync<ApiResponse<TResponse>>()
?? throw new ArgumentException();
}
catch { return Error<TResponse>(); }
}
public async Task<ApiResponse<TResponse>> PostAsync<TResponse, TRequest>(string url, TRequest request)
{
try
{
using var response = await _http.PostAsJsonAsync(url, request);
return await response.Content.ReadFromJsonAsync<ApiResponse<TResponse>>()
?? throw new ArgumentException();
}
catch { return Error<TResponse>(); }
}
// PutAsync / DeleteAsync follow the same shape.
private static ApiResponse<TResponse> Error<TResponse>()
=> ApiResponse<TResponse>.Fail("An unexpected error occurred.");
}
File upload via reflection
When a request DTO carries an IBrowserFile alongside plain fields, this builds a multipart/form-data body automatically: it pulls the file out via the expression you pass in, then walks every other property with reflection to add it as a form field. This means a new upload endpoint never needs its own hand-written multipart code — just a DTO shape and a call site.
public async Task<ApiResponse<TResponse>> PostIncludeFileAsync<TResponse, TRequest>(
string url, TRequest request, Expression<Func<TRequest, IBrowserFile?>> fileProperty)
{
try
{
using var content = new MultipartFormDataContent();
var file = fileProperty.Compile()(request);
if (fileProperty.Body is not MemberExpression expr ||
expr.Member is not PropertyInfo fileInfo)
return Error<TResponse>();
if (file is not null)
{
var streamContent = new StreamContent(file.OpenReadStream());
if (file.ContentType.Trim().Length == 0)
return ApiResponse<TResponse>.Fail("Invalid file type.");
streamContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);
content.Add(streamContent, fileInfo.Name, file.Name);
}
foreach (var prop in typeof(TRequest).GetProperties())
{
if (prop.Name == fileInfo.Name) continue;
var value = prop.GetValue(request);
string actual = value switch
{
null => string.Empty,
DateTime dt => dt.ToString("o"),
string s => s,
_ when prop.PropertyType.IsPrimitive || prop.PropertyType.IsEnum || value is decimal
=> value.ToString() ?? string.Empty,
_ => JsonSerializer.Serialize(value)
};
content.Add(new StringContent(actual), prop.Name);
}
using var response = await _http.PostAsync(url, content);
return await response.Content.ReadFromJsonAsync<ApiResponse<TResponse>>()
?? throw new ArgumentException();
}
catch { return Error<TResponse>(); }
}
A feature service on top
Feature-specific services (one per API controller) are thin: each method is a one-line call into IHttpService with the right URL and generic types.
public class ResourceService : IResourceService
{
private readonly IHttpService _http;
public ResourceService(IHttpService http) => _http = http;
public Task<ApiResponse<List<ResourceDto>>> GetAllAsync()
=> _http.GetAsync<List<ResourceDto>>("/Resource");
public Task<ApiResponse<object>> CreateAsync(CreateResourceRequest request)
=> _http.PostAsync<object, CreateResourceRequest>("/Resource", request);
}