3

I built my API service and now I want to consume the information from a WPF application.
So far I created the class ApiHelper which initializes and provides the HttpClient used to call the API endpoints and the class MemberService which exposes the methods to get the data that it retrieves from the API.

public class ApiHelper : IApiHelper
{
    private readonly IConfiguration _config;

    public HttpClient ApiClient { get; private set; }

    public ApiHelper(IConfiguration config)
    {
        _config = config;
        InitializeClient();

    }

    private void InitializeClient()
    {
        string apiBase = _config["api"];

        ApiClient = new HttpClient
        {
            BaseAddress = new Uri(apiBase)
        };

        ApiClient.DefaultRequestHeaders.Clear();
        ApiClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

This is the service:

public class MemberService : IMemberService
{
    private readonly IApiHelper _apiHelper;

    public MemberService(IApiHelper apiHelper)
    {
        _apiHelper = apiHelper;
    }

    public async Task<List<Member>> GetAllMembersAsync()
    {
        using (HttpResponseMessage response = await _apiHelper.ApiClient.GetAsync("/api/Members"))
        {
            // If the response has a successful code...
            if (response.IsSuccessStatusCode)
            {
                // Read the response
                var result = await response.Content.ReadAsAsync<List<MemberResponse>>();
                var output = AutoMapping.Mapper.Map<List<Member>>(result);
                return output;
            }
            else
            // If the response is not successful
            {
                // Error handling here
            }
        }

        return null;
    }

My question is not about handling errors such as HttpRequestException that I could handle with a try catch block, but how to handle responses with a status code not in the 2xx range.
In particular, the endpoints of the API are token-protected so a user has to authenticate before making a request and after the token expires, it needs to be refreshed.
What I'd like to do is when the response has a status of 401 - Unauthorized, instead of throwing an exception or returning an object with an error message, the method should try to refresh the token (calling the appropriate method in another service) because it could be expired and just if the call fails again, throw the exception. I'd like to have this behavior for all the API calls.
It's the first time I work with API calls, so this is what I thought to do, but I'd like to have some tips about how to do it. What is the best approach to achive this? Or do you suggest another way to make API calls and manage its responses?

1

What I'd like to do is when the response has a status of 401 - Unauthorized, instead of throwing an exception or returning an object with an error message, the method should try to refresh the token (calling the appropriate method in another service) because it could be expired and just if the call fails again, throw the exception. I'd like to have this behavior for all the API calls.

That sounds like a great idea.

Let's start with a helper method to do exactly that:

public class ApiHelper : IApiHelper
{
    ...
    public async HttpContent GetAuthenticatedAsync(string requestUri)
    {
        var response = await ApiClient.GetAsync(requestUri);

        // Pseudo code:
        //
        // if (200): return response.Content
        // if (401): 
        //   refresh token
        //   try GetAsync again
        //   if (200): return response
        //   else: throw meaningful exception - something is wrong with our credentials
        // else: throw meaningful exception - unexpected return value
        ...
    }
}

Then I'd use encapsulation to ensure that your business logic can't accidentally bypass your helper method: Make ApiClient private and add new low-level helper methods as needed.

You have now taken the first step to abstract away the transport mechanism. This improves readability of your business logic, since the code there can now concentrate on solving problems in your problem domain, rather than dealing with issues on the transport layer. If you always use the same deserialization logic, it might make sense to put that into ApiHelper as well (e.g. some public async TResult GetAuthenticatedAsync<TResult, TResultMessage>(string requestUri)).

It will also make unit testing easier: Authentication issues should be tested by testing ApiHelper, not by testing MemberService.

| improve this answer | |
  • you would be better off using a handler for this – Ewan May 11 at 15:07
  • While I'm aware you wrote pseudocode, I would point out that repeating the if (200): return response.Content logic is not a great approach. It would be much better to use a loop to not have to copy/paste the status checking logic. – Flater May 11 at 15:28
  • @Flater: I'm not sure I agree: Since the status checking logic is trivial (if (response.IsSuccessStatusCode)), adding a loop to avoid duplicating that check would actually reduce readability. And I'm not a big fan for using loops for "do this twice but with slightly different logic" scenarios - I prefer moving common code into separate functions for that. But in that case, the implementers of HttpResponse already did that for us. – Heinzi May 11 at 15:46
  • @Heinzi: You're assuming a single retry, but that's not a given. The logic is always the same: try, if success return, if error attempt to correct and repeat (unless retry count exceeded). There's no "slightly different" logic inbetween loops. Copy/pasting code (which is what the if 200 return content; is) detracts from maintainability. – Flater May 11 at 15:57
  • 1
    @Flater: Followed by "In particular...", describing the concrete problem that the OP wants solved. ;-) But let's not argue about interpreting the OP's words, I think we both agree on the technical points. I was about to suggest that you write your own answer with a generic retry logic, but this has already been asked and answered. – Heinzi May 12 at 9:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.