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