Our application architecture consists of:
- SPA JavaScript calls a set of Web API endpoints
- The Controllers then call an Entity specific Service
- Entity specific Service calls a Pass Through Service
- Pass Through Service calls an API Service that calls external APIs using the HttpClient library via a wrapper.
- The response is "passed through" to the originating client
Controller:
[RoutePrefix(WebApiConfig.UrlPrefix)] public class EntityController : ApiController { [Route("entity/next")] public async Task<IHttpActionResult> PostPassthru(HttpRequestMessage requestMessage) { var entityApiService = new EntityApiService>(); HttpResponseMessage response; string pathQuery; pathQuery = Common.StripPrefix(WebApiConfig.UrlPrefix, requestMessage.RequestUri.PathAndQuery, true); response = await EntityApiService.CallApiAsync(pathQuery, HttpMethod.Post, requestMessage.Content, requestMessage.Headers); return ResponseMessage(response); } }
EntityApiService:
public class EntityApiService : IEntityApiService { PassThroughApiService deafultApiService; public string API_NAME { get { return ApiNames.Entity; } } public EntityApiService() { deafultApiService = new PassThroughApiService(API_NAME); } public Task<HttpResponseMessage> GetAsync(string pathAndQuery, HttpRequestHeaders requestHeaders) { return deafultApiService.GetAsync(pathAndQuery, requestHeaders); } public async Task<HttpResponseMessage> CallApiAsync(string pathAndQuery, HttpMethod method, HttpContent content, HttpRequestHeaders requestHeaders, bool addJsonMimeRequest = true) { return await deafultApiService.CallApiAsync(pathAndQuery.ApplyPrefix(ApiPrefix), method, content, requestHeaders, addJsonMimeRequest); } }
PassThroughApiService:
public class PassThroughApiService : IApiService { private string _apiName; public string API_NAME { get { return _apiName; } } public PassThroughApiService(string apiName) { this._apiName = apiName; } public async Task<HttpResponseMessage> CallApiAsync(string pathAndQuery, HttpMethod method, HttpContent content, HttpRequestHeaders requestHeaders, bool addJsonMimeRequest = true) { HttpResponseMessage response; if (content != null && addJsonMimeRequest) content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json"); var apiService = New ApiService(); response = await apiService.CallApiAsync(API_NAME, pathAndQuery, method, content, requestHeaders, true); return response; } public Task<HttpResponseMessage> GetAsync(string pathAndQuery, HttpRequestHeaders requestHeaders) { return this.CallApiAsync(pathAndQuery, HttpMethod.Get, null, requestHeaders); } }
ApiService:
public class ApiService : IApiService { // Do not use a USING statement and do not call Dispose, see wrapper comments for details private static IHttpClient _httpClient; static ApiService() { _httpClient = new HttpClientWrapper(); } public Uri BuildUri(IApiMap apiMap, string pathAndQuery) { Uri uri; Uri baseUri; try { baseUri = new Uri(apiMap.ApiUri); } catch (Exception ex) { throw new Exception("Base URI mapped from API MAP was invalid.", ex); } try { uri = new Uri(baseUri, pathAndQuery); } catch (Exception ex) { throw new Exception("PathAndQuery value was invalid.", ex); } return uri; } public Task<HttpResponseMessage> CallApiAsync(string apiName, string pathAndQuery, HttpMethod method, HttpContent content, HttpRequestHeaders requestHeaders, bool addJsonMimeAccept = true) { ApiMapService apiMapService = new ApiMapService(); IApiMap apiMap; Uri uri; if (String.IsNullOrWhiteSpace(apiName)) throw new ArgumentNullException("apiName", "ApiName cannot be null or empty."); if (String.IsNullOrWhiteSpace(pathAndQuery)) throw new ArgumentNullException("pathAndQuery", "PathAndQuery cannot be null."); if (method == null) throw new ArgumentNullException("method", "Method cannot be null."); apiMap = apiMapService.GetByName(apiName); if (apiMap == null) throw new Exception("The ApiName value provided did not resolve to a record."); uri = BuildUri(apiMap, pathAndQuery); return CallApiAsync(uri, method, content, requestHeaders, addJsonMimeAccept); } public Task<HttpResponseMessage> CallApiAsync(Uri uri, HttpMethod method, HttpContent content, HttpRequestHeaders requestHeaders, bool addJsonMimeAccept = true) { HttpRequestMessage request; if (uri == null) throw new ArgumentNullException("uri"); if (method == null) throw new ArgumentNullException("method"); request = new HttpRequestMessage(method, uri.AbsoluteUri); if (addJsonMimeAccept) request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (content != null) request.Content = content; if (requestHeaders != null) { foreach (var item in requestHeaders.Where(header => header.Key.Contains(HeaderPrefix))) { request.Headers.Add(item.Key.Replace(HeaderPrefix, ""), item.Value); } } return _httpClient.SendAsync(request); } }
HttpClientWrapper
public class HttpClientWrapper : IHttpClient { private readonly HttpClient _client; public Uri BaseAddress { get { return _client.BaseAddress; } set { _client.BaseAddress = value; } } public HttpRequestHeaders DefaultRequestHeaders { get { return _client.DefaultRequestHeaders; } } public HttpClientWrapper() { bool ignoreCertificateErrors = ConfigurationManager.AppSettings["IgnoreCertificateErrors"].ToBool(); string environmentName = ConfigurationManager.AppSettings["EnvironmentName"]; _client = new HttpClient(); // If ignoreCertificateErrors config value is true, // the setting ignores all Certificate validation errors in AppDomain, // in every environment but PRODUCTION. if (ignoreCertErrors && environmentName.ToUpper() != "PRODUCTION") { ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; } } public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request) { return _client.SendAsync(request); } private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing && _client != null) { _client.Dispose(); } disposedValue = true; } } public void Dispose() { Dispose(true); } }
Any suggestions to improve?