2
\$\begingroup\$

This is our company's first attempt at an API and a mobile friendly web project, primarily we are desktop developers. We are using Identity Server 4 for serving up tokens and authentication, we have a mobile friendly web application and an API. I did a lot of the groundwork setting up the communication between the application and the API, and landed on the following wrapper framework.

public class ServiceBase
{
    public ServiceBase(IConfiguration config, IHttpContextAccessor httpContextAccessor) 
    {           
        ApiDomain = config["IdentityServer:ApiResource"];

        // .Result property causes the async operation to halt until it resolves.
        AccessToken = httpContextAccessor.HttpContext.GetTokenAsync("access_token").Result;


        ApiClient = new ApiHttpClient(ApiDomain, AccessToken);

    }

    protected ApiHttpClient ApiClient { get; private set; }
    protected string ApiDomain { get; private set; }
    protected string AccessToken { get; private set; }

}

This is the HttpClient wrapper class that I am a little worried about - I did see a Visual Studio twitch session in which the presenters were talking about the IHttpClientFactory service methods, which got me to thinking that we should refactor.

public class ApiHttpClient: IApiHttpClient
{
    // Article used to develop the asyn/await pattern used
    // https://stackoverflow.com/questions/15907356/how-to-initialize-an-object-using-async-await-pattern

    //Article used to develop the JSON serialize/deserialize pattern used 
    // https://stackoverflow.com/questions/16019729/deserializing-json-object-into-a-c-sharp-list

    public ApiHttpClient() { }

    private HttpClient _httpClient;
    public ApiHttpClient(string baseApiUrl, string token)
    {
        BaseApiUrl = baseApiUrl;
        Token = token;
        _httpClient = new HttpClient();
        _httpClient.SetBearerToken(token);
    }

    public string BaseApiUrl { get; set; }
    public string Token { get; set; }
    public string FullApiUrl { get; set; }


    public async Task<string> ApiClientAsyncString(string partialUri)
    {
        ValidateUrl(partialUri);
        var content = await _httpClient.GetStringAsync(FullApiUrl);

        return JArray.Parse(content).ToString();
    }

    public async Task<string> ApiClientAsyncStringNonJson(string partialUri)
    {
        ValidateUrl(partialUri);
        var content = await _httpClient.GetStringAsync(FullApiUrl);

        return content.ToString();
    }

    public async Task<ApiContentResponse<T>> GetAsync<T>(string partialUrl) where T : class
    {
        ValidateUrl(partialUrl);

        var response = await _httpClient.GetAsync(FullApiUrl);
        return new ApiContentResponse<T>(response);

    }
    public async Task<ApiContentResponse<T>> PostAsync<T>(string partialUrl, T modelView) where T : class
    {
        ValidateUrl(partialUrl);

        var json = JsonConvert.SerializeObject(modelView);
        var postContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(FullApiUrl, postContent);

        return new ApiContentResponse<T>(response);
    }

    public async Task<ApiContentResponse<U>> PostAsync<T,U>(string partialUrl, T modelView) where T : class where U : class
    {
        ValidateUrl(partialUrl);

        var json = JsonConvert.SerializeObject(modelView);
        var postContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(FullApiUrl, postContent);

        return new ApiContentResponse<U>(response);
    }


    public async Task<ApiContentResponse<T>> PostAsync<T,K>(string partialUrl, K criteria) 
        where T: class 
        where K: class
    {
        ValidateUrl(partialUrl);

        var json = JsonConvert.SerializeObject(criteria);
        var postContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(FullApiUrl, postContent);

        return new ApiContentResponse<T>(response);
    }

    public async Task<ApiValueContentResponse<T>> PostValueAsync<T, K>(string partialUrl, K criteria) 
        where K : class
    {
        ValidateUrl(partialUrl);

        var json = JsonConvert.SerializeObject(criteria);
        var postContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(FullApiUrl, postContent);

        return new ApiValueContentResponse<T>(response);
    }

    public async Task<ApiContentResponse<T>> DeleteAsync<T>(string partialUrl) where T : class
    {
        ValidateUrl(partialUrl);

        var response = await _httpClient.DeleteAsync(FullApiUrl);
        return new ApiContentResponse<T>(response);


    }

