Step 7: Implementing a REST Client of the External System
You will now implement a REST API client of the external system.
Note:
The classes and the communication with the external
system depend on the external system.
Implementing a REST Client of the External System
- In the Visual Studio project of the extension library, for each external entity,
define the data provider class, as shown in the following example. This is a
helper class that is used by the processor class to retrieve the data of a
particular entity from the external system. In this example, the implementation
of the data provider classes for the customer entity and the system status
entity involves the following classes and interfaces:
RestDataProviderBase
, which is shown in the following codeTip:You can see this code on GitHub.using PX.Commerce.Core; using PX.Common; using System.Collections.Generic; using System.Net; using System.Threading; using BigCom = PX.Commerce.BigCommerce.API.REST; namespace WooCommerceTest { public abstract class RestDataProviderBase { internal const string COMMERCE_RETRY_COUNT = "CommerceRetryCount"; protected const int BATCH_SIZE = 10; protected const string ID_STRING = "id"; protected const string PARENT_ID_STRING = "parent_id"; protected const string OTHER_PARAM = "other_param"; protected readonly int commerceRetryCount = WebConfig.GetInt(COMMERCE_RETRY_COUNT, 3); protected IWooRestClient _restClient; protected abstract string GetListUrl { get; } protected abstract string GetSingleUrl { get; } public RestDataProviderBase() { } public virtual T Create<T>(T entity, BigCom.UrlSegments urlSegments = null) where T : class, IWooEntity, new() { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .ForContext("Object", entity) .Verbose("{CommerceCaption}: WooCommerce REST API - Creating new {EntityType} entity with parameters {UrlSegments}", BCCaptions.CommerceLogCaption, typeof(T).ToString(), urlSegments?.ToString() ?? "none"); int retryCount = 0; while (true) { try { var request = _restClient.MakeRequest(GetListUrl, urlSegments?.GetUrlSegments()); request.Method = RestSharp.Method.POST; T result = _restClient.Post<T>(request, entity); return result; } catch (BigCom.RestException ex) { if (ex?.ResponceStatusCode == default(HttpStatusCode).ToString() && retryCount < commerceRetryCount) { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope( GetType())) .Error("{CommerceCaption}: Operation failed, RetryCount { RetryCount}, Exception {ExceptionMessage}", BCCaptions.CommerceLogCaption, retryCount, ex?.ToString()); retryCount++; Thread.Sleep(1000 * retryCount); } else throw; } } } public virtual TE Create<T, TE>(List<T> entities, BigCom.UrlSegments urlSegments = null) where T : class, IWooEntity, new() where TE : IList<T>, new() { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .ForContext("Object", entities) .Verbose("{CommerceCaption}: WooCommerce REST API - Creating new {EntityType} entity with parameters {UrlSegments}", BCCaptions.CommerceLogCaption, typeof(T).ToString(), urlSegments?.ToString() ?? "none"); int retryCount = 0; while (true) { try { var request = _restClient.MakeRequest(GetListUrl, urlSegments?.GetUrlSegments()); TE result = _restClient.Post<T, TE>(request, entities); return result; } catch (BigCom.RestException ex) { if (ex?.ResponceStatusCode == default(HttpStatusCode).ToString() && retryCount < commerceRetryCount) { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope( GetType())) .Error("{CommerceCaption}: Operation failed, RetryCount {RetryCount}, Exception {ExceptionMessage}", BCCaptions.CommerceLogCaption, retryCount, ex?.ToString()); retryCount++; Thread.Sleep(1000 * retryCount); } else throw; } } } public virtual T Update<T>(T entity, BigCom.UrlSegments urlSegments) where T : class, new() { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .ForContext("Object", entity) .Verbose("{CommerceCaption}: WooCommerce REST API - Updating {EntityType} entity with parameters {UrlSegments}", BCCaptions.CommerceLogCaption, typeof(T).ToString(), urlSegments?.ToString() ?? "none"); int retryCount = 0; while (true) { try { var request = _restClient.MakeRequest(GetSingleUrl, urlSegments?.GetUrlSegments()); T result = _restClient.Put<T>(request, entity); return result; } catch (BigCom.RestException ex) { if (ex?.ResponceStatusCode == default(HttpStatusCode).ToString() && retryCount < commerceRetryCount) { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .Error("{CommerceCaption}: Operation failed, RetryCount {RetryCount}, Exception {ExceptionMessage}", BCCaptions.CommerceLogCaption, retryCount, ex?.ToString()); retryCount++; Thread.Sleep(1000 * retryCount); } else throw; } } } public virtual bool Delete(BigCom.UrlSegments urlSegments) { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .Verbose("{CommerceCaption}: WooCommerce REST API - Deleting {EntityType} entry with parameters {UrlSegments}", BCCaptions.CommerceLogCaption, GetType().ToString(), urlSegments?.ToString() ?? "none"); int retryCount = 0; while (true) { try { var request = _restClient.MakeRequest(GetSingleUrl, urlSegments.GetUrlSegments()); var result = _restClient.Delete(request); return result; } catch (BigCom.RestException ex) { if (ex?.ResponceStatusCode == default(HttpStatusCode).ToString() && retryCount < commerceRetryCount) { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .Error("{CommerceCaption}: Operation failed, RetryCount {RetryCount}, Exception {ExceptionMessage}", BCCaptions.CommerceLogCaption, retryCount, ex?.ToString()); retryCount++; Thread.Sleep(1000 * retryCount); } else throw; } } } protected static BigCom.UrlSegments MakeUrlSegments(string id) { var segments = new BigCom.UrlSegments(); segments.Add(ID_STRING, id); return segments; } protected static BigCom.UrlSegments MakeParentUrlSegments( string parentId) { var segments = new BigCom.UrlSegments(); segments.Add(PARENT_ID_STRING, parentId); return segments; } protected static BigCom.UrlSegments MakeUrlSegments(string id, string parentId) { var segments = new BigCom.UrlSegments(); segments.Add(PARENT_ID_STRING, parentId); segments.Add(ID_STRING, id); return segments; } protected static BigCom.UrlSegments MakeUrlSegments(string id, string parentId, string param) { var segments = new BigCom.UrlSegments(); segments.Add(PARENT_ID_STRING, parentId); segments.Add(ID_STRING, id); segments.Add(OTHER_PARAM, param); return segments; } } }
IFilter
, which is shown in the code belowTip:You can see this code on GitHub.using RestSharp; using System; namespace WooCommerceTest { public interface IFilter { void AddFilter(IRestRequest request); int? Limit { get; set; } int? Page { get; set; } int? Offset { get; set; } string Order { get; set; } string OrderBy { get; set; } DateTime? CreatedAfter { get; set; } } }
Filter
, which is shown in the following codeTip:You can see this code on GitHub.using RestSharp; using RestSharp.Extensions; using System; using System.ComponentModel; namespace WooCommerceTest { public class Filter : IFilter { protected const string RFC2822_DATE_FORMAT = "{0:ddd, dd MMM yyyy HH:mm:ss} GMT"; protected const string ISO_DATE_FORMAT = "{0:yyyy-MM-ddTHH:mm:ss}"; [Description("per_page")] public int? Limit { get; set; } [Description("page")] public int? Page { get; set; } [Description("offset")] public int? Offset { get; set; } [Description("order")] public string Order { get; set; } [Description("orderby")] public string OrderBy { get; set; } public DateTime? CreatedAfter { get; set; } public virtual void AddFilter(IRestRequest request) { foreach (var propertyInfo in GetType().GetProperties()) { DescriptionAttribute attr = propertyInfo. GetAttribute<DescriptionAttribute>(); if (attr == null) continue; string key = attr.Description; object value = propertyInfo.GetValue(this); if (value != null) { if (propertyInfo.PropertyType == typeof(DateTime) || propertyInfo.PropertyType == typeof(DateTime?)) { value = string.Format(ISO_DATE_FORMAT, value); } request.AddParameter(key, value); } } } } }
RestDataProvider
, which is shown in the code belowTip:You can see this code on GitHub.using PX.Commerce.Core; using PX.Common; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using BigCom = PX.Commerce.BigCommerce.API.REST; namespace WooCommerceTest { public abstract class RestDataProvider : RestDataProviderBase { public RestDataProvider() : base() { } public virtual TE Get<T, TE>(IFilter filter = null, BigCom.UrlSegments urlSegments = null) where T : class, IWooEntity, new() where TE : IEnumerable<T>, new() { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .Verbose("{CommerceCaption}: WooCommerce REST API - Getting {EntityType} entry with parameters {UrlSegments}", BCCaptions.CommerceLogCaption, GetType().ToString(), urlSegments?.ToString() ?? "none"); var request = _restClient.MakeRequest(GetListUrl, urlSegments?.GetUrlSegments()); filter?.AddFilter(request); var response = _restClient.GetList<T, TE>(request); return response; } public virtual IEnumerable<T> GetAll<T, TE>(IFilter filter = null, BigCom.UrlSegments urlSegments = null) where T : class, IWooEntity, new() where TE : IEnumerable<T>, new() { var localFilter = filter ?? new Filter(); var needGet = true; localFilter.Page = localFilter.Page.HasValue ? localFilter.Page : 1; localFilter.Limit = localFilter.Limit.HasValue ? localFilter.Limit : 50; localFilter.Order = "desc"; TE entity = default; while (needGet) { int retryCount = 0; while (retryCount <= commerceRetryCount) { try { entity = Get<T, TE>(localFilter, urlSegments); break; } catch (Exception ex) { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .Verbose("{CommerceCaption}: Failed at page {Page}, RetryCount {RetryCount}, Exception {ExceptionMessage}", BCCaptions.CommerceLogCaption, localFilter.Page, retryCount, ex?.Message); if (retryCount == commerceRetryCount) throw; retryCount++; Thread.Sleep(1000 * retryCount); } } if (entity == null) yield break; foreach (T data in entity) { yield return data; } if (entity.Count() < localFilter.Limit) needGet = false; else if (entity.Count() == localFilter.Limit && localFilter.CreatedAfter != null && localFilter.CreatedAfter > entity.ToList()[localFilter.Limit.Value - 1].DateCreatedUT) needGet = false; localFilter.Page++; } } public virtual T GetByID<T>(BigCom.UrlSegments urlSegments, IFilter filter = null) where T : class, new() { _restClient.Logger?.ForContext("Scope", new BCLogTypeScope(GetType())) .Verbose("{CommerceCaption}: WooCommerce REST API - Getting by ID {EntityType} entry with parameters {UrlSegments}", BCCaptions.CommerceLogCaption, typeof(T).ToString(), urlSegments?.ToString() ?? "none"); var request = _restClient.MakeRequest(GetSingleUrl, urlSegments.GetUrlSegments()); if (filter != null) filter.AddFilter(request); return _restClient.Get<T>(request); } public virtual bool Delete(IFilter filter = null) { var request = _restClient.MakeRequest(GetSingleUrl); filter?.AddFilter(request); var response = _restClient.Delete(request); return response; } } }
CustomerDataProvider
, which is shown in the following codeTip:You can see this code on GitHub.using System.Collections.Generic; namespace WooCommerceTest { public class CustomerDataProvider : RestDataProvider { protected override string GetListUrl { get; } = "/customers"; protected override string GetSingleUrl { get; } = "/customers/{id}"; public CustomerDataProvider(IWooRestClient restClient) : base() { _restClient = restClient; } public IEnumerable<CustomerData> GetAll(IFilter filter = null) { var localFilter = filter ?? new Filter(); localFilter.OrderBy = "registered_date"; return base.GetAll<CustomerData, List<CustomerData>>(filter); } public CustomerData GetCustomerById(int id) { var segments = CustomerDataProvider.MakeUrlSegments(id.ToString()); return base.GetByID<CustomerData>(segments); } } }
SystemStatusProvider
, which is shown belowTip:You can see this code on GitHub.namespace WooCommerceTest { public class SystemStatusProvider { private readonly IWooRestClient _restClient; public SystemStatusProvider(IWooRestClient restClient) { _restClient = restClient; } public SystemStatusData Get() { const string resourceUrl = "/system_status"; var request = _restClient.MakeRequest(resourceUrl); var country = _restClient.Get<SystemStatusData>(request); return country; } } }
- Create a class for the REST client of the external system, which defines the
methods that you need to process REST requests to the external system. In this
example, you are creating the
WooRestClient
class, shown in the following code.Tip:You can see this code on GitHub.using PX.Commerce.Core; using RestSharp; using RestSharp.Deserializers; using RestSharp.Serializers; using System; using System.Collections.Generic; using System.Net; using BigCom = PX.Commerce.BigCommerce.API.REST; namespace WooCommerceTest { public class WooRestClient : WooRestClientBase, IWooRestClient { public WooRestClient(IDeserializer deserializer, ISerializer serializer, BigCom.IRestOptions options, Serilog.ILogger logger) : base(deserializer, serializer, options, logger) { } public T Get<T>(string url) where T : class, new() { RestRequest request = MakeRequest(url); request.Method = Method.GET; var response = Execute<T>(request); if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NoContent || response.StatusCode == HttpStatusCode.NotFound) { T result = response.Data; if (result != null && result is BCAPIEntity) ( result as BCAPIEntity).JSON = response.Content; return result; } throw new Exception(response.Content); } public T Post<T>(IRestRequest request, T entity) where T : class, IWooEntity, new() { request.Method = Method.POST; request.AddJsonBody(entity); IRestResponse<T> response = Execute<T>(request); if (response.StatusCode == HttpStatusCode.Created || response.StatusCode == HttpStatusCode.OK) { T result = response.Data; if (result != null && result is BCAPIEntity) ( result as BCAPIEntity).JSON = response.Content; return result; } LogError(BaseUrl, request, response); throw new BigCom.RestException(response); } public TE Post<T, TE>(IRestRequest request, List<T> entities) where T : class, IWooEntity, new() where TE : IEnumerable<T>, new() { request.Method = Method.POST; request.AddJsonBody(entities); IRestResponse<TE> response = Execute<TE>(request); if (response.StatusCode == HttpStatusCode.Created || response.StatusCode == HttpStatusCode.OK) { TE result = response.Data; if (result != null && result is IEnumerable<BCAPIEntity>) (result as List<BCAPIEntity>).ForEach( e => e.JSON = response.Content); return result; } LogError(BaseUrl, request, response); throw new BigCom.RestException(response); } public T Put<T>(IRestRequest request, T entity) where T : class, new() { request.Method = Method.PUT; request.AddJsonBody(entity); var response = Execute<T>(request); if (response.StatusCode == HttpStatusCode.Created || response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NoContent) { T result = response.Data; if (result != null && result is BCAPIEntity) ( result as BCAPIEntity).JSON = response.Content; return result; } LogError(BaseUrl, request, response); throw new BigCom.RestException(response); } public T Get<T>(IRestRequest request) where T : class, new() { request.Method = Method.GET; var response = Execute<T>(request); if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NoContent || response.StatusCode == HttpStatusCode.NotFound) { T result = response.Data; if (result != null && result is BCAPIEntity) ( result as BCAPIEntity).JSON = response.Content; return result; } LogError(BaseUrl, request, response); throw new BigCom.RestException(response); } public TE GetList<T, TE>(IRestRequest request) where T : class, IWooEntity, new() where TE : IEnumerable<T>, new() { request.Method = Method.GET; var response = Execute<TE>(request); if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NoContent) { TE result = response.Data; return result; } LogError(BaseUrl, request, response); throw new BigCom.RestException(response); } public bool Delete(IRestRequest request) { request.Method = Method.DELETE; var response = Execute(request); if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NoContent || response.StatusCode == HttpStatusCode.NotFound) { return true; } LogError(BaseUrl, request, response); throw new BigCom.RestException(response); } } }
In this example, the following supplementary classes and interfaces are used, which are shown in the following code fragments:WooRestClientBase
Tip:You can see this code on GitHub.using System; using System.Collections.Generic; using System.Linq; using PX.Commerce.BigCommerce.API.REST; using PX.Commerce.Core; using RestSharp; using RestSharp.Deserializers; using RestSharp.Serializers; namespace WooCommerceTest { public abstract class WooRestClientBase : RestClient { protected ISerializer _serializer; protected IDeserializer _deserializer; public Serilog.ILogger Logger { get; set; } = null; protected WooRestClientBase(IDeserializer deserializer, ISerializer serializer, IRestOptions options, Serilog.ILogger logger) { _serializer = serializer; _deserializer = deserializer; AddHandler("application/json", () => deserializer); AddHandler("text/json", () => deserializer); AddHandler("text/x-json", () => deserializer); Authenticator = new Autentificator(options.XAuthClient, options.XAuthTocken); try { BaseUrl = new Uri(options.BaseUri); } catch (UriFormatException e) { throw new UriFormatException( "Invalid URL: The format of the URL could not be determined.", e); } Logger = logger; } public RestRequest MakeRequest(string url, Dictionary<string, string> urlSegments = null) { var request = new RestRequest(url) { JsonSerializer = _serializer, RequestFormat = DataFormat.Json }; if (urlSegments != null) { foreach (var urlSegment in urlSegments) { request.AddUrlSegment(urlSegment.Key, urlSegment.Value); } } return request; } protected void LogError(Uri baseUrl, IRestRequest request, IRestResponse response) { //Get the values of the parameters passed to the API var parameters = string.Join(", ", request.Parameters.Select( x => x.Name.ToString() + "=" + (x.Value ?? "NULL")).ToArray()); //Set up the information message with the URL, //the status code, and the parameters. var info = "Request to " + baseUrl.AbsoluteUri + request.Resource + " failed with status code " + response.StatusCode + ", parameters: " + parameters; var description = "Response content: " + response.Content; //Acquire the actual exception var ex = (response.ErrorException?.Message) ?? info; //Log the exception and info message Logger.ForContext("Scope", new BCLogTypeScope(GetType())) .ForContext("Exception", response.ErrorException?.Message) .Error("{CommerceCaption}: {ResponseError}, Status Code: {StatusCode}", BCCaptions.CommerceLogCaption, description, response.StatusCode); } } }
IWooRestClient
Tip:You can see this code on GitHub.using RestSharp; using Serilog; using System.Collections.Generic; namespace WooCommerceTest { public interface IWooRestClient { RestRequest MakeRequest(string url, Dictionary<string, string> urlSegments = null); T Post<T>(IRestRequest request, T entity) where T : class, IWooEntity, new(); TE Post<T, TE>(IRestRequest request, List<T> entities) where T : class, IWooEntity, new() where TE : IEnumerable<T>, new(); T Put<T>(IRestRequest request, T entity) where T : class, new(); T Get<T>(IRestRequest request) where T : class, new(); TE GetList<T, TE>(IRestRequest request) where T : class, IWooEntity, new() where TE : IEnumerable<T>, new(); ILogger Logger { set; get; } bool Delete(IRestRequest request); } }
IWooEntity
Tip:You can see this code on GitHub.using System; namespace WooCommerceTest { public interface IWooEntity { DateTime? DateCreatedUT { get; set; } DateTime? DateModified { get; set; } } }
Authenticator
Tip:You can see this code on GitHub.using RestSharp; using RestSharp.Authenticators; using RestSharp.Authenticators.OAuth; namespace WooCommerceTest { public class Autentificator : IAuthenticator { private readonly string _consumerKey; private readonly string _consumerSecret; public Autentificator(string consumerKey, string consumerSecret) { _consumerKey = consumerKey; _consumerSecret = consumerSecret; } public void Authenticate(IRestClient client, IRestRequest request) { request.BuildOAuth1QueryString((RestClient)client, _consumerKey, _consumerSecret); } } public static class RestRequestExtensions { public static IRestRequest BuildOAuth1QueryString( this IRestRequest request, RestClient client, string consumerKey, string consumerSecret) { var auth = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret); auth.ParameterHandling = OAuthParameterHandling.UrlOrPostParameters; auth.Authenticate(client, request); //Convert all these oauth params from cookie to querystring request.Parameters.ForEach(x => { if (x.Name.StartsWith("oauth_")) x.Type = ParameterType.QueryString; }); return request; } } }
- Create the connector class and define the
GetRestClient
static methods in it, as shown in the following code.Tip:You can see this code on GitHub.using Newtonsoft.Json; using System; using PX.Commerce.BigCommerce.API.REST; using PX.Commerce.Core.REST; using PX.Commerce.Objects; using CommonServiceLocator; namespace WooCommerceTest { public class WooCommerceConnector { public static WooRestClient GetRestClient(BCBindingWooCommerce binding) { return GetRestClient(binding.StoreBaseUrl, binding.StoreXAuthClient, binding.StoreXAuthToken); } public static WooRestClient GetRestClient(String url, String clientID, String token) { RestOptions options = new RestOptions { BaseUri = url, XAuthClient = clientID, XAuthTocken = token }; JsonSerializer serializer = new JsonSerializer { MissingMemberHandling = MissingMemberHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Include, DateFormatHandling = DateFormatHandling.IsoDateFormat, DateTimeZoneHandling = DateTimeZoneHandling.Unspecified, ContractResolver = new GetOnlyContractResolver() }; RestJsonSerializer restSerializer = new RestJsonSerializer(serializer); WooRestClient client = new WooRestClient(restSerializer, restSerializer, options, ServiceLocator.Current.GetInstance<Serilog.ILogger>()); return client; } } }
- Build the project.