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

IHttpService.cs
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.

HttpService.cs
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.

HttpService.cs — PostIncludeFileAsync
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.

ResourceService.cs
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);
}