public class MagentoClientService : HttpClientBase, IMagentoClientService { private readonly MagentoSettings _settings; private readonly HttpClient _client; private readonly ILogger _logger; public MagentoClientService(HttpClient httpClient, IOptions settings, IDistributedCache cache, ILogger logger) : base(httpClient, settings, cache) { _settings = settings.Value ?? throw new ArgumentNullException(nameof(settings)); _client = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public string Token { get; set; } #region Product public async Task CreateProductAsync(Product product) { Stopwatch watch = new Stopwatch(); watch.Start(); SetAuthenticationHeader(); string url = $"{_settings.GetMagentoProductsUrl()}/{product.Id}"; RootProduct root = new RootProduct() { Product = product }; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(root, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); var result = await _client.PutAsync(url, content); watch.Stop(); if (Debugger.IsAttached) { //_logger.LogInformation($"Request:{Environment.NewLine}" + // $"TraceId:{context.TraceIdentifier} " + // $"Schema:{context.Request.Scheme} " + // $"Host: {context.Request.Host} " + // $"Path: {context.Request.Path} " + // $"Method: {context.Request.Method} " + // $"QueryString: {context.Request.QueryString} " + // $"Request Body: {bodyAsText}"); } if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.Unauthorized || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await result.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"The product {product.Id} cannot be created or updated {(int)result.StatusCode} error {error?.message}"); } if (!result.IsSuccessStatusCode) _logger.LogError($"{product.Id}- {product.Name} cannot be created or updated {(int)result.StatusCode}"); return result; } public async Task ReadProductAsync(string productId) { Stopwatch watch = new Stopwatch(); watch.Start(); SetAuthenticationHeader(); string url = $"{_settings.GetMagentoProductsUrl()}/{productId}"; return await _client.GetAsync(url); } public async Task UpdateProductAsync(Product product) { var result = await CreateProductAsync(product); return result; } public async Task UpdateProductStatusAsync(ProductBase product) { Stopwatch watch = new Stopwatch(); watch.Start(); SetAuthenticationHeader(); string url = $"{_settings.GetMagentoProductsUrl()}/{product.Id}"; RootProductBase root = new RootProductBase() { Product = product }; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(root, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); //Trace.WriteLine(url); //Trace.WriteLine(inData); var result = await _client.PutAsync(url, content); watch.Stop(); if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.Unauthorized || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await result.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"The product {product.Id} cannot be created or updated {(int)result.StatusCode} error {error?.message}"); } if (!result.IsSuccessStatusCode) _logger.LogError($"{product.Id} - {product.Name} cannot be created or updated {(int)result.StatusCode}"); return result; } public async Task DeleteProductAsync(string productId) { SetAuthenticationHeader(); string url = $"{_settings.GetMagentoProductsUrl()}/{productId}"; var result = await _client.DeleteAsync(url); if (!result.IsSuccessStatusCode) _logger.LogError($"{productId} cannot be deleted {(int)result.StatusCode}"); return result; } public async Task DeleteProductFromCategoryAsync(string categoryId, string productId) { SetAuthenticationHeader(); string url = $"{_settings.GetMagentoCategoriesUrl(categoryId, productId)}"; var response = await _client.DeleteAsync(url); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest || response.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await response.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"Cannot remove the product {productId} assignment from the category {categoryId}. Message : {error.message}"); return response; } return response; } public async Task AddProductToCategoryAsync(string categoryId, string productId) { SetAuthenticationHeader(); string url = $"{_settings.GetMagentoCategoriesUrl(categoryId)}"; RootProductlink root = new() { ProductLink = new Productlink() { CategoryId = categoryId, Position = 0, Sku = productId } }; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(root, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); var response = await _client.PostAsync(url, content); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest || response.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await response.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"Cannot add the product {productId} to the category {categoryId}. Message : {error.message}"); return response; } return response; } #endregion #region Stock public async Task UpdateStockAsync(Inventory.Stock stock) { if (stock == null) throw new ArgumentNullException(nameof(stock)); SetAuthenticationHeader(); string url = $"{_settings.GetMagentoProductsUrl()}/{stock.Product.Sku}"; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(stock, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); var result = await _client.PutAsync(url, content); //https://devdocs.magento.com/redoc/2.2/#tag/productsproductSkustockItemsitemId if (!result.IsSuccessStatusCode) { string cont = await result.Content.ReadAsStringAsync(); //_logger.LogInformation($"delete with status {cont}"); if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.Unauthorized || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { var error = MagentoError.FromJson(cont); _logger.LogError($"Stock item of the product {stock.Product.Sku} cannot be uploaded correctly. status code {(int)result.StatusCode} error {error?.message}"); } } return result; } #endregion #region Media public async Task UploadPhotoAsync(string productId, MediaGallery media) { SetAuthenticationHeader(); string url = $"{_settings.GetMagentoProductsUrl()}/{productId}/media"; RootMedia root = new RootMedia() { Media = media }; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(root, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); //Trace.WriteLine(url); //Trace.WriteLine(inData); //var result = await _client.PostAsync(url, content); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content, Version = HttpVersion.Version11 }; HttpResponseMessage result = _client.SendAsync(request).Result; if (!result.IsSuccessStatusCode) { string cont = await result.Content.ReadAsStringAsync(); if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.Unauthorized || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { var error = MagentoError.FromJson(cont); _logger.LogError($"{productId}-{media.FileContent.Name} cannot be uploaded. status code {(int)result.StatusCode} error {error?.message}"); } } return result; } public async Task DeletePhotoAsync(string productId, int mediaId) { HttpResponseMessage resp = null; int tentatives = 1; string deleteUrl = $"{_settings.GetMagentoProductsUrl()}/{productId}/media/{mediaId}"; //Trace.WriteLine(deleteUrl); do { SetAuthenticationHeader(); tentatives++; resp = await _client.DeleteAsync(deleteUrl); if (resp.IsSuccessStatusCode) break; string cont = await resp.Content.ReadAsStringAsync(); if ((resp.StatusCode == System.Net.HttpStatusCode.BadRequest) || (resp.StatusCode == System.Net.HttpStatusCode.InternalServerError)) { var error = MagentoError.FromJson(cont); if (error != null) _logger.LogError($"The media {mediaId} of product {productId} cannot be deleted for the following reason {error.code} - {error.message} Stack Trace - {error.trace} Http Status Code - {resp.StatusCode}"); } } while (tentatives <= 10); return resp; } public async Task DeletePhotoAsync(string productId) { HttpResponseMessage resp = null; int tentatives = 1; string deleteUrl = $"{_settings.GetMagentoProductsUrl()}/{productId}/media/all"; //Trace.WriteLine(deleteUrl); do { SetAuthenticationHeader(); tentatives++; resp = await _client.DeleteAsync(deleteUrl); if (resp.IsSuccessStatusCode) break; string cont = await resp.Content.ReadAsStringAsync(); if ((resp.StatusCode == System.Net.HttpStatusCode.BadRequest) || (resp.StatusCode == System.Net.HttpStatusCode.InternalServerError)) { var error = MagentoError.FromJson(cont); if (error != null) _logger.LogError($"The media of product {productId} cannot be deleted for the following reason {error.code} - {error.message} Stack Trace - {error.trace} Http Status Code - {resp.StatusCode}"); } } while (tentatives <= 5); return resp; } #endregion #region Configurable public async Task UpdateConfigurableAsync(string productId, string attributeId, string attributeLabel, int valueIndex) { SetAuthenticationHeader(); string url = $"{_settings.GetMagentoConfigurableProductsUrl()}/{productId}/options"; RootConfigurableOption root = new RootConfigurableOption() { Option = new Model.Magento.Option() { AttributeId = attributeId, Label = attributeLabel, IsuseDefault = true, Position = 0, Values = new Value[] { new Value() { ValueIndex = valueIndex } } }, }; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(root, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); var result = await _client.PostAsync(url, content); if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await result.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"The configurable product {productId} option cannot be updated. Error code {(int)result.StatusCode} Message {error?.message}"); } return result; } public async Task UpdateConfigurableChildrenAsync(string productId, string childProductId) { SetAuthenticationHeader(); //Delete children if any string url = $"{_settings.GetMagentoDefaultConfigurableProductsUrl()}/{productId}/children/{childProductId}"; var result = await _client.DeleteAsync(url); if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await result.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"Cannot delete the child product {childProductId} to the configurable product {productId}. Error code {(int)result.StatusCode} Message {error?.message}"); } //Update url = $"{_settings.GetMagentoDefaultConfigurableProductsUrl()}/{productId}/child"; RootConfigurableChild root = new RootConfigurableChild() { ChildSku = childProductId }; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(root, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); result = await _client.PostAsync(url, content); if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await result.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"Cannot link the product {childProductId} to the configurable product {productId}. Error code {(int)result.StatusCode} Message {error?.message}"); } return result; } #endregion #region Cross-Selling/Related Products public async Task CreateCrossSellingProductAsync(string productId, List linkedProducts) { SetAuthenticationHeader(); //Delete linked product if any using var response = await ReadProductAsync(productId); response.EnsureSuccessStatusCode(); var magentoProduct = await response.DeserializeAsync(); foreach (var link in magentoProduct.ProductLinks) { string deleteUrl = $"{_settings.GetMagentoRelatedProductsDeleteUrl(productId, link.LinkedProductSku)}"; using (var res = await _client.DeleteAsync(deleteUrl)) { if (res.StatusCode == HttpStatusCode.NotFound) continue; //404 don't consider and proceed to next if (res.StatusCode == System.Net.HttpStatusCode.BadRequest || res.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await res.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"Cannot delete the related product {link.LinkedProductSku} from the product {productId}. Error code {(int)res.StatusCode} Message {error?.message}"); } } } // foreach (string linkedProductId in linkedProducts) //{ // string deleteUrl = $"{_settings.GetMagentoRelatedProductsDeleteUrl(productId, linkedProductId)}"; // using (var res = await _client.DeleteAsync(deleteUrl)) // { // if (res.StatusCode == HttpStatusCode.NotFound) continue; //404 don't consider and proceed to next // if (res.StatusCode == System.Net.HttpStatusCode.BadRequest // || res.StatusCode == System.Net.HttpStatusCode.InternalServerError) // { // string cont = await res.Content.ReadAsStringAsync(); // var error = MagentoError.FromJson(cont); // _logger.LogError($"Cannot delete the related product {linkedProductId} from the product {productId}. Error code {(int)res.StatusCode} Message {error?.message}"); // } // } //} //Create realted product string url = $"{_settings.GetMagentoRelatedProductsUrl(productId)}"; var items = linkedProducts.Select((f, i) => new Item { LinkedProductSku = f, LinkType = "related", LinkedProductType = "simple", Position = i, Sku = productId }); RootCrossSelling root = new RootCrossSelling() { Items = items.ToArray() }; JsonSerializerOptions options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string inData = JsonSerializer.Serialize(root, options); HttpContent content = new StringContent(inData, Encoding.UTF8, "application/json"); var result = await _client.PostAsync(url, content); if (result.StatusCode == System.Net.HttpStatusCode.BadRequest || result.StatusCode == System.Net.HttpStatusCode.InternalServerError) { string cont = await result.Content.ReadAsStringAsync(); var error = MagentoError.FromJson(cont); _logger.LogError($"Cannot relate the products {string.Join(',', linkedProducts)} to the product {productId}. Error code {(int)result.StatusCode} Message {error?.message}"); } return result; } #endregion private void SetAuthenticationHeader() { if (string.IsNullOrEmpty(Token)) throw new ApplicationException("Application token cannot be empty"); var tokens = Token.Split("Bearer ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (tokens.Length != 1) throw new ApplicationException("Invalid token"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens[0]); } private async Task LogRequest(HttpContext context) { var body = context.Request.Body; context.Request.EnableBuffering(); var buffer = new byte[Convert.ToInt32(context.Request.ContentLength)]; await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); var bodyAsText = Encoding.UTF8.GetString(buffer); context.Request.Body = body; _logger.LogInformation($"Request:{Environment.NewLine}" + $"TraceId:{context.TraceIdentifier} " + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"Method: {context.Request.Method} " + $"QueryString: {context.Request.QueryString} " + $"Request Body: {bodyAsText}"); context.Request.Body.Position = 0; } private async Task LogResponse(HttpContext context) { var response = context.Response; response.Body.Seek(0, SeekOrigin.Begin); string text = await new StreamReader(response.Body).ReadToEndAsync(); response.Body.Seek(0, SeekOrigin.Begin); _logger.LogInformation($"Response :{Environment.NewLine}" + $"TraceId:{context.TraceIdentifier} " + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"QueryString: {context.Request.QueryString} " + $"Response Body: {text}"); } private static string ReadStreamInChunks(Stream stream) { const int readChunkBufferLength = 4096; stream.Seek(0, SeekOrigin.Begin); using (var textWriter = new StringWriter()) { using (var reader = new StreamReader(stream)) { var readChunk = new char[readChunkBufferLength]; int readChunkLength; do { readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength); textWriter.Write(readChunk, 0, readChunkLength); } while (readChunkLength > 0); return textWriter.ToString(); } } } }