Add ApiClient.mustache for http client library (#8859)

* add api client mustache for http client

* clean up api client template for restsharp

* test c# httpclient in appveyor

* remove apiclient.cs

* regenerate apiclient.cs

* set library for http client

* remove Libraries

* use libraries folder
This commit is contained in:
William Cheng 2021-03-02 10:52:05 +08:00 committed by GitHub
parent 16e7408eb7
commit f7b2baf38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 689 additions and 350 deletions

View File

@ -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

View File

@ -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);

View File

@ -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
/// <summary>
/// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
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<T>(IRestResponse response)
{{/useRestSharp}}
{{#useHttpClient}}
public T Deserialize<T>(HttpResponseMessage response)
{{/useHttpClient}}
{
var result = (T)Deserialize(response, typeof(T));
return result;
@ -103,36 +93,19 @@ namespace {{packageName}}.Client
/// <param name="response">The HTTP response.</param>
/// <param name="type">Object type.</param>
/// <returns>Object representation of the JSON string.</returns>
{{#useRestSharp}}
internal object Deserialize(IRestResponse response, Type type)
{
IList<Parameter> headers = response.Headers;
{{/useRestSharp}}
{{#useHttpClient}}
internal object Deserialize(HttpResponseMessage response, Type type)
{
IList<string> 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}}
/// <summary>
/// Specifies the settings on a <see cref="JsonSerializer" /> object.
@ -231,7 +183,7 @@ namespace {{packageName}}.Client
}
}
};
{{#useRestSharp}}
/// <summary>
/// Allows for extending request processing for <see cref="ApiClient"/> generated code.
/// </summary>
@ -244,7 +196,6 @@ namespace {{packageName}}.Client
/// <param name="request">The RestSharp request object</param>
/// <param name="response">The RestSharp response object</param>
partial void InterceptResponse(IRestRequest request, IRestResponse response);
{{/useRestSharp}}
/// <summary>
/// Initializes a new instance of the <see cref="ApiClient" />, 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}}
}
/// <summary>
@ -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}}
/// <summary>
/// Constructs the RestSharp version of an http method
/// </summary>
@ -590,258 +528,8 @@ namespace {{packageName}}.Client
}
return result;
}
{{/useRestSharp}}
{{#useHttpClient}}
/// <summary>
/// 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.
/// </summary>
/// <param name="method">The http verb.</param>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>[private] A new HttpRequestMessage instance.</returns>
/// <exception cref="ArgumentNullException"></exception>
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<Tuple<HttpContent, string, string>> contentList = new List<Tuple<HttpContent, string, string>>();
if (options.FormParameters != null && options.FormParameters.Count > 0)
{
contentList.Add(new Tuple<HttpContent, string, string>(new FormUrlEncodedContent(options.FormParameters), null, null));
}
if (options.Data != null)
{
var serializer = new CustomJsonCodec(SerializerSettings, configuration);
contentList.Add(
new Tuple<HttpContent, string, string>(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<HttpContent, string, string>(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<T> ToApiResponse<T>(HttpResponseMessage response, object responseData, HttpClientHandler handler, Uri uri)
{
T result = (T) responseData;
string rawContent = response.Content.ToString();
var transformed = new ApiResponse<T>(response.StatusCode, new Multimap<string, string>({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, rawContent)
{
ErrorText = response.ReasonPhrase,
Cookies = new List<Cookie>()
};
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<T> Exec<T>(HttpRequestMessage req, IReadableConfiguration configuration)
{
return ExecAsync<T>(req, configuration).Result;
}
private async Task<ApiResponse<T>> ExecAsync<T>(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<Cookie> : 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<T>(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<T>(response, responseData, handler, req.RequestUri);
return result;
}
{{/useHttpClient}}
{{#supportsAsync}}
{{#useRestSharp}}
private async Task<ApiResponse<T>> ExecAsync<T>(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
/// <summary>
@ -1065,12 +752,7 @@ namespace {{packageName}}.Client
public Task<ApiResponse<T>> PatchAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
{{#useRestSharp}}
return ExecAsync<T>(NewRequest(HttpMethod.Patch, path, options, config), config, cancellationToken);
{{/useRestSharp}}
{{#useHttpClient}}
return ExecAsync<T>(NewRequest(new HttpMethod("PATCH"), path, options, config), config, cancellationToken);
{{/useHttpClient}}
}
#endregion IAsynchronousClient
{{/supportsAsync}}
@ -1171,12 +853,7 @@ namespace {{packageName}}.Client
public ApiResponse<T> Patch<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
{{#useRestSharp}}
return Exec<T>(NewRequest(HttpMethod.Patch, path, options, config), config);
{{/useRestSharp}}
{{#useHttpClient}}
return Exec<T>(NewRequest(new HttpMethod("PATCH"), path, options, config), config);
{{/useHttpClient}}
}
#endregion ISynchronousClient
}

View File

@ -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
{
/// <summary>
/// To Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
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;
}
/// <summary>
/// Serialize the object into a JSON string.
/// </summary>
/// <param name="obj">Object to be serialized.</param>
/// <returns>A JSON string.</returns>
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<T>(HttpResponseMessage response)
{
var result = (T)Deserialize(response, typeof(T));
return result;
}
/// <summary>
/// Deserialize the JSON string into a proper object.
/// </summary>
/// <param name="response">The HTTP response.</param>
/// <param name="type">Object type.</param>
/// <returns>Object representation of the JSON string.</returns>
internal object Deserialize(HttpResponseMessage response, Type type)
{
IList<string> 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."); }
}
}
/// <summary>
/// Provides a default implementation of an Api client (both synchronous and asynchronous implementatios),
/// encapsulating general REST accessor use cases.
/// </summary>
{{>visibility}} partial class ApiClient : ISynchronousClient{{#supportsAsync}}, IAsynchronousClient{{/supportsAsync}}
{
private readonly String _baseUrl;
{{#reUseHttpClient}}
private readonly HttpClientHandler _httpClientHandler;
private readonly HttpClient _httpClient;
{{/reUseHttpClient}}
/// <summary>
/// Specifies the settings on a <see cref="JsonSerializer" /> object.
/// These settings can be adjusted to accomodate custom serialization rules.
/// </summary>
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
}
}
};
/// <summary>
/// Initializes a new instance of the <see cref="ApiClient" />, defaulting to the global configurations' base url.
/// </summary>
public ApiClient()
{
_baseUrl = {{packageName}}.Client.GlobalConfiguration.Instance.BasePath;
{{#reUseHttpClient}}
_httpClientHandler = new HttpClientHandler();
_httpClient = new HttpClient(_httpClientHandler);
{{/reUseHttpClient}}
}
/// <summary>
/// Initializes a new instance of the <see cref="ApiClient" />
/// </summary>
/// <param name="basePath">The target service's base path in URL format.</param>
/// <exception cref="ArgumentException"></exception>
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}}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="method">The http verb.</param>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>[private] A new HttpRequestMessage instance.</returns>
/// <exception cref="ArgumentNullException"></exception>
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<Tuple<HttpContent, string, string>> contentList = new List<Tuple<HttpContent, string, string>>();
if (options.FormParameters != null && options.FormParameters.Count > 0)
{
contentList.Add(new Tuple<HttpContent, string, string>(new FormUrlEncodedContent(options.FormParameters), null, null));
}
if (options.Data != null)
{
var serializer = new CustomJsonCodec(SerializerSettings, configuration);
contentList.Add(
new Tuple<HttpContent, string, string>(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<HttpContent, string, string>(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<T> ToApiResponse<T>(HttpResponseMessage response, object responseData, HttpClientHandler handler, Uri uri)
{
T result = (T) responseData;
string rawContent = response.Content.ToString();
var transformed = new ApiResponse<T>(response.StatusCode, new Multimap<string, string>({{#caseInsensitiveResponseHeaders}}StringComparer.OrdinalIgnoreCase{{/caseInsensitiveResponseHeaders}}), result, rawContent)
{
ErrorText = response.ReasonPhrase,
Cookies = new List<Cookie>()
};
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<T> Exec<T>(HttpRequestMessage req, IReadableConfiguration configuration)
{
return ExecAsync<T>(req, configuration).Result;
}
private async Task<ApiResponse<T>> ExecAsync<T>(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<Cookie> : 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<T>(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<T>(response, responseData, handler, req.RequestUri);
return result;
}
{{#supportsAsync}}
#region IAsynchronousClient
/// <summary>
/// Make a HTTP GET request (async).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <param name="cancellationToken">Token that enables callers to cancel the request.</param>
/// <returns>A Task containing ApiResponse</returns>
public Task<ApiResponse<T>> GetAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
return ExecAsync<T>(NewRequest(HttpMethod.Get, path, options, config), config, cancellationToken);
}
/// <summary>
/// Make a HTTP POST request (async).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <param name="cancellationToken">Token that enables callers to cancel the request.</param>
/// <returns>A Task containing ApiResponse</returns>
public Task<ApiResponse<T>> PostAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
return ExecAsync<T>(NewRequest(HttpMethod.Post, path, options, config), config, cancellationToken);
}
/// <summary>
/// Make a HTTP PUT request (async).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <param name="cancellationToken">Token that enables callers to cancel the request.</param>
/// <returns>A Task containing ApiResponse</returns>
public Task<ApiResponse<T>> PutAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
return ExecAsync<T>(NewRequest(HttpMethod.Put, path, options, config), config, cancellationToken);
}
/// <summary>
/// Make a HTTP DELETE request (async).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <param name="cancellationToken">Token that enables callers to cancel the request.</param>
/// <returns>A Task containing ApiResponse</returns>
public Task<ApiResponse<T>> DeleteAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
return ExecAsync<T>(NewRequest(HttpMethod.Delete, path, options, config), config, cancellationToken);
}
/// <summary>
/// Make a HTTP HEAD request (async).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <param name="cancellationToken">Token that enables callers to cancel the request.</param>
/// <returns>A Task containing ApiResponse</returns>
public Task<ApiResponse<T>> HeadAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
return ExecAsync<T>(NewRequest(HttpMethod.Head, path, options, config), config, cancellationToken);
}
/// <summary>
/// Make a HTTP OPTION request (async).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <param name="cancellationToken">Token that enables callers to cancel the request.</param>
/// <returns>A Task containing ApiResponse</returns>
public Task<ApiResponse<T>> OptionsAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
return ExecAsync<T>(NewRequest(HttpMethod.Options, path, options, config), config, cancellationToken);
}
/// <summary>
/// Make a HTTP PATCH request (async).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <param name="cancellationToken">Token that enables callers to cancel the request.</param>
/// <returns>A Task containing ApiResponse</returns>
public Task<ApiResponse<T>> PatchAsync<T>(string path, RequestOptions options, IReadableConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var config = configuration ?? GlobalConfiguration.Instance;
return ExecAsync<T>(NewRequest(new HttpMethod("PATCH"), path, options, config), config, cancellationToken);
}
#endregion IAsynchronousClient
{{/supportsAsync}}
#region ISynchronousClient
/// <summary>
/// Make a HTTP GET request (synchronous).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>A Task containing ApiResponse</returns>
public ApiResponse<T> Get<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
return Exec<T>(NewRequest(HttpMethod.Get, path, options, config), config);
}
/// <summary>
/// Make a HTTP POST request (synchronous).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>A Task containing ApiResponse</returns>
public ApiResponse<T> Post<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
return Exec<T>(NewRequest(HttpMethod.Post, path, options, config), config);
}
/// <summary>
/// Make a HTTP PUT request (synchronous).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>A Task containing ApiResponse</returns>
public ApiResponse<T> Put<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
return Exec<T>(NewRequest(HttpMethod.Put, path, options, config), config);
}
/// <summary>
/// Make a HTTP DELETE request (synchronous).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>A Task containing ApiResponse</returns>
public ApiResponse<T> Delete<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
return Exec<T>(NewRequest(HttpMethod.Delete, path, options, config), config);
}
/// <summary>
/// Make a HTTP HEAD request (synchronous).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>A Task containing ApiResponse</returns>
public ApiResponse<T> Head<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
return Exec<T>(NewRequest(HttpMethod.Head, path, options, config), config);
}
/// <summary>
/// Make a HTTP OPTION request (synchronous).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>A Task containing ApiResponse</returns>
public ApiResponse<T> Options<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
return Exec<T>(NewRequest(HttpMethod.Options, path, options, config), config);
}
/// <summary>
/// Make a HTTP PATCH request (synchronous).
/// </summary>
/// <param name="path">The target path (or resource).</param>
/// <param name="options">The additional request options.</param>
/// <param name="configuration">A per-request configuration object. It is assumed that any merge with
/// GlobalConfiguration has been done before calling this method.</param>
/// <returns>A Task containing ApiResponse</returns>
public ApiResponse<T> Patch<T>(string path, RequestOptions options, IReadableConfiguration configuration = null)
{
var config = configuration ?? GlobalConfiguration.Instance;
return Exec<T>(NewRequest(new HttpMethod("PATCH"), path, options, config), config);
}
#endregion ISynchronousClient
}
}

View File

@ -31,9 +31,9 @@ using Polly;
namespace Org.OpenAPITools.Client
{
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// <summary>
/// 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
/// <summary>
/// Make a HTTP GET request (async).

View File

@ -36,7 +36,7 @@ namespace Org.OpenAPITools.Client
/// <summary>
/// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
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
}
}
};
/// <summary>
/// Allows for extending request processing for <see cref="ApiClient"/> generated code.
/// </summary>
@ -528,9 +529,6 @@ namespace Org.OpenAPITools.Client
return result;
}
private async Task<ApiResponse<T>> ExecAsync<T>(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
RestClient client = new RestClient(_baseUrl);

View File

@ -36,7 +36,7 @@ namespace Org.OpenAPITools.Client
/// <summary>
/// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
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
}
}
};
/// <summary>
/// Allows for extending request processing for <see cref="ApiClient"/> generated code.
/// </summary>
@ -528,9 +529,6 @@ namespace Org.OpenAPITools.Client
return result;
}
private async Task<ApiResponse<T>> ExecAsync<T>(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
RestClient client = new RestClient(_baseUrl);

View File

@ -35,7 +35,7 @@ namespace Org.OpenAPITools.Client
/// <summary>
/// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
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
}
}
};
/// <summary>
/// Allows for extending request processing for <see cref="ApiClient"/> generated code.
/// </summary>
@ -527,9 +528,6 @@ namespace Org.OpenAPITools.Client
return result;
}
private async Task<ApiResponse<T>> ExecAsync<T>(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
RestClient client = new RestClient(_baseUrl);

View File

@ -36,7 +36,7 @@ namespace Org.OpenAPITools.Client
/// <summary>
/// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
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
}
}
};
/// <summary>
/// Allows for extending request processing for <see cref="ApiClient"/> generated code.
/// </summary>
@ -528,9 +529,6 @@ namespace Org.OpenAPITools.Client
return result;
}
private async Task<ApiResponse<T>> ExecAsync<T>(RestRequest req, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
RestClient client = new RestClient(_baseUrl);