3
\$\begingroup\$

I need to wrap httpClient because I'm using a custom token provider. I will use this code with asp.net mvc to communicate with our webApi2 server. ( Using webApi2 with directly from ui with angularjs etc. off the case. )

I shared this code and asking your opinions because;

  • I don't want any deadlock to occur.
  • I see code reuse which is bad (but don't know how to solve).
  • Maybe throwing exceptions somehow different and correct way.
  • It supposed to work under huge load (150K realtime requests from users)

Note: I implemented it with concerns of di. So I can wrap restSharp too.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using MyLibrary.Core.Components.Json;
using MyLibrary.Core.Components.Json.Implementations;

namespace MyLibrary.HttpClientWrapper
{
    public class ResourceServerRestClient : IResourceServerRestClient
    {
        private readonly ITokenProvider _tokenProvider;
        private readonly IJsonManager _jsonManager;

        public ResourceServerRestClient(ITokenProvider tokenProvider, IJsonManager jsonManager)
        {
            _tokenProvider = tokenProvider;
            _jsonManager = jsonManager;
        }

        public string BaseAddress { get; set; }

        public Task<T> GetAsync<T>(string uri, string clientId)
        {
            return CheckAndInvokeAsync(async token =>
            {
                using (var client = new HttpClient())
                {         
                    ConfigurateHttpClient(client, token, clientId);

                    HttpResponseMessage response = await client.GetAsync(uri).ConfigureAwait(false);

                    if (response.IsSuccessStatusCode)
                    {
                        return await response.Content.ReadAsAsync<T>();
                    }

                    var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");

                    exception.Data.Add("StatusCode", response.StatusCode);

                    throw exception;
                }
            });
        }

        public Task<T> PostAsJsonAsync<T>(object data, string uri, string clientId)
        {
            return CheckAndInvokeAsync(async token =>
            {
                using (var client = new HttpClient())
                {
                    ConfigurateHttpClient(client, token, clientId);

                    HttpResponseMessage response = await client.PostAsJsonAsync(uri, data);

                    if (response.IsSuccessStatusCode)
                    {
                        return await response.Content.ReadAsAsync<T>();
                    }

                    var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");

                    exception.Data.Add("StatusCode", response.StatusCode);

                    throw exception;
                }
            });
        }

        public async Task PostAsJsonAsync(object data, string uri, string clientId)
        {
            await CheckAndInvokeAsync(async token =>
            {
                using (var client = new HttpClient())
                {
                    ConfigurateHttpClient(client, token, clientId);

                    HttpResponseMessage response = await client.PostAsJsonAsync(uri, data);

                    if (!response.IsSuccessStatusCode)
                    {
                        var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");

                        exception.Data.Add("StatusCode", response.StatusCode);

                        throw exception;
                    }
                }

                return Task.FromResult(0);
            });
        }

        public Task PutAsJsonAsync(object data, string uri, string clientId)
        {
            return CheckAndInvokeAsync(async token =>
            {
                using (var client = new HttpClient())
                {
                    ConfigurateHttpClient(client, token, clientId);

                    HttpResponseMessage response = await client.PutAsJsonAsync(uri, data);

                    if (!response.IsSuccessStatusCode)
                    {
                        var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");

                        exception.Data.Add("StatusCode", response.StatusCode);

                        throw exception;
                    }
                }

                return Task.FromResult(0);
            });
        }

        public Task<T> PutAsJsonAsync<T>(object data, string uri, string clientId)
        {
            return CheckAndInvokeAsync(async token =>
            {
                using (var client = new HttpClient())
                {
                    ConfigurateHttpClient(client, token, clientId);

                    HttpResponseMessage response = await client.PutAsJsonAsync(uri, data);

                    if (response.IsSuccessStatusCode)
                    {
                        return await response.Content.ReadAsAsync<T>();
                    }

                    var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");

                    exception.Data.Add("StatusCode", response.StatusCode);

                    throw exception;
                }
            });
        }

        private void ConfigurateHttpClient(HttpClient client, string bearerToken, string resourceServiceClientName)
        {
            if (!string.IsNullOrEmpty(resourceServiceClientName))
            {
                client.DefaultRequestHeaders.Add("CN", resourceServiceClientName);
            }

            if (string.IsNullOrEmpty(BaseAddress))
            {
                throw new Exception("BaseAddress is required!");
            }

            client.BaseAddress = new Uri(BaseAddress);
            client.Timeout = new TimeSpan(0, 0, 0, 10);
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
        }

        private async Task<T> CheckAndInvokeAsync<T>(Func<string, Task<T>> method)
        {
            try
            {
                string token = await _tokenProvider.IsTokenNullOrExpired();

                if (!string.IsNullOrEmpty(token))
                {
                    return await method(token);
                }

                var exception = new Exception();
                exception.Data.Add("StatusCode", HttpStatusCode.Unauthorized);
                throw exception;
            }
            catch (Exception ex)
            {
                if (ex.Data.Contains("StatusCode") && ((HttpStatusCode)ex.Data["StatusCode"]) == HttpStatusCode.Unauthorized)
                {
                    string token = await _tokenProvider.GetTokenAsync();

                    if (!string.IsNullOrEmpty(token))
                    {
                        return await method(token);
                    }
                }

                throw;
            }
        }

        public void ThrowResourceServerException(List<string> messages)
        {
            string message = messages.Aggregate((p, q) => q + " - " + p);

            var exception = new Exception(message);

            exception.Data.Add("ServiceOperationException", message);

            throw exception;
        }

    }
}
\$\endgroup\$
2
\$\begingroup\$

Here is a one way to do it

public class ResourceServerRestClient : IResourceServerRestClient
{
    private readonly ITokenProvider _tokenProvider;
    private readonly IJsonManager _jsonManager;
    private HttpClient _client;

    // you can inject the interfaces
    public ResourceServerRestClient(ITokenProvider tokenProvider, IJsonManager jsonManager)
    {
        _tokenProvider = tokenProvider;
        _jsonManager = jsonManager;
    }

    // who set this property?
    public string BaseAddress { get; set; }

    // this is just to demonstrate a simple reuse technique. you can do it in other ways. (singleton, DI, static)
    public HttpClient Client => _client ?? (_client = new HttpClient());

    public Task<T> GetAsync<T>(string uri, string clientId)
    {
        return InvokeAsync<T>(
           clientId,
           client => client.GetAsync(uri),
           response => response.Content.ReadAsAsync<T>());
    }

    public Task<T> PostAsJsonAsync<T>(object data, string uri, string clientId)
    {
        return InvokeAsync<T>(
           clientId,
           client => client.PostAsJsonAsync(uri, data),
           response => response.Content.ReadAsAsync<T>());
    }

    public Task PostAsJsonAsync(object data, string uri, string clientId)
    {
        return InvokeAsync<object>(
            clientId,
            client => client.PostAsJsonAsync(uri, data));
    }

    public Task PutAsJsonAsync(object data, string uri, string clientId)
    {
        return InvokeAsync<object>(
            clientId,
            client => client.PutAsJsonAsync(uri, data));
    }

    public Task<T> PutAsJsonAsync<T>(object data, string uri, string clientId)
    {
        return InvokeAsync<T>(
            clientId,
            client => client.PutAsJsonAsync(uri, data),
            response => response.Content.ReadAsAsync<T>());
    }

    private async Task<T> InvokeAsync<T>(
        string clientId,
        Func<HttpClient, Task<HttpResponseMessage>> operation,
        Func<HttpResponseMessage, Task<T>> actionOnResponse = null)
    {
        if(operation == null)
            throw new ArgumentNullException(nameof(operation));
        // consider to make pre check validation also to clientId argument if it's needed

        var token = GetToken();

        ConfigurateHttpClient(_client, token, clientId);

        HttpResponseMessage response = await operation(_client).ConfigureAwait(false);

        if(!response.IsSuccessStatusCode)
        {
            var exception = new Exception($"Resource server returned an error. StatusCode : {response.StatusCode}");
            exception.Data.Add("StatusCode", response.StatusCode);
            throw exception;
        }
        if(actionOnResponse != null)
        {
            return await actionOnResponse(response).ConfigureAwait(false);
        }
        else
        {
            return default(T);
        }
    }

    private string GetToken()
    {
        // if IsTokenNullOrExpired return null and not string.Empty, you can do the foloowing:
        var token = await _tokenProvider.IsTokenNullOrExpired() ?? await _tokenProvider.GetTokenAsync();
        if(string.IsNullOrEmpty(token))
        {
            var exception = new Exception();
            exception.Data.Add("StatusCode", HttpStatusCode.Unauthorized);
            throw exception;
        }
        //else, do this:
        string token = await _tokenProvider.IsTokenNullOrExpired();
        if(string.IsNullOrEmpty(token))
        {
            token = await _tokenProvider.GetTokenAsync();
            if(string.IsNullOrEmpty(token))
            {
                var exception = new Exception();
                exception.Data.Add("StatusCode", HttpStatusCode.Unauthorized);
                throw exception;
            }
        }
        return token;
    }

    private void ConfigurateHttpClient(HttpClient client, string bearerToken, string resourceServiceClientName)
    {
        // do this first
        if(string.IsNullOrEmpty(BaseAddress))
        {
            throw new Exception("BaseAddress is required!");
        }
        // consider to do pre check also for arguments if it make sense

        if(!string.IsNullOrEmpty(resourceServiceClientName))
        {
            client.DefaultRequestHeaders.Add("CN", resourceServiceClientName);
        }

        client.BaseAddress = new Uri(BaseAddress);
        client.Timeout = new TimeSpan(0, 0, 0, 10);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
    }
}

I hope it's help..

\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer @Dudi Keleti, I'm reviewing/thinking your code rightnow. But I have one quick question. Can you explain to me in just a few sentences, why all async calls (get,put,post,delete etc.) should use ".ConfigureAwait(false);". I mean, i think, there is no gui or "main thread" in one mvc request to webapi. Why I need to use it? \$\endgroup\$ – Lost_In_Library Mar 29 '16 at 11:59
  • 1
    \$\begingroup\$ because you don't care about capturing the current context. if it was UI work then it will be ConfigureAwait(true). check this for more info \$\endgroup\$ – Dudi Keleti Mar 29 '16 at 12:38
  • \$\begingroup\$ Is it good to re-create httpClient for every request? I found these: codereview.stackexchange.com/questions/69950/… \$\endgroup\$ – Lost_In_Library Mar 31 '16 at 5:39
  • 1
    \$\begingroup\$ @Lost_In_Library yep the link is correct. reuse the client if there is no reason to recreate. if you want I can update my answer later today. \$\endgroup\$ – Dudi Keleti Mar 31 '16 at 5:54
  • 1
    \$\begingroup\$ @Lost_In_Library done:) if it's seems OK for you, please accept the answer. \$\endgroup\$ – Dudi Keleti Mar 31 '16 at 11:12

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.