diff --git a/appveyor.yml b/appveyor.yml index d83e2e73aef..549974b2499 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,6 +43,8 @@ build_script: - dotnet build samples\server\petstore\aspnetcore-3.0\Org.OpenAPITools.sln # build C# aspnetcore 2.2 server - dotnet build samples\server\petstore\aspnetcore\Org.OpenAPITools.sln + # build C# API client (httpclient) + - dotnet build samples\client\petstore\csharp-netcore\OpenAPIClient-httpclient\Org.OpenAPITools.sln # build C# API client (netcore) - dotnet build samples\client\petstore\csharp-netcore\OpenAPIClient\Org.OpenAPITools.sln - dotnet build samples\client\petstore\csharp-netcore\OpenAPIClientCore\Org.OpenAPITools.sln @@ -64,6 +66,8 @@ build_script: # run the locally installed openapi-generator-gradle-plugin - gradle -b modules\openapi-generator-gradle-plugin\samples\local-spec\build.gradle buildGoSdk --stacktrace test_script: + # test c# API client (httpclient) + - dotnet test samples\client\petstore\csharp-netcore\OpenAPIClient-httpclient\src\Org.OpenAPITools.Test\Org.OpenAPITools.Test.csproj # test c# API client (netcore) - dotnet test samples\client\petstore\csharp-netcore\OpenAPIClientCore\src\Org.OpenAPITools.Test\Org.OpenAPITools.Test.csproj - dotnet test samples\client\petstore\csharp-netcore\OpenAPIClient\src\Org.OpenAPITools.Test\Org.OpenAPITools.Test.csproj diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java index 13551b9a83e..8aa4cc08ffb 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java @@ -583,8 +583,11 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen { additionalProperties.put("useRestSharp", true); needsCustomHttpMethod = true; } else if (HTTPCLIENT.equals(getLibrary())) { + setLibrary(HTTPCLIENT); additionalProperties.put("useHttpClient", true); needsUriBuilder = true; + } else { + throw new RuntimeException("Invalid HTTP library " + getLibrary() + ". Only restsharp, httpclient are supported."); } String framework = (String) additionalProperties.getOrDefault(CodegenConstants.DOTNET_FRAMEWORK, defaultFramework.name); @@ -607,7 +610,6 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen { } strategy.configureAdditionalProperties(additionalProperties); - setTargetFrameworkNuget(strategy.getNugetFrameworkIdentifier()); setTargetFramework(strategy.name); setTestTargetFramework(strategy.testTargetFramework); diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/ApiClient.mustache index a056ef479a2..5647bd77797 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/ApiClient.mustache @@ -20,17 +20,12 @@ using System.Web; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; -{{#useRestSharp}} using RestSharp; using RestSharp.Deserializers; using RestSharpMethod = RestSharp.Method; -{{/useRestSharp}} {{#useWebRequest}} using System.Net.Http; {{/useWebRequest}} -{{#useHttpClient}} -using System.Net.Http; -{{/useHttpClient}} {{#supportsRetry}} using Polly; {{/supportsRetry}} @@ -40,7 +35,7 @@ namespace {{packageName}}.Client /// /// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. /// - internal class CustomJsonCodec {{#useRestSharp}} : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer {{/useRestSharp}} + internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer { private readonly IReadableConfiguration _configuration; private static readonly string _contentType = "application/json"; @@ -86,12 +81,7 @@ namespace {{packageName}}.Client } } - {{#useRestSharp}} public T Deserialize(IRestResponse response) - {{/useRestSharp}} - {{#useHttpClient}} - public T Deserialize(HttpResponseMessage response) - {{/useHttpClient}} { var result = (T)Deserialize(response, typeof(T)); return result; @@ -103,36 +93,19 @@ namespace {{packageName}}.Client /// The HTTP response. /// Object type. /// Object representation of the JSON string. - {{#useRestSharp}} internal object Deserialize(IRestResponse response, Type type) { IList headers = response.Headers; - {{/useRestSharp}} - {{#useHttpClient}} - internal object Deserialize(HttpResponseMessage response, Type type) - { - IList headers = response.Headers.Select(x => x.Key + "=" + x.Value).ToList(); - {{/useHttpClient}} if (type == typeof(byte[])) // return byte array { - {{#useRestSharp}} return response.RawBytes; - {{/useRestSharp}} - {{#useHttpClient}} - return response.Content.ReadAsByteArrayAsync().Result; - {{/useHttpClient}} } // TODO: ? if (type.IsAssignableFrom(typeof(Stream))) if (type == typeof(Stream)) { - {{#useRestSharp}} var bytes = response.RawBytes; - {{/useRestSharp}} - {{#useHttpClient}} - var bytes = response.Content.ReadAsByteArrayAsync().Result; - {{/useHttpClient}} if (headers != null) { var filePath = String.IsNullOrEmpty(_configuration.TempFolderPath) @@ -156,33 +129,18 @@ namespace {{packageName}}.Client if (type.Name.StartsWith("System.Nullable`1[[System.DateTime")) // return a datetime object { - {{#useRestSharp}} return DateTime.Parse(response.Content, null, System.Globalization.DateTimeStyles.RoundtripKind); - {{/useRestSharp}} - {{#useHttpClient}} - return DateTime.Parse(response.Content.ReadAsStringAsync().Result, null, System.Globalization.DateTimeStyles.RoundtripKind); - {{/useHttpClient}} } if (type == typeof(String) || type.Name.StartsWith("System.Nullable")) // return primitive type { - {{#useRestSharp}} return Convert.ChangeType(response.Content, type); - {{/useRestSharp}} - {{#useHttpClient}} - return Convert.ChangeType(response.Content.ReadAsStringAsync().Result, type); - {{/useHttpClient}} } // at this point, it must be a model (json) try { - {{#useRestSharp}} return JsonConvert.DeserializeObject(response.Content, type, _serializerSettings); - {{/useRestSharp}} - {{#useHttpClient}} - return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result, type, _serializerSettings); - {{/useHttpClient}} } catch (Exception e) { @@ -208,12 +166,6 @@ namespace {{packageName}}.Client {{>visibility}} partial class ApiClient : ISynchronousClient{{#supportsAsync}}, IAsynchronousClient{{/supportsAsync}} { private readonly String _baseUrl; -{{#useHttpClient}} - {{#reUseHttpClient}} - private readonly HttpClientHandler _httpClientHandler; - private readonly HttpClient _httpClient; - {{/reUseHttpClient}} -{{/useHttpClient}} /// /// Specifies the settings on a object. @@ -231,7 +183,7 @@ namespace {{packageName}}.Client } } }; -{{#useRestSharp}} + /// /// Allows for extending request processing for generated code. /// @@ -244,7 +196,6 @@ namespace {{packageName}}.Client /// The RestSharp request object /// The RestSharp response object partial void InterceptResponse(IRestRequest request, IRestResponse response); -{{/useRestSharp}} /// /// Initializes a new instance of the , defaulting to the global configurations' base url. @@ -252,12 +203,6 @@ namespace {{packageName}}.Client public ApiClient() { _baseUrl = {{packageName}}.Client.GlobalConfiguration.Instance.BasePath; -{{#useHttpClient}} - {{#reUseHttpClient}} - _httpClientHandler = new HttpClientHandler(); - _httpClient = new HttpClient(_httpClientHandler); - {{/reUseHttpClient}} -{{/useHttpClient}} } /// @@ -271,15 +216,8 @@ namespace {{packageName}}.Client throw new ArgumentException("basePath cannot be empty"); _baseUrl = basePath; -{{#useHttpClient}} - {{#reUseHttpClient}} - _httpClientHandler = new HttpClientHandler(); - _httpClient = new HttpClient(_httpClientHandler); - {{/reUseHttpClient}} -{{/useHttpClient}} } - {{#useRestSharp}} /// /// Constructs the RestSharp version of an http method /// @@ -590,258 +528,8 @@ namespace {{packageName}}.Client } return result; } - {{/useRestSharp}} - - {{#useHttpClient}} - /// - /// 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); - - // In case of POST or PUT pass query parameters in request body - if (method != HttpMethod.Post && method != HttpMethod.Put) - { - builder.AddQueryParameters(options.QueryParameters); - } - - HttpRequestMessage request = new HttpRequestMessage(method, builder.GetFullUri()); - - 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>(); - - if (options.FormParameters != null && options.FormParameters.Count > 0) - { - contentList.Add(new Tuple(new FormUrlEncodedContent(options.FormParameters), null, null)); - } - - if (options.Data != null) - { - var serializer = new CustomJsonCodec(SerializerSettings, configuration); - contentList.Add( - new Tuple(new StringContent(serializer.Serialize(options.Data), new UTF8Encoding(), "application/json"), null, null)); - } - - if (options.FileParameters != null && options.FileParameters.Count > 0) - { - foreach (var fileParam in options.FileParameters) - { - var bytes = ClientUtils.ReadAsBytes(fileParam.Value); - var fileStream = fileParam.Value as FileStream; - contentList.Add(new Tuple(new ByteArrayContent(bytes), fileParam.Key, - fileStream?.Name ?? "no_file_name_provided")); - } - } - - if (contentList.Count > 1) - { - string boundary = "---------" + Guid.NewGuid().ToString().ToUpperInvariant(); - var multipartContent = new MultipartFormDataContent(boundary); - foreach (var content in contentList) - { - if(content.Item2 != null) - { - multipartContent.Add(content.Item1, content.Item2, content.Item3); - } - else - { - multipartContent.Add(content.Item1); - } - } - - request.Content = multipartContent; - } - else - { - request.Content = contentList.FirstOrDefault()?.Item1; - } - - // 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, HttpClientHandler handler); - partial void InterceptResponse(HttpRequestMessage req, HttpResponseMessage response); - - private ApiResponse ToApiResponse(HttpResponseMessage response, object responseData, HttpClientHandler handler, Uri uri) - { - T result = (T) responseData; - string rawContent = response.Content.ToString(); - - var transformed = new ApiResponse(response.StatusCode, new Multimap({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, rawContent) - { - ErrorText = response.ReasonPhrase, - Cookies = new List() - }; - - if (response.Headers != null) - { - foreach (var responseHeader in response.Headers) - { - - } - } - - if (response != null) - { - foreach (Cookie cookie in handler.CookieContainer.GetCookies(uri)) - { - transformed.Cookies.Add(cookie); - } - } - - return transformed; - } - - private ApiResponse Exec(HttpRequestMessage req, IReadableConfiguration configuration) - { - return ExecAsync(req, configuration).Result; - } - - private async Task> ExecAsync(HttpRequestMessage req, - IReadableConfiguration configuration, - System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) - { - {{^reUseHttpClient}} - var handler = new HttpClientHandler(); - var client = new HttpClient(); - {{/reUseHttpClient}} - {{#reUseHttpClient}} - var handler = _httpClientHandler; - var client = _httpClient; - {{/reUseHttpClient}} - 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) - { - handler.Proxy = configuration.Proxy; - } - - if (configuration.UserAgent != null) - { - client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", configuration.UserAgent); - } - - if (configuration.ClientCertificates != null) - { - handler.ClientCertificates.AddRange(configuration.ClientCertificates); - } - - var cookieContainer = req.Properties.ContainsKey("CookieContainer") ? req.Properties["CookieContainer"] as List : null; - - if (cookieContainer != null) - { - foreach (var cookie in cookieContainer) - { - handler.CookieContainer.Add(cookie); - } - } - - InterceptRequest(req, handler); - - HttpResponseMessage response; -{{#supportsRetry}} - if (RetryConfiguration.AsyncRetryPolicy != null) - { - var policy = RetryConfiguration.AsyncRetryPolicy; - var policyResult = await policy - .ExecuteAndCaptureAsync(() => client.SendAsync(req, cancellationToken)) - .ConfigureAwait(false); - response = (policyResult.Outcome == OutcomeType.Successful) ? - policyResult.Result : new HttpResponseMessage() - { - ReasonPhrase = policyResult.FinalException.ToString(), - RequestMessage = req - }; - } - else - { -{{/supportsRetry}} - response = await client.SendAsync(req, cancellationToken).ConfigureAwait(false); -{{#supportsRetry}} - } -{{/supportsRetry}} - - object responseData = deserializer.Deserialize(response); - - // if the response type is oneOf/anyOf, call FromJSON to deserialize the data - if (typeof({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema).IsAssignableFrom(typeof(T))) - { - T instance = (T) Activator.CreateInstance(typeof(T)); - MethodInfo method = typeof(T).GetMethod("FromJson"); - method.Invoke(instance, new object[] {response.Content}); - responseData = instance; - } - else if (typeof(T).Name == "Stream") // for binary response - { - responseData = (T) (object) await response.Content.ReadAsStreamAsync(); - } - - InterceptResponse(req, response); - - var result = ToApiResponse(response, responseData, handler, req.RequestUri); - - return result; - } - - {{/useHttpClient}} - - {{#supportsAsync}} - {{#useRestSharp}} private async Task> ExecAsync(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { RestClient client = new RestClient(_baseUrl); @@ -960,7 +648,6 @@ namespace {{packageName}}.Client } return result; } - {{/useRestSharp}} #region IAsynchronousClient /// @@ -1065,12 +752,7 @@ namespace {{packageName}}.Client public Task> PatchAsync(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { var config = configuration ?? GlobalConfiguration.Instance; - {{#useRestSharp}} return ExecAsync(NewRequest(HttpMethod.Patch, path, options, config), config, cancellationToken); - {{/useRestSharp}} - {{#useHttpClient}} - return ExecAsync(NewRequest(new HttpMethod("PATCH"), path, options, config), config, cancellationToken); - {{/useHttpClient}} } #endregion IAsynchronousClient {{/supportsAsync}} @@ -1171,12 +853,7 @@ namespace {{packageName}}.Client public ApiResponse Patch(string path, RequestOptions options, IReadableConfiguration configuration = null) { var config = configuration ?? GlobalConfiguration.Instance; - {{#useRestSharp}} return Exec(NewRequest(HttpMethod.Patch, path, options, config), config); - {{/useRestSharp}} - {{#useHttpClient}} - return Exec(NewRequest(new HttpMethod("PATCH"), path, options, config), config); - {{/useHttpClient}} } #endregion ISynchronousClient } diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/httpclient/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/httpclient/ApiClient.mustache new file mode 100644 index 00000000000..512ee09dafc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/httpclient/ApiClient.mustache @@ -0,0 +1,669 @@ +{{>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; +{{#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 T Deserialize(HttpResponseMessage response) + { + var result = (T)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 object Deserialize(HttpResponseMessage response, Type type) + { + IList headers = response.Headers.Select(x => x.Key + "=" + x.Value).ToList(); + + if (type == typeof(byte[])) // return byte array + { + return response.Content.ReadAsByteArrayAsync().Result; + } + + // TODO: ? if (type.IsAssignableFrom(typeof(Stream))) + if (type == typeof(Stream)) + { + var bytes = response.Content.ReadAsByteArrayAsync().Result; + 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(response.Content.ReadAsStringAsync().Result, null, System.Globalization.DateTimeStyles.RoundtripKind); + } + + if (type == typeof(String) || type.Name.StartsWith("System.Nullable")) // return primitive type + { + return Convert.ChangeType(response.Content.ReadAsStringAsync().Result, type); + } + + // at this point, it must be a model (json) + try + { + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result, 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 implementatios), + /// encapsulating general REST accessor use cases. + /// + {{>visibility}} partial class ApiClient : ISynchronousClient{{#supportsAsync}}, IAsynchronousClient{{/supportsAsync}} + { + private readonly String _baseUrl; + {{#reUseHttpClient}} + private readonly HttpClientHandler _httpClientHandler; + private readonly HttpClient _httpClient; + {{/reUseHttpClient}} + + /// + /// Specifies the settings on a object. + /// These settings can be adjusted to accomodate 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. + /// + public ApiClient() + { + _baseUrl = {{packageName}}.Client.GlobalConfiguration.Instance.BasePath; + {{#reUseHttpClient}} + _httpClientHandler = new HttpClientHandler(); + _httpClient = new HttpClient(_httpClientHandler); + {{/reUseHttpClient}} + } + + /// + /// Initializes a new instance of the + /// + /// The target service's base path in URL format. + /// + public ApiClient(String basePath) + { + if (string.IsNullOrEmpty(basePath)) + throw new ArgumentException("basePath cannot be empty"); + + _baseUrl = basePath; + {{#reUseHttpClient}} + _httpClientHandler = new HttpClientHandler(); + _httpClient = new HttpClient(_httpClientHandler); + {{/reUseHttpClient}} + } + + /// + /// 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); + + // In case of POST or PUT pass query parameters in request body + if (method != HttpMethod.Post && method != HttpMethod.Put) + { + builder.AddQueryParameters(options.QueryParameters); + } + + HttpRequestMessage request = new HttpRequestMessage(method, builder.GetFullUri()); + + 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>(); + + if (options.FormParameters != null && options.FormParameters.Count > 0) + { + contentList.Add(new Tuple(new FormUrlEncodedContent(options.FormParameters), null, null)); + } + + if (options.Data != null) + { + var serializer = new CustomJsonCodec(SerializerSettings, configuration); + contentList.Add( + new Tuple(new StringContent(serializer.Serialize(options.Data), new UTF8Encoding(), "application/json"), null, null)); + } + + if (options.FileParameters != null && options.FileParameters.Count > 0) + { + foreach (var fileParam in options.FileParameters) + { + var bytes = ClientUtils.ReadAsBytes(fileParam.Value); + var fileStream = fileParam.Value as FileStream; + contentList.Add(new Tuple(new ByteArrayContent(bytes), fileParam.Key, + fileStream?.Name ?? "no_file_name_provided")); + } + } + + if (contentList.Count > 1) + { + string boundary = "---------" + Guid.NewGuid().ToString().ToUpperInvariant(); + var multipartContent = new MultipartFormDataContent(boundary); + foreach (var content in contentList) + { + if(content.Item2 != null) + { + multipartContent.Add(content.Item1, content.Item2, content.Item3); + } + else + { + multipartContent.Add(content.Item1); + } + } + + request.Content = multipartContent; + } + else + { + request.Content = contentList.FirstOrDefault()?.Item1; + } + + // 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, HttpClientHandler handler); + partial void InterceptResponse(HttpRequestMessage req, HttpResponseMessage response); + + private ApiResponse ToApiResponse(HttpResponseMessage response, object responseData, HttpClientHandler handler, Uri uri) + { + T result = (T) responseData; + string rawContent = response.Content.ToString(); + + var transformed = new ApiResponse(response.StatusCode, new Multimap({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, rawContent) + { + ErrorText = response.ReasonPhrase, + Cookies = new List() + }; + + if (response.Headers != null) + { + foreach (var responseHeader in response.Headers) + { + + } + } + + if (response != null) + { + foreach (Cookie cookie in handler.CookieContainer.GetCookies(uri)) + { + transformed.Cookies.Add(cookie); + } + } + + return transformed; + } + + private ApiResponse Exec(HttpRequestMessage req, IReadableConfiguration configuration) + { + return ExecAsync(req, configuration).Result; + } + + private async Task> ExecAsync(HttpRequestMessage req, + IReadableConfiguration configuration, + System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + {{^reUseHttpClient}} + var handler = new HttpClientHandler(); + var client = new HttpClient(); + {{/reUseHttpClient}} + {{#reUseHttpClient}} + var handler = _httpClientHandler; + var client = _httpClient; + {{/reUseHttpClient}} + 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) + { + handler.Proxy = configuration.Proxy; + } + + if (configuration.UserAgent != null) + { + client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", configuration.UserAgent); + } + + if (configuration.ClientCertificates != null) + { + handler.ClientCertificates.AddRange(configuration.ClientCertificates); + } + + var cookieContainer = req.Properties.ContainsKey("CookieContainer") ? req.Properties["CookieContainer"] as List : null; + + if (cookieContainer != null) + { + foreach (var cookie in cookieContainer) + { + handler.CookieContainer.Add(cookie); + } + } + + InterceptRequest(req, handler); + + HttpResponseMessage response; +{{#supportsRetry}} + if (RetryConfiguration.AsyncRetryPolicy != null) + { + var policy = RetryConfiguration.AsyncRetryPolicy; + var policyResult = await policy + .ExecuteAndCaptureAsync(() => client.SendAsync(req, cancellationToken)) + .ConfigureAwait(false); + response = (policyResult.Outcome == OutcomeType.Successful) ? + policyResult.Result : new HttpResponseMessage() + { + ReasonPhrase = policyResult.FinalException.ToString(), + RequestMessage = req + }; + } + else + { +{{/supportsRetry}} + response = await client.SendAsync(req, cancellationToken).ConfigureAwait(false); +{{#supportsRetry}} + } +{{/supportsRetry}} + + object responseData = deserializer.Deserialize(response); + + // if the response type is oneOf/anyOf, call FromJSON to deserialize the data + if (typeof({{{packageName}}}.{{modelPackage}}.AbstractOpenAPISchema).IsAssignableFrom(typeof(T))) + { + T instance = (T) Activator.CreateInstance(typeof(T)); + MethodInfo method = typeof(T).GetMethod("FromJson"); + method.Invoke(instance, new object[] {response.Content}); + responseData = instance; + } + else if (typeof(T).Name == "Stream") // for binary response + { + responseData = (T) (object) await response.Content.ReadAsStreamAsync(); + } + + InterceptResponse(req, response); + + var result = ToApiResponse(response, responseData, handler, req.RequestUri); + + return result; + } + + {{#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 + } +} diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Client/ApiClient.cs b/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Client/ApiClient.cs index 155947dbe92..0c65211b9c1 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Client/ApiClient.cs +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Client/ApiClient.cs @@ -31,9 +31,9 @@ using Polly; namespace Org.OpenAPITools.Client { /// - /// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. + /// To Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. /// - internal class CustomJsonCodec + internal class CustomJsonCodec { private readonly IReadableConfiguration _configuration; private static readonly string _contentType = "application/json"; @@ -202,8 +202,7 @@ namespace Org.OpenAPITools.Client _baseUrl = basePath; } - - /// + /// /// 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. @@ -436,10 +435,6 @@ namespace Org.OpenAPITools.Client return result; } - - - - #region IAsynchronousClient /// /// Make a HTTP GET request (async). diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Client/ApiClient.cs b/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Client/ApiClient.cs index b8280a56d64..d7022fee2af 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Client/ApiClient.cs +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Client/ApiClient.cs @@ -36,7 +36,7 @@ namespace Org.OpenAPITools.Client /// /// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. /// - internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer + internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer { private readonly IReadableConfiguration _configuration; private static readonly string _contentType = "application/json"; @@ -183,6 +183,7 @@ namespace Org.OpenAPITools.Client } } }; + /// /// Allows for extending request processing for generated code. /// @@ -528,9 +529,6 @@ namespace Org.OpenAPITools.Client return result; } - - - private async Task> ExecAsync(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { RestClient client = new RestClient(_baseUrl); diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Client/ApiClient.cs b/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Client/ApiClient.cs index b8280a56d64..d7022fee2af 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Client/ApiClient.cs +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Client/ApiClient.cs @@ -36,7 +36,7 @@ namespace Org.OpenAPITools.Client /// /// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. /// - internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer + internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer { private readonly IReadableConfiguration _configuration; private static readonly string _contentType = "application/json"; @@ -183,6 +183,7 @@ namespace Org.OpenAPITools.Client } } }; + /// /// Allows for extending request processing for generated code. /// @@ -528,9 +529,6 @@ namespace Org.OpenAPITools.Client return result; } - - - private async Task> ExecAsync(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { RestClient client = new RestClient(_baseUrl); diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Client/ApiClient.cs b/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Client/ApiClient.cs index f2c8a77cb8f..1b168c688a5 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Client/ApiClient.cs +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Client/ApiClient.cs @@ -35,7 +35,7 @@ namespace Org.OpenAPITools.Client /// /// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. /// - internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer + internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer { private readonly IReadableConfiguration _configuration; private static readonly string _contentType = "application/json"; @@ -182,6 +182,7 @@ namespace Org.OpenAPITools.Client } } }; + /// /// Allows for extending request processing for generated code. /// @@ -527,9 +528,6 @@ namespace Org.OpenAPITools.Client return result; } - - - private async Task> ExecAsync(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { RestClient client = new RestClient(_baseUrl); diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Client/ApiClient.cs b/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Client/ApiClient.cs index b8280a56d64..d7022fee2af 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Client/ApiClient.cs +++ b/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Client/ApiClient.cs @@ -36,7 +36,7 @@ namespace Org.OpenAPITools.Client /// /// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON. /// - internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer + internal class CustomJsonCodec : RestSharp.Serializers.ISerializer, RestSharp.Deserializers.IDeserializer { private readonly IReadableConfiguration _configuration; private static readonly string _contentType = "application/json"; @@ -183,6 +183,7 @@ namespace Org.OpenAPITools.Client } } }; + /// /// Allows for extending request processing for generated code. /// @@ -528,9 +529,6 @@ namespace Org.OpenAPITools.Client return result; } - - - private async Task> ExecAsync(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { RestClient client = new RestClient(_baseUrl);