    private void ValidateUrl(string partialUri)
    {
        if (string.IsNullOrWhiteSpace(BaseApiUrl))
        {
            throw new ArgumentNullException("BaseApiUrl cannot be null or empty.");
        }

        if (string.IsNullOrWhiteSpace(Token))
        {
            throw new ArgumentNullException("Token cannot be null or empty.");
        }

        if (string.IsNullOrWhiteSpace(partialUri))
        {
            throw new ArgumentNullException("partialUri cannot be null or empty.");
        }

        FullApiUrl = UrlCombine(BaseApiUrl, partialUri);
    }

    private string UrlCombine(string partialUrl1, string partialUrl2)
    {
        if(partialUrl1.Length == 0)
        {
            return partialUrl2;
        }
        if(partialUrl2.Length == 0)
        {
            return partialUrl1;
        }

        partialUrl1 = partialUrl1.TrimEnd(new char[] { '/', '\\' });
        partialUrl2 = partialUrl2.TrimStart(new char[] { '/', '\\' });
        return $"{partialUrl1}/{partialUrl2}";
    }

Following are response classes created to better manage the responses and standardize handling the responses.

public sealed class ApiContentResponse<T> where T : class
{
    private readonly HttpResponseMessage _httpResponseMessage;
    private Task _initializationTask;

    public ApiContentResponse(HttpResponseMessage httpResponseMessage)
    {
        _httpResponseMessage = httpResponseMessage;
        _initializationTask = InitializeAsync();
    }

    private async Task InitializeAsync()
    {
        var content = await _httpResponseMessage.Content.ReadAsStringAsync();
        Content = JsonConvert.DeserializeObject<T>(content);
        Error = _httpResponseMessage.ReasonPhrase;
        RequestMessage = _httpResponseMessage.RequestMessage;
        StatusCode = _httpResponseMessage.StatusCode;
        Headers = _httpResponseMessage.Headers;
        Version = _httpResponseMessage.Version;
        IsSuccess = _httpResponseMessage.IsSuccessStatusCode;

    }

    public string Error { get; set; }
    public T Content { get; set; }
    public HttpHeaders Headers { get; set; }
    public HttpStatusCode StatusCode { get; set; }
    public HttpRequestMessage RequestMessage { get; set; }
    public Version Version { get; set; }
    public bool IsSuccess { get; set; }

}

public sealed class ApiValueContentResponse<T>
{
    private readonly HttpResponseMessage _httpResponseMessage;
    private Task _initializationTask;

    public ApiValueContentResponse(HttpResponseMessage httpResponseMessage)
    {
        _httpResponseMessage = httpResponseMessage;
        _initializationTask = InitializeAsync();
    }

    private async Task InitializeAsync()
    {
        var content = await _httpResponseMessage.Content.ReadAsStringAsync();
        Content = JsonConvert.DeserializeObject<T>(content);
        Error = _httpResponseMessage.ReasonPhrase;
        RequestMessage = _httpResponseMessage.RequestMessage;
        StatusCode = _httpResponseMessage.StatusCode;
        Headers = _httpResponseMessage.Headers;
        Version = _httpResponseMessage.Version;
        IsSuccess = _httpResponseMessage.IsSuccessStatusCode;

    }

    public string Error { get; set; }
    public T Content { get; set; }
    public HttpHeaders Headers { get; set; }
    public HttpStatusCode StatusCode { get; set; }
    public HttpRequestMessage RequestMessage { get; set; }
    public Version Version { get; set; }
    public bool IsSuccess { get; set; }

}

And an example usage of the above classes.

public interface ITestService 
{
    List<TestModel> GetTestObjects() {get; set;}        
}

public class TestService: ServiceBase, ITestService
{
    public List<TestModel> GetListOfTest()
    {

        var apiResults = ApiClient.GetAsync<ApiTest_DTO>($"api/testlist").Result;
        var testModels = new List<TestModel>();
        // transformation from list of DTO to Model objects

        return testModels;
    }
}

public class TestController : Controller
{
    private readonly ITestService _testService;

