{{>partial_header}} using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters; using System.Text; using System.Threading; using System.Text.RegularExpressions; using System.Threading.Tasks; {{^netStandard}} using System.Web; {{/netStandard}} using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; {{#useWebRequest}} using System.Net.Http; {{/useWebRequest}} using System.Net.Http; using System.Net.Http.Headers; {{#supportsRetry}} using Polly; {{/supportsRetry}} namespace {{packageName}}.Client { /// /// To Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. /// internal class CustomJsonCodec { private readonly IReadableConfiguration _configuration; private static readonly string _contentType = "application/json"; private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { // OpenAPI generated types generally hide default constructors. ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy { OverrideSpecifiedNames = false } } }; public CustomJsonCodec(IReadableConfiguration configuration) { _configuration = configuration; } public CustomJsonCodec(JsonSerializerSettings serializerSettings, IReadableConfiguration configuration) { _serializerSettings = serializerSettings; _configuration = configuration; } /// /// Serialize the object into a JSON string. /// /// Object to be serialized. /// A JSON string. public string Serialize(object obj) { if (obj != null && obj is {{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema) { // the object to be serialized is an oneOf/anyOf schema return (({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema)obj).ToJson(); } else { return JsonConvert.SerializeObject(obj, _serializerSettings); } } public async Task Deserialize(HttpResponseMessage response) { var result = (T) await Deserialize(response, typeof(T)); return result; } /// /// Deserialize the JSON string into a proper object. /// /// The HTTP response. /// Object type. /// Object representation of the JSON string. internal async Task Deserialize(HttpResponseMessage response, Type type) { IList headers = response.Headers.Select(x => x.Key + "=" + x.Value).ToList(); if (type == typeof(byte[])) // return byte array { return await response.Content.ReadAsByteArrayAsync(); } // TODO: ? if (type.IsAssignableFrom(typeof(Stream))) if (type == typeof(Stream)) { var bytes = await response.Content.ReadAsByteArrayAsync(); if (headers != null) { var filePath = string.IsNullOrEmpty(_configuration.TempFolderPath) ? Path.GetTempPath() : _configuration.TempFolderPath; var regex = new Regex(@"Content-Disposition=.*filename=['""]?([^'""\s]+)['""]?$"); foreach (var header in headers) { var match = regex.Match(header.ToString()); if (match.Success) { string fileName = filePath + ClientUtils.SanitizeFilename(match.Groups[1].Value.Replace("\"", "").Replace("'", "")); File.WriteAllBytes(fileName, bytes); return new FileStream(fileName, FileMode.Open); } } } var stream = new MemoryStream(bytes); return stream; } if (type.Name.StartsWith("System.Nullable`1[[System.DateTime")) // return a datetime object { return DateTime.Parse(await response.Content.ReadAsStringAsync(), null, System.Globalization.DateTimeStyles.RoundtripKind); } if (type == typeof(string) || type.Name.StartsWith("System.Nullable")) // return primitive type { return Convert.ChangeType(await response.Content.ReadAsStringAsync(), type); } // at this point, it must be a model (json) try { return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), type, _serializerSettings); } catch (Exception e) { throw new ApiException(500, e.Message); } } public string RootElement { get; set; } public string Namespace { get; set; } public string DateFormat { get; set; } public string ContentType { get { return _contentType; } set { throw new InvalidOperationException("Not allowed to set content type."); } } } /// /// Provides a default implementation of an Api client (both synchronous and asynchronous implementations), /// encapsulating general REST accessor use cases. /// /// /// The Dispose method will manage the HttpClient lifecycle when not passed by constructor. /// {{>visibility}} partial class ApiClient : IDisposable, ISynchronousClient{{#supportsAsync}}, IAsynchronousClient{{/supportsAsync}} { private readonly string _baseUrl; private readonly HttpClientHandler _httpClientHandler; private readonly HttpClient _httpClient; private readonly bool _disposeClient; /// /// Specifies the settings on a object. /// These settings can be adjusted to accommodate custom serialization rules. /// public JsonSerializerSettings SerializerSettings { get; set; } = new JsonSerializerSettings { // OpenAPI generated types generally hide default constructors. ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy { OverrideSpecifiedNames = false } } }; /// /// Initializes a new instance of the , defaulting to the global configurations' base url. /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. /// It's better to reuse the HttpClient and HttpClientHandler. /// public ApiClient() : this({{packageName}}.Client.GlobalConfiguration.Instance.BasePath) { } /// /// Initializes a new instance of the . /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. /// It's better to reuse the HttpClient and HttpClientHandler. /// /// The target service's base path in URL format. /// public ApiClient(string basePath) { if (string.IsNullOrEmpty(basePath)) throw new ArgumentException("basePath cannot be empty"); _httpClientHandler = new HttpClientHandler(); _httpClient = new HttpClient(_httpClientHandler, true); _disposeClient = true; _baseUrl = basePath; } /// /// Initializes a new instance of the , defaulting to the global configurations' base url. /// /// An instance of HttpClient. /// An optional instance of HttpClientHandler that is used by HttpClient. /// /// /// Some configuration settings will not be applied without passing an HttpClientHandler. /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. /// public ApiClient(HttpClient client, HttpClientHandler handler = null) : this(client, {{packageName}}.Client.GlobalConfiguration.Instance.BasePath, handler) { } /// /// Initializes a new instance of the . /// /// An instance of HttpClient. /// The target service's base path in URL format. /// An optional instance of HttpClientHandler that is used by HttpClient. /// /// /// /// Some configuration settings will not be applied without passing an HttpClientHandler. /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. /// public ApiClient(HttpClient client, string basePath, HttpClientHandler handler = null) { if (client == null) throw new ArgumentNullException("client cannot be null"); if (string.IsNullOrEmpty(basePath)) throw new ArgumentException("basePath cannot be empty"); _httpClientHandler = handler; _httpClient = client; _baseUrl = basePath; } /// /// Disposes resources if they were created by us /// public void Dispose() { if(_disposeClient) { _httpClient.Dispose(); } } /// Prepares multipart/form-data content {{! TODO: Add handling of improper usage }} HttpContent PrepareMultipartFormDataContent(RequestOptions options) { string boundary = "---------" + Guid.NewGuid().ToString().ToUpperInvariant(); var multipartContent = new MultipartFormDataContent(boundary); foreach (var formParameter in options.FormParameters) { multipartContent.Add(new StringContent(formParameter.Value), formParameter.Key); } if (options.FileParameters != null && options.FileParameters.Count > 0) { foreach (var fileParam in options.FileParameters) { var content = new StreamContent(fileParam.Value.Content); content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); multipartContent.Add(content, fileParam.Key, fileParam.Value.Name); } } return multipartContent; } /// /// Provides all logic for constructing a new HttpRequestMessage. /// At this point, all information for querying the service is known. Here, it is simply /// mapped into the a HttpRequestMessage. /// /// The http verb. /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// [private] A new HttpRequestMessage instance. /// private HttpRequestMessage NewRequest( HttpMethod method, string path, RequestOptions options, IReadableConfiguration configuration) { if (path == null) throw new ArgumentNullException("path"); if (options == null) throw new ArgumentNullException("options"); if (configuration == null) throw new ArgumentNullException("configuration"); WebRequestPathBuilder builder = new WebRequestPathBuilder(_baseUrl, path); builder.AddPathParameters(options.PathParameters); builder.AddQueryParameters(options.QueryParameters); HttpRequestMessage request = new HttpRequestMessage(method, builder.GetFullUri()); if (configuration.UserAgent != null) { request.Headers.TryAddWithoutValidation("User-Agent", configuration.UserAgent); } if (configuration.DefaultHeaders != null) { foreach (var headerParam in configuration.DefaultHeaders) { request.Headers.Add(headerParam.Key, headerParam.Value); } } if (options.HeaderParameters != null) { foreach (var headerParam in options.HeaderParameters) { foreach (var value in headerParam.Value) { // Todo make content headers actually content headers request.Headers.TryAddWithoutValidation(headerParam.Key, value); } } } List> contentList = new List>(); string contentType = null; if (options.HeaderParameters != null && options.HeaderParameters.ContainsKey("Content-Type")) { var contentTypes = options.HeaderParameters["Content-Type"]; contentType = contentTypes.FirstOrDefault(); } {{!// TODO Add error handling in case of improper usage}} if (contentType == "multipart/form-data") { request.Content = PrepareMultipartFormDataContent(options); } else if (contentType == "application/x-www-form-urlencoded") { request.Content = new FormUrlEncodedContent(options.FormParameters); } else { if (options.Data != null) { if (options.Data is FileParameter fp) { contentType = contentType ?? "application/octet-stream"; var streamContent = new StreamContent(fp.Content); streamContent.Headers.ContentType = new MediaTypeHeaderValue(contentType); request.Content = streamContent; } else { var serializer = new CustomJsonCodec(SerializerSettings, configuration); request.Content = new StringContent(serializer.Serialize(options.Data), new UTF8Encoding(), "application/json"); } } } // TODO provide an alternative that allows cookies per request instead of per API client if (options.Cookies != null && options.Cookies.Count > 0) { request.Properties["CookieContainer"] = options.Cookies; } return request; } partial void InterceptRequest(HttpRequestMessage req); partial void InterceptResponse(HttpRequestMessage req, HttpResponseMessage response); private async Task> ToApiResponse(HttpResponseMessage response, object responseData, Uri uri) { T result = (T) responseData; string rawContent = await response.Content.ReadAsStringAsync(); var transformed = new ApiResponse(response.StatusCode, new Multimap({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, rawContent) { ErrorText = response.ReasonPhrase, Cookies = new List() }; // process response headers, e.g. Access-Control-Allow-Methods if (response.Headers != null) { foreach (var responseHeader in response.Headers) { transformed.Headers.Add(responseHeader.Key, ClientUtils.ParameterToString(responseHeader.Value)); } } // process response content headers, e.g. Content-Type if (response.Content.Headers != null) { foreach (var responseHeader in response.Content.Headers) { transformed.Headers.Add(responseHeader.Key, ClientUtils.ParameterToString(responseHeader.Value)); } } if (_httpClientHandler != null && response != null) { try { foreach (Cookie cookie in _httpClientHandler.CookieContainer.GetCookies(uri)) { transformed.Cookies.Add(cookie); } } catch (PlatformNotSupportedException) {} } return transformed; } private ApiResponse Exec(HttpRequestMessage req, IReadableConfiguration configuration) { return ExecAsync(req, configuration).GetAwaiter().GetResult(); } private async Task> ExecAsync(HttpRequestMessage req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var deserializer = new CustomJsonCodec(SerializerSettings, configuration); var finalToken = cancellationToken; if (configuration.Timeout > 0) { var tokenSource = new CancellationTokenSource(configuration.Timeout); finalToken = CancellationTokenSource.CreateLinkedTokenSource(finalToken, tokenSource.Token).Token; } if (configuration.Proxy != null) { if(_httpClientHandler == null) throw new InvalidOperationException("Configuration `Proxy` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); _httpClientHandler.Proxy = configuration.Proxy; } if (configuration.ClientCertificates != null) { if(_httpClientHandler == null) throw new InvalidOperationException("Configuration `ClientCertificates` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); _httpClientHandler.ClientCertificates.AddRange(configuration.ClientCertificates); } var cookieContainer = req.Properties.ContainsKey("CookieContainer") ? req.Properties["CookieContainer"] as List : null; if (cookieContainer != null) { if(_httpClientHandler == null) throw new InvalidOperationException("Request property `CookieContainer` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor."); foreach (var cookie in cookieContainer) { _httpClientHandler.CookieContainer.Add(cookie); } } InterceptRequest(req); HttpResponseMessage response; {{#supportsRetry}} if (RetryConfiguration.AsyncRetryPolicy != null) { var policy = RetryConfiguration.AsyncRetryPolicy; var policyResult = await policy .ExecuteAndCaptureAsync(() => _httpClient.SendAsync(req, cancellationToken)) .ConfigureAwait(false); response = (policyResult.Outcome == OutcomeType.Successful) ? policyResult.Result : new HttpResponseMessage() { ReasonPhrase = policyResult.FinalException.ToString(), RequestMessage = req }; } else { {{/supportsRetry}} response = await _httpClient.SendAsync(req, cancellationToken).ConfigureAwait(false); {{#supportsRetry}} } {{/supportsRetry}} if (!response.IsSuccessStatusCode) { return await ToApiResponse(response, default(T), req.RequestUri); } object responseData = await deserializer.Deserialize(response); // if the response type is oneOf/anyOf, call FromJSON to deserialize the data if (typeof({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema).IsAssignableFrom(typeof(T))) { responseData = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content }); } else if (typeof(T).Name == "Stream") // for binary response { responseData = (T) (object) await response.Content.ReadAsStreamAsync(); } InterceptResponse(req, response); return await ToApiResponse(response, responseData, req.RequestUri); } {{#supportsAsync}} #region IAsynchronousClient /// /// Make a HTTP GET request (async). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// Token that enables callers to cancel the request. /// A Task containing ApiResponse public Task> GetAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; return ExecAsync(NewRequest(HttpMethod.Get, path, options, config), config, cancellationToken); } /// /// Make a HTTP POST request (async). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// Token that enables callers to cancel the request. /// A Task containing ApiResponse public Task> PostAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; return ExecAsync(NewRequest(HttpMethod.Post, path, options, config), config, cancellationToken); } /// /// Make a HTTP PUT request (async). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// Token that enables callers to cancel the request. /// A Task containing ApiResponse public Task> PutAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; return ExecAsync(NewRequest(HttpMethod.Put, path, options, config), config, cancellationToken); } /// /// Make a HTTP DELETE request (async). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// Token that enables callers to cancel the request. /// A Task containing ApiResponse public Task> DeleteAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; return ExecAsync(NewRequest(HttpMethod.Delete, path, options, config), config, cancellationToken); } /// /// Make a HTTP HEAD request (async). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// Token that enables callers to cancel the request. /// A Task containing ApiResponse public Task> HeadAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; return ExecAsync(NewRequest(HttpMethod.Head, path, options, config), config, cancellationToken); } /// /// Make a HTTP OPTION request (async). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// Token that enables callers to cancel the request. /// A Task containing ApiResponse public Task> OptionsAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; return ExecAsync(NewRequest(HttpMethod.Options, path, options, config), config, cancellationToken); } /// /// Make a HTTP PATCH request (async). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// Token that enables callers to cancel the request. /// A Task containing ApiResponse public Task> PatchAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; return ExecAsync(NewRequest(new HttpMethod("PATCH"), path, options, config), config, cancellationToken); } #endregion IAsynchronousClient {{/supportsAsync}} #region ISynchronousClient /// /// Make a HTTP GET request (synchronous). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// A Task containing ApiResponse public ApiResponse Get(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; return Exec(NewRequest(HttpMethod.Get, path, options, config), config); } /// /// Make a HTTP POST request (synchronous). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// A Task containing ApiResponse public ApiResponse Post(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; return Exec(NewRequest(HttpMethod.Post, path, options, config), config); } /// /// Make a HTTP PUT request (synchronous). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// A Task containing ApiResponse public ApiResponse Put(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; return Exec(NewRequest(HttpMethod.Put, path, options, config), config); } /// /// Make a HTTP DELETE request (synchronous). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// A Task containing ApiResponse public ApiResponse Delete(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; return Exec(NewRequest(HttpMethod.Delete, path, options, config), config); } /// /// Make a HTTP HEAD request (synchronous). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// A Task containing ApiResponse public ApiResponse Head(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; return Exec(NewRequest(HttpMethod.Head, path, options, config), config); } /// /// Make a HTTP OPTION request (synchronous). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// A Task containing ApiResponse public ApiResponse Options(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; return Exec(NewRequest(HttpMethod.Options, path, options, config), config); } /// /// Make a HTTP PATCH request (synchronous). /// /// The target path (or resource). /// The additional request options. /// A per-request configuration object. It is assumed that any merge with /// GlobalConfiguration has been done before calling this method. /// A Task containing ApiResponse public ApiResponse Patch(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; return Exec(NewRequest(new HttpMethod("PATCH"), path, options, config), config); } #endregion ISynchronousClient } }