HttpClient

Creating an HTTP REST client without external dependencies

Using a specific REST library like Refit is the recommended way. Even though I have personally experienced that Refit would not have been allowed for any of my customers, this may well be the case.

.NET provides everything to create a JSON or XML-based REST client without dependencies. The disadvantage here is, of course, that you have to program out everything yourself - even the special cases.

Therefore, you must first create a wrapper that handles the HTTP communication. For simple Get or Post this is relatively simple, but can also become very complex.

Sample

public abstract class MyJsonHttpClient
{
    protected HttpClient HttpClient { get; }
    private string BaseAddress { get; }

    public MyJsonHttpClient(HttpClient httpClient, string baseAddress)
    {
        HttpClient = httpClient;
        BaseAddress = baseAddress;
    }

    protected async Task<T?> InternalGetAsync<T>(string uri)
    {
        using HttpResponseMessage response 
            = await HttpClient.GetAsync(BaseAddress + uri).ConfigureAwait(false);

        return await DeserializeResponse<T>(response).ConfigureAwait(false);
    }

    protected async Task<TResponse?> InternalPostAsync<TBody, TResponse>(string uri, TBody body)
    {
        string jsonContent = JsonSerializer.Serialize(body, s_jsonSerializerOptions);
        using StringContent httpContent = new(jsonContent, Encoding.UTF8, "application/json");

        using HttpResponseMessage response 
            = await HttpClient.PostAsync(BaseAddress + uri, httpContent).ConfigureAwait(false);

        return await DeserializeResponse<TResponse>(response).ConfigureAwait(false);
    }

    protected JsonSerializerOptions s_jsonSerializerOptions 
      = new() { PropertyNameCaseInsensitive = true };
    protected async Task<T?> DeserializeResponse<T>(HttpResponseMessage responseMessage)
    {
        Stream bodyStream
            = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
        return await JsonSerializer
                          .DeserializeAsync<T>(bodyStream, s_jsonSerializerOptions)
                          .ConfigureAwait(false);
    }
}

This abstract base class can now be used to build your own API. The abstraction keeps the actual API implementation lean.

public class MyTaskAPI : MyJsonHttpClient
{
    public MyTaskAPI(HttpClient httpClient, string baseAddress)
        : base(httpClient, baseAddress)
    { }

    public Task<TaskApiModel?> GetTask(int id)
        => InternalGetAsync<TaskApiModel>($"/tasks/{id}");

    public Task<TaskApiModel[]?> GetAllTasks()
        => InternalGetAsync<TaskApiModel[]>("/tasks");

    public Task<TaskApiModel?> AddTask(TaskApiCreateModel newTask)
        => InternalPostAsync<TaskApiCreateModel, TaskApiModel>("/tasks", newTask);
}

The implementation can then - as with the other libraries - simply be used to query, create or manipulate the corresponding resources.

HttpClient httpClient = new HttpClient();   // single instance or use HttpClientFactory
MyTaskAPI myApi = new MyTaskAPI(httpClient, "https://my-rest-api.com");

// Get single task
TaskApiModel? task = await myApi.GetTask(123);

// Get all tasks
TaskApiModel[]? tasks = await myApi.GetAllTasks();

// Add new task
TaskApiCreateModel newTask = new TaskApiCreateModel { Title = "Buy flowers" };
TaskApiModel? addedTask = await myApi.AddTask(newTask);
Pros

  • No dependency
  • Full implementation control

Cons

  • Very time-consuming, especially for optimizations
  • Very high test effort (unit tests + integration tests)
  • High risk of error
  • High maintenance effort

Full Sample

See the full sample on GitHub: https://github.com/BenjaminAbt/dotnet.rest-samples