    public TestController(ITestService testService)
    {
        _testService = testService;
    }

    public async Tast<IActionResult> Index()
    {
        return View(_testService.GetListOfTest());
    }
}

Let me know if I am missing anything, I believe I have added the necessary code that I would like reviewed. We are getting closer to ready for a beta production release and I am getting nervous.

\$\endgroup\$
3

1 Answer 1

5
\$\begingroup\$

Async/Await

In a couple of places, you use Task.Result instead of await Task. This answer explains more verbosely why this is considered bad practice, but the simple explanation is that Task.Result waits synchronously while await Task waits asynchronously.

HttpClient

You are correct that how the HttpClient is used can be improved. Since you are in dotnet core you have the ability to use DI and let the framework do the managing of the lifetime of your HttpClients. There are a couple of ways to design this, and it depends on how the ApiHttpClient class is used and what its lifespan is. If the class is short-lived (created for a controller to handle a request) then it can take an HttpClient as the only parameter in the constructor. This requires a bit of setup in the startup.cs methods for the instance to have the correct token and base url. If the class is longer-lived then it can take an IHttpClientFactory and ask for an instance of HttpClient as it needs it.

General

ApiHttpClient

The ApiHttpClient class looks to be a helper class at first glance, but the class properties seem to be leaking information outside that indicates not everything is encapsulated as it should.

  1. The constructor takes in and then sets the BaseApiUrl, but that BaseApiUrl has a public getter/setter. My assumption is that this should not change over the lifespan of the class instance thus should not be settable and probably does not need to be gettable either. Both cases are fixed if the constructor takes an HttpClient/IHttpClientFactory instead.
  2. FullApiUrl is set in ValidateUrl and is publicly gettable/settable. This probably shouldn't be a property at all and should be local to the methods in which it is needed.
  3. Token - Same as BaseApiUrl, probably shouldn't be publicly accessible, and with HttpClient refactor won't belong to the class at all.
  4. Code reuse could occur in many of the ApiClientAsync* methods. Consider having ApiClientAsyncString call ApiClientAsyncStringNonJson and then convert the result to Json.

ApiContentResponse/ApiValueContentResponse

  1. The constructor calls an async method and does not await the result. Async Constructors do not exist, hence why the code probably exists in the state it is. Since the task is not awaited, there is no guarantee that once the constructor call is completed that any of the properties are actually set. The task could be waiting on ReadAsStringAsync indefinitely for all we know.
    • The best away that I can think is to provide the result of ReadAsStringAsync directly in the constructor, and possibly not provide the HttpResponseMessage if it is not needed.
    • A second option (if it exists) is to use a synchronous ReadAsString instead, but I would recommend this less than the first option.

Code Confidence

This might be a bit off-topic of the code posted specifically, but because it was mentioned in the original post I want to talk to it.

You mention that you are nearing release to production and you are getting nervous. A way to know that code is working is by testing it. There are many layers to this and all should be used to some extent.

  1. Unit Testing can be used to know a specific piece of code that handles valid input/output, edge cases, and error cases correctly.
  2. Integration Testing can be used to verify one piece of (hopefully unit tested) code is using another piece of (hopefully unit tested) code correctly.
  3. Manual Testing is the final catch-all to get your eyes and hands on the product. Do some exploratory testing, make sure the GUIs look right.

There the amount of these tests should form a pyramid in an ideal situation. The base and most by volume being unit tests followed next by integration tests and finally (and least of all) manual tests.

Finally, have a production go-live checklist. Automate as much of it as you can, but always have a smoke-test in place to verify certain critical pieces of the product are working when deployed.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Welcome to Code Review! Good points, nice answer! \$\endgroup\$
    – Heslacher
    Commented Oct 24, 2019 at 3:55
  • \$\begingroup\$ @Jeffrey Parks: Thank you for the detailed code review! I/we have already implemented a the HttpClientFactory and are in the process of removing .Result from out code. \$\endgroup\$
    – Aaron
    Commented Oct 29, 2019 at 14:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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