Src/VTEX/VTEXWrapper.cs
// ***********************************************************************
// Assembly : VTEX
// Author : Guilherme Branco Stracini
// Created : 01-15-2023
//
// Last Modified By : Guilherme Branco Stracini
// Last Modified On : 01-16-2023
// ***********************************************************************
// <copyright file="VTEXWrapper.cs" company="Guilherme Branco Stracini">
// © 2020 Guilherme Branco Stracini. All rights reserved.
// </copyright>
// <summary></summary>
// ***********************************************************************
namespace VTEX
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CrispyWaffle.Extensions;
using CrispyWaffle.Log;
using CrispyWaffle.Telemetry;
using CrispyWaffle.Utilities;
using Enums;
using GoodPractices;
/// <summary>
/// Class Wrapper. This class cannot be inherited.
/// </summary>
/// <seealso cref="IDisposable" />
// TODO change public to internal after remove from Integração Service
public sealed class VTEXWrapper : IDisposable
{
#region Private fields
/// <summary>
/// The application key
/// </summary>
private string _appKey;
/// <summary>
/// The application token
/// </summary>
private string _appToken;
/// <summary>
/// The authentication cookie
/// </summary>
private string _authCookie;
/// <summary>
/// The account name
/// </summary>
private readonly string _accountName;
/// <summary>
/// The internal user agent
/// </summary>
private static string _internalUserAgent;
/// <summary>
/// Gets the internal user agent.
/// </summary>
/// <value>The internal user agent.</value>
private static string InternalUserAgent
{
get
{
if (!string.IsNullOrWhiteSpace(_internalUserAgent))
{
return _internalUserAgent;
}
var assembly = System
.Reflection.Assembly.GetAssembly(typeof(VTEXWrapper))
.GetName();
_internalUserAgent = $@"{assembly.Name}/{assembly.Version}";
return _internalUserAgent;
}
}
/// <summary>
/// The request mediator
/// </summary>
private readonly ManualResetEvent _requestMediator = new ManualResetEvent(false);
#endregion
#region ~Ctor
/// <summary>
/// Initializes a new instance of the <see cref="VTEXWrapper" /> class.
/// </summary>
/// <param name="accountName">The account name.</param>
public VTEXWrapper(string accountName)
{
_accountName = accountName;
_requestMediator.Set();
}
#endregion
#region Implementation of IDisposable
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
_appKey = null;
_appToken = null;
_requestMediator.Dispose();
}
#endregion
#region Private methods
/// <summary>
/// Services the invoker internal.
/// </summary>
/// <param name="method">The method.</param>
/// <param name="endpoint">The endpoint.</param>
/// <param name="token">The token.</param>
/// <param name="data">The data.</param>
/// <param name="uriBuilder">The URI builder.</param>
/// <param name="cookie">The cookie.</param>
/// <param name="requiresAuthentication">if set to <c>true</c> [requires authentication].</param>
/// <param name="isRetry">if set to <c>true</c> [is retry].</param>
/// <returns>System.String.</returns>
private async Task<string> ServiceInvokerInternal(
HttpRequestMethod method,
string endpoint,
CancellationToken token,
string data,
UriBuilder uriBuilder,
Cookie cookie,
bool requiresAuthentication,
bool isRetry = false
)
{
HttpResponseMessage response = null;
string result = null;
Exception exr;
try
{
_requestMediator.WaitOne();
LogConsumer.Trace(
"ServiceInvokerAsync -> Method: {0} | Endpoint: {1}",
method.GetHumanReadableValue(),
endpoint
);
LogConsumer.Debug(uriBuilder.ToString());
var cookieContainer = new CookieContainer();
using var handler = new HttpClientHandler { CookieContainer = cookieContainer };
using var client = new HttpClient(handler);
ConfigureClient(client, requiresAuthentication);
if (cookie != null)
{
cookieContainer.Add(uriBuilder.Uri, cookie);
}
response = await RequestInternalAsync(method, token, data, client, uriBuilder)
.ConfigureAwait(false);
token.ThrowIfCancellationRequested();
result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return result;
}
catch (AggregateException e)
{
var ex = e.InnerExceptions.FirstOrDefault() ?? e.InnerException ?? e;
exr = HandleException(ex, response, uriBuilder.Uri, method, data, result);
if (isRetry)
{
throw exr;
}
}
catch (Exception e)
{
exr = HandleException(e, response, uriBuilder.Uri, method, data, result);
if (isRetry)
{
throw exr;
}
}
return await ServiceInvokerInternal(
method,
endpoint,
token,
data,
uriBuilder,
cookie,
requiresAuthentication,
true
)
.ConfigureAwait(false);
}
/// <summary>
/// Handles the exception.
/// </summary>
/// <param name="exception">The exception.</param>
/// <param name="response">The response.</param>
/// <param name="uri">The URI.</param>
/// <param name="method">The method.</param>
/// <param name="data">The data.</param>
/// <param name="result">The result.</param>
/// <returns>Exception.</returns>
/// <exception cref="UnexpectedApiResponseException"></exception>
private Exception HandleException(
Exception exception,
HttpResponseMessage response,
Uri uri,
HttpRequestMethod method,
string data,
string result
)
{
var statusCode = 0;
if (response != null)
{
statusCode = (int)response.StatusCode;
}
var ex = new UnexpectedApiResponseException(
uri,
method.ToString(),
data,
result,
statusCode,
exception
);
if (statusCode == 429 || statusCode == 503)
{
_requestMediator.Reset();
LogConsumer.Warning(
"HTTP {2} status code on method {0} - uri {1}",
method.ToString(),
uri,
statusCode
);
Thread.Sleep(60 * 1000);
_requestMediator.Set();
return ex;
}
if (statusCode != 0 && statusCode != 408 && statusCode != 500 && statusCode != 502)
{
throw ex;
}
LogConsumer.Warning("Retrying the {0} request", method.ToString());
TelemetryAnalytics.TrackHit(
$"VTEX_handle_exception_retrying_{method.ToString()}_request"
);
return ex;
}
/// <summary>
/// Configures the client.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="requiresAuthentication">if set to <c>true</c> [requires authentication].</param>
private void ConfigureClient(HttpClient client, bool requiresAuthentication)
{
client.DefaultRequestHeaders.ExpectContinue = false;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue(@"application/json")
);
client.DefaultRequestHeaders.TryAddWithoutValidation(
@"User-Agent",
$@"guiBranco-VTEX-SDK-dotnet {InternalUserAgent} +https://github.com/guibranco/VTEX-SDK-dotnet"
);
if (!requiresAuthentication)
{
return;
}
client.DefaultRequestHeaders.Add(@"X-VTEX-API-AppKey", _appKey);
client.DefaultRequestHeaders.Add(@"X-VTEX-API-AppToken", _appToken);
}
/// <summary>
/// Requests the internal asynchronous.
/// </summary>
/// <param name="method">The method.</param>
/// <param name="token">The token.</param>
/// <param name="data">The data.</param>
/// <param name="client">The client.</param>
/// <param name="uriBuilder">The URI builder.</param>
/// <returns>A Task<HttpResponseMessage> representing the asynchronous operation.</returns>
/// <exception cref="System.ArgumentOutOfRangeException">method - null</exception>
private static async Task<HttpResponseMessage> RequestInternalAsync(
HttpRequestMethod method,
CancellationToken token,
string data,
HttpClient client,
UriBuilder uriBuilder
)
{
HttpResponseMessage response;
StringContent content = null;
if (!string.IsNullOrWhiteSpace(data))
{
content = new StringContent(data, Encoding.UTF8, @"application/json");
}
switch (method)
{
case HttpRequestMethod.DELETE:
response = await client
.DeleteAsync(uriBuilder.Uri, token)
.ConfigureAwait(false);
break;
case HttpRequestMethod.GET:
response = await client.GetAsync(uriBuilder.Uri, token).ConfigureAwait(false);
break;
case HttpRequestMethod.POST:
response = await client
.PostAsync(uriBuilder.Uri, content, token)
.ConfigureAwait(false);
break;
case HttpRequestMethod.PUT:
response = await client
.PutAsync(uriBuilder.Uri, content, token)
.ConfigureAwait(false);
break;
case HttpRequestMethod.PATCH:
var request = new HttpRequestMessage(new HttpMethod(@"PATCH"), uriBuilder.Uri)
{
Content = content
};
response = await client.SendAsync(request, token).ConfigureAwait(false);
request.Dispose();
break;
default:
throw new ArgumentOutOfRangeException(nameof(method), method, null);
}
return response;
}
#endregion
#region Public methods
/// <summary>
/// Sets the rest credentials.
/// </summary>
/// <param name="appKey">The application key.</param>
/// <param name="appToken">The application token.</param>
public void SetRestCredentials(string appKey, string appToken)
{
_appKey = appKey;
_appToken = appToken;
}
/// <summary>
/// Sets the vtex identifier client authentication cookie.
/// </summary>
/// <param name="cookieValue">The cookie value.</param>
public void SetVtexIdClientAuthCookie(string cookieValue)
{
_authCookie = cookieValue;
}
/// <summary>
/// Services the invoker asynchronous.
/// </summary>
/// <param name="method">The method.</param>
/// <param name="endpoint">The endpoint.</param>
/// <param name="token">The token.</param>
/// <param name="queryString">The query string.</param>
/// <param name="data">The data.</param>
/// <param name="restEndpoint">The rest endpoint.</param>
/// <returns>A Task<System.String> representing the asynchronous operation.</returns>
/// <exception cref="System.ArgumentOutOfRangeException">restEndpoint - null</exception>
public async Task<string> ServiceInvokerAsync(
HttpRequestMethod method,
[Localizable(false)] string endpoint,
CancellationToken token,
Dictionary<string, string> queryString = null,
string data = null,
RequestEndpoint restEndpoint = RequestEndpoint.DEFAULT
)
{
Cookie cookie = null;
var requiresAuthentication = true;
var protocol = @"https";
var port = 443;
var host = GetHostData(
ref endpoint,
ref queryString,
restEndpoint,
ref cookie,
ref protocol,
ref port,
ref requiresAuthentication
);
var query = string.Empty;
if (queryString is { Count: > 0 })
{
query = new QueryStringBuilder().AddRange(queryString).ToString();
}
var builder = new UriBuilder(protocol, host, port, endpoint)
{
Query = query.Replace(@"?", string.Empty)
};
return await ServiceInvokerInternal(
method,
endpoint,
token,
data,
builder,
cookie,
requiresAuthentication
)
.ConfigureAwait(false);
}
/// <summary>
/// Gets the host data.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="queryString">The query string.</param>
/// <param name="restEndpoint">The rest endpoint.</param>
/// <param name="cookie">The cookie.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="port">The port.</param>
/// <param name="requiresAuthentication">if set to <c>true</c> [requires authentication].</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentOutOfRangeException">restEndpoint - null</exception>
private string GetHostData(
ref string endpoint,
ref Dictionary<string, string> queryString,
RequestEndpoint restEndpoint,
ref Cookie cookie,
ref string protocol,
ref int port,
ref bool requiresAuthentication
)
{
string host;
switch (restEndpoint)
{
case RequestEndpoint.DEFAULT:
host = $@"{_accountName}.{VTEXConstants.PlatformStableDomain}";
endpoint = $@"api/{endpoint}";
break;
case RequestEndpoint.PAYMENTS:
host = $@"{_accountName}.{VTEXConstants.PaymentsDomain}";
endpoint = $@"api/{endpoint}";
break;
case RequestEndpoint.LOGISTICS:
host = VTEXConstants.LogisticsDomain;
endpoint = $@"api/{endpoint}";
if (queryString == null)
{
queryString = new();
}
queryString.Add(@"an", _accountName);
break;
case RequestEndpoint.API:
case RequestEndpoint.MASTER_DATA:
host = VTEXConstants.ApiDomain;
endpoint = $@"{_accountName}/{endpoint}";
break;
case RequestEndpoint.BRIDGE:
host = $@"{_accountName}.{VTEXConstants.MyVtexDomain}";
endpoint = $@"api/{endpoint}";
if (!string.IsNullOrWhiteSpace(_authCookie))
{
cookie = new(VTEXConstants.VtexIdClientAuthCookieName, _authCookie);
}
break;
case RequestEndpoint.HEALTH:
protocol = @"http";
port = 80;
host = VTEXConstants.MonitoringDomain;
endpoint = @"api/healthcheck/modules";
requiresAuthentication = false;
break;
default:
throw new ArgumentOutOfRangeException(nameof(restEndpoint), restEndpoint, null);
}
return host;
}
#endregion
}
}