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.