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;
}
}
}
-
\$\begingroup\$ Use IHttpClientFactory instead. \$\endgroup\$andrew.fox– andrew.fox2021年02月10日 13:27:39 +00:00Commented Feb 10, 2021 at 13:27
1 Answer 1
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..
-
\$\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– Lost_In_Library2016年03月29日 11:59:49 +00:00Commented Mar 29, 2016 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– Dudi Keleti2016年03月29日 12:38:44 +00:00Commented Mar 29, 2016 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– Lost_In_Library2016年03月31日 05:39:42 +00:00Commented Mar 31, 2016 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– Dudi Keleti2016年03月31日 05:54:21 +00:00Commented Mar 31, 2016 at 5:54
-
1\$\begingroup\$ @Lost_In_Library done:) if it's seems OK for you, please accept the answer. \$\endgroup\$Dudi Keleti– Dudi Keleti2016年03月31日 11:12:18 +00:00Commented Mar 31, 2016 at 11:12
Explore related questions
See similar questions with these tags.